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

DBとLaravelの接続設定【備忘録】

はじめに

PHP/Laravelの勉強にて備忘録として

前提

MySQLがインストールされていること

LaravelのDB接続設定ファイル

config/database.php を開く

config/database.php
/*中略*/
    'default' => env('DB_CONNECTION', 'mysql'),
/*中略*/

となっている。

env('DB_CONNECTION', 'mysql')
上記のenvはDB_CONNECTIONが設定されていればDB_CONNECTION
設定されていなければMySQLを使用という意味である

ここでLaravelの環境変数を見てみる。

tinker(php artisan tinker)を起動し、 print_r($_ENV)を入力

$ php artisan tinker
$ print_r($_ENV)
    <中略>
    [APP_DEBUG] => true
    [APP_LOG_LEVEL] => debug
    [APP_URL] => http://localhost
    [DB_CONNECTION] => mysql
    [DB_HOST] => 127.0.0.1
    [DB_PORT] => 3306
    <中略>

DB_CONNECTIONはmysqlになっていることを確認。
ではLaravelの環境変数を決めているのは誰か。

プロジェクトトップにある.envファイルを開く

.env
<中略>
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
<中略>

これの DB_CONNECTION=mysql となっているところを使用したいDBに指定する。
今回はMySQLを使用するのでそのまま

config/database.php
        /*中略*/
        'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],
        /*中略*/

config/database.php で自分が使用するmysql(今回は)の詳細設定をする

重要なのはdatabase, username, passwordで、すべenvなので.envで編集する

.env
<中略>
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
<中略>

今のままではDB名、ユーザ名、パスワードが上記のままなので変える。

.env
<中略>
DB_DATABASE=test
DB_USERNAME=root
DB_PASSWORD=
<中略>

上記のようにするとDB名がtest ユーザ名がrootでパスワード無しでログインする。

testデータベースを作成する
$ sudo service mysqld start
$ mysql -u root
mysql> CREATE DATABASE `test`;
データベースが接続されているか確認
$ php artisan tinker
>>> DB::connection();
=> Illuminate\Database\MySqlConnection {#2891} 
タイムゾーン設定

タイムゾーンの設定を「東京(Asia/Tokyo)」にする
レコード保存時に時間情報が設定したタイムゾーンの時間で保存される

config/app.php
    'timezone' => 'Asia/Tokyo',

以上

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

Laravelで複数の宛先にメールを送信するときにつまづいた。

環境

Laravel 5.5

背景

仕事で、とあるアクションを行った時に複数の宛先にメールを送る方法がわからずに四苦八苦してしまった。

問題点

app/Mail/sendmail.php
public function build()
    {
        return   $this->from('XXXXfromXXXX@gmail.com')
                ->subject('テスト送信完了')
                ->to(????)
                ->view('emails.sendmail');
    }

初めは直接->to()の中に記述していたが、エラーでうまくいかず...

見るべき場所

途中Maiableを継承している場所を確認すればいいことに気づいた。

app/Mail/sendmail.php
use Illuminate\Mail\Mailable;

class OrderShipped extends Mailable
{

illuminateなのでvendor以下のMailableを確認する。

vendor\laravel\framework\src\Illuminate\Mail\Mailable.php
/**
     * The "to" recipients of the message.
     *
     * @var array
     */
    public $to = [];

変数$toは配列で初期化されていることを発見。
(中の処理を詳しく見たい方はvendor以下のMailableクラスを参考にしてください。)

問題解決

配列を作成して、引数として渡す方法で解決した。

app/Mail/sendmail.php
public function build()
    {
        $to = array('XXXto1XXX@gmail.com','XXXto2XXX@yahoo.co.jp');

        return   $this->from('XXXXfromXXXX@gmail.com')
                ->subject('テスト送信完了')
                ->to($to)
                ->view('emails.sendmail');
    }

気づき

継承しているクラスを確認するのが大事。

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

AmazonインセンティブAPI実装(Amazonギフト券)

経緯

Amazonギフトカード実装をお願いされ、気軽にokを出したものの、概要がとってもわかりずらく、かつ参考文献が全くなく、意外と四苦八苦したので、同じ悩みの方が出ないように投稿する事にしました。
なお、事前に用意されたphpのサンプルコードからLaravelに実装し直す、という流れが一番良いと思いますが、あまりにも面倒だったのと時間との勝負だった為、決してベストプラクティスでないことはご了承ください。。

本題

登録→確認方法

基本的には導入マニュアルがあるので、これに沿って開発をしていきます。
AmazonインセンティブAPIとの統合プロセスについて
登録した後、サンドボックス環境で正常に動作するか確認

サンプルコード参照

FAQ付録にサンドボックス環境のサンプルコードがあります。
今回はphp/Laravelで実装予定だったので、ここのphpのサンプルコードを読み、htmlSDKv2_php_cr⁩/htmlSDKv2_php/rollups⁩/function.phpにキー発行実装があります。

実装方法

  1. fuciton.phpのみ取り出し、app/Library配下に設置
  2. controllerからのinvokeRequest を指定
  3. Laravel実装の為、globalで引っ張ってきてるところをconfigにまとめ、アクセスキー等は.envに設定
  4. 実装完了。。。。

注意点としては、サンドボックス環境とプロダクション環境の2つともアクセスキーを発行しなければいけません。

余談(メイン)

今回は、jsonで返したので、XML2Array.phpは使用していません。
今回の僕の実装の流れとしては、
登録した後、サンドボックス環境でidが合っているか確認するところまでは順調でした。
ただresponseを叩いた時に、雲行きが怪しくなりました。responseの情報以外に沢山の情報があり、気づいてしまったんです。

あれ、、???これ、、、、api叩くだけじゃダメじゃね、、、?

そうです。僕は必要なrequestを提げて、apiを叩けばそれで実装が完了すると思っていました。
そんな甘い世界ではありません。このapiではSignatureキーを採用しているので、あちらで発行するキーと同じ発行をしなければなりませんでした。
その実装が開発マニュアルに書いてあるのかと思いきや、そのような実装は見つからずましてや必要なrequestすら曖昧すぎてよくわからなかったです。泣きそうでした。

そんな泣きそうな中、FAQ付録にサンドボックス環境においてのサンプルコードを見つけ、phpのサンプルコードのhtmlSDKv2_php_cr⁩/htmlSDKv2_php/rollups⁩/function.phpにたどり着きました。。

実装する人は少ないと思いますが、今後同じように実装する人を救えますように。。

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

2019年の勉強会を集計してみた〜都道府県別&キーワード別〜

この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019 15日目の記事になります。

はじめに

皆さん勉強会はお好きでしょうか?
私は興味のあるものを見つけては社内外の開催に関わらず参加しています
それまで知らなかった技術や知見に出会えることがその醍醐味かなと思っています?

さて、2019年はどのような勉強会がどこで多く開催されたのでしょうか?
個人的に気になって集計してみたので結果とそのやり方を公開します!

結果

2019年に開催された勉強会(イベント)の集計結果です。

都道府県別開催数

まずは都道府県別の開催数です。
東京の開催数が多すぎて単純な数値にすると他県の棒が潰れてしまうため対数軸にしています。
さすが東京!

スクリーンショット 2019-12-15 18.00.14.png

割合で見てもやはり東京が際立ちます。

スクリーンショット 2019-12-15 18.17.11.png

上位10県の数値は以下の通りです。

都道府県 開催数
東京都 23486
大阪府 4360
愛知県 1812
福岡県 1294
神奈川県 1012
兵庫県 951
京都府 681
北海道 678
沖縄県 395
広島県 291

ソフトウェア業,情報処理・提供サービス業,インターネット附随サービス業の事業従事者数との割合

都道府県別のエンジニア1人あたりの勉強会開催数を出そうと思いました。
しかし、都道府県別のエンジニア人口のデータが見つからなかったため、「平成 30 年特定サービス産業実態調査報告書(経済産業省)」から以下の業種の県別事業従事者数を合計した値を参考値として使いました。
なお、令和元年の数値は本記事執筆時点では公開されていません。

  • ソフトウェア業
    • 日本標準産業分類に掲げる小分類 391-ソフトウェア業に属する業務を主業として営む 事業所
  • 情報処理・提供サービス業
    • 日本標準産業分類に掲げる小分類 392-情報処理・提供サービス業に属する業務を主 業として営む事業所
  • インターネット附随サービス業
    • 日本標準産業分類に掲げる小分類 401-インターネット附随サービス業に属する業務を 主業として営む事業所

結果は以下の通りです。割合 = 開催数 / (上記3業種の事業従業者数の合計)です。

スクリーンショット 2019-12-15 23.55.43.png

奈良の数値が高いのは少し意外でした。島根はRubyゆかりの地であることが影響しているのかもしれません。

タグ(キーワード)別開催数

勉強会のタイトルや内容説明を基にタグ付けを行い集計しました。
なお、1つの勉強会に対して複数のタグがつくことがあります。

スクリーンショット 2019-12-15 20.16.59.png

勉強会プログラミング初心者Web入門初心者向けなどが多くの割合を締めていますが、もう少し具体的な技術の傾向を見たかったので、これらを除外して再度集計しました。

スクリーンショット 2019-12-15 20.15.52.png

これを見るとAIやIoTなどの近年のITトレンドが反映されていると感じます。

都道府県別タグ上位

東京、大阪、愛知でタグの多いものを見てみましょう。
なお、勉強会プログラミング初心者Web入門初心者向けは除外しています。

東京

スクリーンショット 2019-12-15 20.27.40.png

大阪

スクリーンショット 2019-12-15 20.29.53.png

愛知

スクリーンショット 2019-12-15 20.31.22.png

例えば愛知だけはPHPが4位に入っているなど、4位以下で少しづつ地域の違いが出ているように見えます。

注意事項

以上の結果について、後述の集計方法に記載の通り厳密に勉強会だけを集計していない点はご了承ください。

集計方法

ここからはどうやって集計したかを説明します。

情報源

勉強会の情報は以下のサービスが提供しているAPIから取得しています。

今回の集計では勉強会=これらのサイトで登録されたイベントとしています。
厳密には懇親会などの勉強会でないイベントも含まれています。
また、ITに関係ないものも存在しますが、今回の集計では除外していません?

タグの一覧はQiitaのAPIから取得しています
今回はQiitaの記事数が多いタグ順に300件を取得するようにしました。

方法

データベースにイベント情報を保存し、Metabaseで集計をしています。
テーブルは以下の通りです。

  • prefectures:都道県マスタ
  • events:勉強会(イベント)
  • tags:タグマスタ
  • event_tag:勉強会とタグの多対多関係を表現するための中間テーブル

スクリーンショット 2019-12-15 13.10.28.png

処理

勉強会の情報をテーブルに保存するために以下の処理をしています。

  1. 都道府県マスタのデータ登録
  2. タグ一覧を取得
  3. 勉強会情報を取得
  4. 勉強会がどの都道府県で開催されているか判定する
  5. 勉強会にタグ付けをする

今回は現在絶賛学習中のLaravelで作ってみました。
以下からはコードの話が中心になるので、興味のある方は読んでいただけると幸いです。

都道府県マスタのデータ登録

都道府県データはlaravelのseederで初期登録できるようにしました。

# seederファイルの作成
$php artisan make:seeder PrefectureSeeder

seederの内容です。
なお、prefectures.idはJISで規定されている都道府県コードの値にしています。

<?php
use Illuminate\Database\Seeder;
use App\Models\Prefecture;

class PrefectureSeeder extends Seeder
{
    public function run()
    {
        $prefectures = [
            [1, '北海道', 'ホッカイドウ', 'hokkaido'],
            [2, '青森県', 'アオモリケン', 'aomori'],
            [3, '岩手県', 'イワテケン', 'iwate'],
            //省略
            [45, '宮崎県', 'ミヤザキケン', 'miyazaki'],
            [46, '鹿児島県', 'カゴシマケン', 'kagoshima'],
            [47, '沖縄県', 'オキナワケン', 'okinawa']
        ];

        foreach ($prefectures as $prefecture) {
            Prefecture::updateOrCreate([
                'id' => $prefecture[0],
                'name' => $prefecture[1],
                'kana_name' => $prefecture[2],
                'english_name' => $prefecture[3]
            ]);
        }
    }
}

これで$php artisan db:seed --class PrefectureSeederを実行するとデータが登録されるようになります。

タグ一覧を取得

コマンドでQiitaのAPIからタグを取得するようにします。
$ php artisan make:command FetchQiitaTagsFromApiを実行するとコマンドの雛形となるファイルが作成されます。

<?php
namespace App\Console\Commands;
use App\Services\QiitaService;
use Illuminate\Console\Command;
use Exception;

class FetchQiitaTagsFromApi extends Command
{
    protected $signature = 'fetch:tags';//コマンド名

    protected $description = 'fetch Qiita tags from api';//説明

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        try {
            QiitaService::fetchTagsFromApi(1, 3);
        } catch (Exception $e) {
            $this->error($e->getMessage());
        }
    }
}

QiitaServiceではAPIで取得した結果を、DBに保存しています。
なお、$jsonArray = ApiClient::getJsonArray($url, $params, 60000);の60000はAPIアクセスのディレイ時間(ms)です。
値はAPIの利用規約やrobots.txtに従って設定します。

<?php
namespace App\Services;
use App\Repositories\TagRepository;
use App\ApiClients\ApiClient;

class QiitaService
{

    const QIITA_HOST = 'https://qiita.com';
    const API_PATHS = ['tags' => '/api/v2/tags',];

    public static function fetchTagsFromApi(int $startPage = 1, int $endPage = 10)
    {

        $url = self::QIITA_HOST . self::API_PATHS['tags'];
        $params = ['per_page' => '100', 'sort' => 'count'];
        $tagRepository = app(TagRepository::class);

        for ($i = $startPage; $i <= $endPage; $i++) {
            $params['page'] = $i;
            $jsonArray = ApiClient::getJsonArray($url, $params, 60000);
            $tagNames = array_column($jsonArray, 'id');
            $tagRepository->saveTagsFromNames($tagNames);
        }
    }
}

ApiClientはAPIアクセスを実行するクラスです。
HTTPクライアントのライブラリGuzzleを使用しています。
後述するイベント情報の取得でもこのクラスを使用しています。

<?php
namespace App\ApiClients;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;

class ApiClient
{
    public static function getJsonArray(string $url, array $queryParams, float $delay = 0.0, array $headers = [])
    {
        try {
            $client = new Client();
            $res = $client->request('GET', $url, ['query' => $queryParams, 'delay' => $delay, 'headers' => $headers]);
            return json_decode($res->getBody(), true);
        } catch (ClientException $e) {
            throw $e;
        }
    }
}

勉強会情報を取得

やっていることはタグ一覧を取得でやっていることとほぼ同じです。(APIから情報取得->DBに保存)
ここではDoorkeeperのイベント取得処理を例に出していますが、他のサイトの場合も行っていることは基本的に同じです。

<?php
namespace App\Console\Commands;
use App\Services\DoorkeeperService;
use Carbon\Carbon;
use Illuminate\Console\Command;

class FetchDoorKeeperEventsFromAPI extends Command
{

    protected $signature = 'fetch:doorkeeper
    {--p|page= : the page offset of the results }
    {--l|locale=ja : the localized text for an event }
    {--o|sort=starts_at : The order of the results(One of published_at, starts_at, updated_at) }
    {--s|since=-1month : Only events taking place during or after this date will be included (YYYYMMDD) }
    {--u|until= : Only events taking place during or before this date will be included (YYYYMMDD) }
    {--k|keyword= : Keyword to search for from the title or description fields }
    {--g|expand_group=0 : Expands the group object(0:false, 1:true) }
    {--a|all=1 : paging to get all records(0:false, 1:true) }';

    protected $description = 'fetch doorkeeper events from api and store to DB';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $queryParams = $this->getQueryParamFromOptions();
        DoorkeeperService::fetchEventsFromAPI($queryParams, !!$this->option('all'));
    }

    private function getQueryParamFromOptions()
    {

        $options = $this->options();

        //除外オプション
        $excludeOptions = ['expand_group', 'all'];
        foreach ($excludeOptions as $excludeOption) {
            unset($options[$excludeOption]);
        }

        //オプションとパラメータの対応
        $optionToParam = [
            'page' => 'page',
            'locale' => 'locale',
            'sort' => 'sort',
            'since' => 'since',
            'until' => 'until',
            'keyword' => 'q',
            'expand_group' => 'expand[]',
        ];

        $params = [];
        foreach ($options as $option => $value) {
            if ($value) {
                $params[$optionToParam[$option]] = $value;
            }
        }

        if ($options['since']) {
            $jst = new Carbon($options['since']);
            $params['since'] = $jst->setTimezone('UTC')->format('Y-m-d\TH:i:s\Z');
        }

        if ($options['until']) {
            $jst = new Carbon($options['until']);
            $params['until'] = $jst->setTimezone('UTC')->format('Y-m-d\TH:i:s\Z');
        }

        if ($this->option('expand_group')) {
            $params[$optionToParam['expand_group']] = 'group';
        }

        return $params;
    }
}

DoorkeeperServiceではDoorkeeperのイベント情報の取得と保存を行います。
APIから返される情報に都道府県だけの情報は存在しないので、取得する際にPrefectureServiceで判定をしています。

<?php
namespace App\Services;
use App\Models\Event;
use Carbon\Carbon;
use App\ApiClients\ApiClient;

class DoorkeeperService
{
    private const EVENT_API_URL = 'https://api.doorkeeper.jp/events';

    static public function fetchEventsFromAPI(array $params, bool $isAllPage)
    {
        if ($isAllPage) {
            for ($p = 1; true; $p++) {
                $params['page'] = $p;
                $headers = ['Authorization' => 'Bearer ' . config('env.doorkeeper_api_token')];
                $jsonArray = ApiClient::getJsonArray(self::EVENT_API_URL, $params, 5000.0, $headers);
                if (empty($jsonArray)) {
                    break;
                }
                self::updateOCreateEventsFromAPIResult($jsonArray);
            }

        } else {
            $jsonArray = ApiClient::getJsonArray(self::EVENT_API_URL, $params, 5000.0);
            self::updateOCreateEventsFromAPIResult($jsonArray);
        }
    }

    static private function updateOCreateEventsFromAPIResult(array $eventJson)
    {
        foreach ($eventJson as $record) {
            $event = $record['event'];
            //住所、または緯度経度から都道府県を判定
            $prefecture = $event['address'] ? PrefectureService::getPrefectureFromAddress($event['address']) : null;
            if (!$prefecture && $event['lat'] && $event['long']) {
                $prefecture = PrefectureService::getPrefectureFromCoordinates($event['lat'], $event['long']);
            }

            Event::updateOrCreate(
                [
                    'site_name' => Event::DOORKEEPER,
                    'event_id' => $event['id']
                ],
                [
                    'title' => $event['title'],
                    'description' => $event['description'],
                    'event_url' => $event['public_url'],
                    'prefecture_id' => $prefecture ? $prefecture->id : null,
                    'address' => $event['address'],
                    'place' => $event['venue_name'],
                    'lat' => $event['lat'],
                    'lon' => $event['long'],
                    'started_at' => self::formatUtcTimeToMySqlDateTime($event['starts_at']),
                    'ended_at' => self::formatUtcTimeToMySqlDateTime($event['ends_at']),
                    'limit' => $event['ticket_limit'],
                    'participants' => $event['participants'],
                    'waiting' => $event['waitlisted'],
                    'group_id' => $event['group'],
                    'event_created_at' => self::formatUtcTimeToMySqlDateTime($event['published_at']),
                    'event_updated_at' => self::formatUtcTimeToMySqlDateTime($event['updated_at']),
                ]
            );
        }
    }

    static private function formatUtcTimeToMySqlDateTime(string $date)
    {
        $carbon = new Carbon($date);
        return $carbon->setTimezone('JST')->format('Y-m-d H:i:s');
    }
}

勉強会がどの都道府県で開催されているか判定する

getPrefectureFromAddressは住所から都道府県を判定する処理に使います。
getPrefectureFromCoordinatesHeartRails Geo APIで緯度経度から都道府県を判定する処理に使います。

<?php
namespace App\Services;
use App\Models\Prefecture;
use App\ApiClients\ApiClient;

class PrefectureService
{

    public static function getPrefectureFromAddress(string $address = ''): ?Prefecture
    {
        $prefectures = Prefecture::select(['id', 'name'])->get();
        foreach ($prefectures as $prefecture) {
            if (mb_strpos($address, $prefecture->name) !== false) {
                return $prefecture;
            }
        }
        return null;
    }

    public static function getPrefectureFromCoordinates(float $lat, float $lon): ?Prefecture
    {
        $url = config('const.geoapi_url');
        $params = [
            'method' => 'searchByGeoLocation',
            'x' => $lon,
            'y' => $lat,
        ];
        $jsonArray = ApiClient::getJsonArray($url, $params, 5000.0);

        if (isset($jsonArray['response']['error'])) {
            return null;
        }

        //1件目のデータの都道府県名
        $firstPrefectureName = $jsonArray['response']['location'][0]['prefecture'];
        $prefecture = Prefecture::where('name', $firstPrefectureName)->first();
        return $prefecture;
    }
}

ただ、これらの処理で判定できないもの(開催住所が市や区ではじまり、緯度経度情報もない)は手動でデータを整えました^^;
政令指定都市や東京都の区くらいはマスタ化して判定を自動化してもよかったかもと思いました。
また、判定できなかったものの中にはそもそもZoomなどで完全にオンラインで開催されている場合がありました。
将来的にはどこに住んでいる人でも参加しやすいオンライン形式が増えていくのかもしれません。

勉強会にタグ付けをする

APIで取得できるイベント情報にタグ情報はないため自前で付与します。
イベント情報にはタイトル、キャッチ、説明があり、これらの中にタグに該当するキーワードがあるかで判定しています。
ただし、例えばRGoの場合、単純にこれらの文字列があるかで判定すると誤ってタグ付けされる可能性が高いため正規表現で検索するようにします。Goの場合は/golang|go言語/uiとしました。
他にもAI/AI/uとして小文字を含まないようにするなどキーワードごとに調整しています。

この正規表現はDBのtags.patternに保存しています(ここは手作業^^;)
また、説明は全部でなく最初の200文字のみを検索対象にしています。

<?php
namespace App\Console\Commands;
use App\Models\Tag;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
use App\Models\Event;
use Illuminate\Support\Facades\DB;

class TaggingForEvents extends Command
{
    protected $signature = 'tagging:events';

    protected $description = 'tagging for events';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        $characterLimit = 200; //descriptionのタグの探索範囲
        $events = Event::all();
        $tags = DB::table('tags')->orderBy('id')->limit(400)->get();

        foreach ($events as $event) {
            $tagIds = [];
            foreach ($tags as $tag) {
                $pattern = $tag->pattern;
                $description = mb_substr(strip_tags($event->description), 0, $characterLimit);
                if (preg_match($pattern, $event->title) || preg_match($pattern, $event->catch)
                    || preg_match($pattern, $description)) {
                    $tagIds[] = $tag->id;
                }
            }
            $event->tags()->sync($tagIds);
        }
    }
}

Metabaseでの集計

集計結果でお見せしたグラフはMetabaseで作成しています。
dockerが使える環境であれば、以下のコマンドで簡単にMetabaseを実行することができます。
ただし、このやり方の場合はコンテナを止めるとMetabaseで保存した集計は全て消えるのでご注意ください。

$ docker run -d -p 12345:3000 --name metabase metabase/metabase

後はブラウザからlocalhost:12345にアクセスすれば、Metabaseの画面が表示されます。
画面上でユーザ登録と接続先DBの情報を入力してやればすぐに使い始めることができます。
ちなみにコンテナからlocalホストへ接続する場合、macであればdocker.for.mac.localhostをwindowsであればdocker.for.win.localhostをホスト名に指定します。

まとめ

  • ATND、connpass、DoorKeeperから勉強会(イベント)の情報を取得し集計した。
  • イベントに対して、Qiitaのタグでタグ付けした。
  • 東京、大阪、愛知が都道府県別のイベント開催数のトップ3
  • 全国的にAI(Python、機械学習、AIなど)に関連するイベントが多く開催された。

お知らせ

エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!

Qiita Jobsのエイチーム引越し侍社内システム企画 / 開発チーム社内システム開発エンジニアを募集!からチャットでご質問いただくことも可能です!

明日

明日は後輩の面倒見の良さが評判の @umesour さんの記事です!
どんな記事を書いてくださるのかとても楽しみです!!
乞うご期待!!

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

Laravelで.envに問題がある場合はデプロイ時にエラーにする

前置き

今時のWebアプリケーション開発では、開発環境と本番環境はほとんど同じ構成になっている上、VCSで一元管理されているソースコードを自動デプロイツールでデプロイするので、デプロイ時に問題が起こることはなかなかありません。

ですが中にはどうしても開発環境と本番環境で同等にできず、その上VCSで管理することもできず、デプロイ時に問題を起こしがちなものもあります。

Laravelにおいては.envがそれです。このファイルは環境ごとに変わるデータを入れるのが目的のため開発環境と本番環境で同じものを使うことは基本的にできない上、APIのキー等の秘密情報を入れることも多く、VCSで管理することもできません。

.env
# .envの一部。データベースの接続情報などが記載されている。
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

実際、laravel newして自動生成される.gitignoreではこのファイルが除外リストに入っています。

一度用意した後は頻繁に変更するものではありませんが、それでもたまにこのファイルに入れておきたいデータが増えることがあります。

そのような変更の後には.envを手動で変更した上でデプロイする必要があります。.envの変更を忘れると、場合によっては致命的な問題になります。外部の決済システムを変更した後、新しい決済システム用の認証情報がない場合、どうなるでしょう?

本題

今日はLaravel Env ValidatorLaravel Deployerを使ってこのような問題をできる限り防ぐための方法をご紹介します。

Laravel Env Validatorは、.env(というか環境変数自体)をLaravelのヴァリデーションルールを使ってヴァリデーションする機能を提供するパッケージです。

config/laravel-env-validator.phpに以下のように記述して、

config/laravel-env-validator.php
<?php

return [
    'rules' => [
        'APP_ENV' => 'in:local,production',
        'APP_KEY' => 'required',
        'APP_DEBUG' => 'boolean',
    ],
];

php artisan config:env-validatorと実行すると、環境変数が検証され、たとえば上記の場合だと、APP_ENVがtestingだったり、APP_KEYが未設定の状態だったりすると以下のようなエラーを出します。

In EnvValidator.php line 31:

  The .env file has some problems. Please check config/laravel-env-validator.php
  The selected APP_ENV is invalid.
  The APP_KEY field is required.

注意としてはconfigとして記述するため、Illuminate\Validation\Ruleのルールを使うようなことはできません。というか実際にはできないことはないのですが、php artisan config:cacheでエラーになってしまうため実用的ではありません。シリアライズできないんですね。ですので複雑なルールを書くような場合は、カスタムルールを用意するのがよさそうです。

さて、これだけでも便利ですが、できればデプロイ時に自動でヴァリデーションさせたいところです。自動で防げるミスは防ぎたいですよね。

そこでLaravel Deployerで以下のように設定します。

まず、php artisan config:env-validatorを実行するタスクをレシピに書きます。

recipe/artisan-config-env-validator.php
<?php

namespace Deployer;

desc('validate .env');
task('artisan:config:env-validator', artisan('config:env-validator'));

次にこのレシピをincludeします。config/deploy.phpは、php artisan deploy:initで事前に用意しておいてください。

config/deploy.php
    'include' => [
        'recipe/artisan-config-env-validator.php',
    ],

最後に、このタスクを実行するよう設定します。

config/deploy.php
        'ready' => [
            'artisan:storage:link',
            'artisan:view:clear',
            'artisan:cache:clear',
            'artisan:config:env-validator',
            'artisan:config:cache',
            'artisan:migrate',
        ],

artisan:config:cacheの前に実行するのが肝です。config:cacheされた後は環境変数は読み込まれないため、環境変数を直接見ているLaravel Env Validatorのヴァリデーションは正常に動作しません。

後はその他必要なデプロイのための設定をして、デプロイするだけです。.envをヴァリデーションして問題なければデプロイが成功しますし、そうでなければ失敗します。これにより、適切なルールを用意している限り、.envの記述ミスで問題のあるデプロイをしてしまうことがなくなります。

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

Laravel構築

Apacheインストール

# yum -y install httpd (yオプションは問い合わせに対して全て「y」で応答)

以下のコマンドで確認
# httpd -v
Server version: Apache/2.4.6 (CentOS)
Server built:   Aug  8 2019 11:41:18

起動確認

# systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
     Docs: man:httpd(8)
           man:apachectl(8)

Active: inactive (dead) は停止

Apache起動

# systemctl start httpd

PHP インストール

  • PHP5.4

CentOS7の標準リポジトリのPHPバージョンが5.4
(古い...サポート切れてるやろ:grin:
PHP5.4以降をインストールする場合は、『PHP7.3』を参照(今回は7.3を例に)

# yum -y install php

以下のコマンド確認
# php -v
PHP 5.4.16 (cli) (built: Oct 30 2018 19:30:51) 
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2013 Zend Technologies
  • PHP7.3

EPELリポジトリをインストール

# yum -y install epel-release

REMIリポジトリをインストール(リポジトリの依存関係によりEPELリポジトリも同時にインストールされる)

# yum -y install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

PHP7.3をインストール

# yum -y install --enablerepo=remi,remi-php73 php php-mbstring php-xml php-xmlrpc php-gd php-pdo php-pecl-mcrypt php-mysqlnd php-pecl-mysql php-zip php-pecl-zip zip unzip git

以下のコマンドで確認
# php -v
PHP 7.3.11 (cli) (built: Oct 22 2019 08:11:04) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies

CentOS7からMariaDBが標準となっているため、
MySQLをインストールするためには削除する必要がある。

# yum remove mariadb-libs
# rm -rf /var/lib/mysql

MySQLのリポジトリをインストール

# yum localinstall -y https://dev.mysql.com/get/mysql80-community-release-el7-2.noarch.rpm

MySQLインストール

# yum install -y mysql-community-server

以下のコマンドで確認
# mysqld --version
/usr/sbin/mysqld  Ver 8.0.18 for Linux on x86_64 (MySQL Community Server - GPL)

MySQLの状態を確認(デフォルトでは停止のはず)

# systemctl status mysqld
● mysqld.service - MySQL Server
   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
   Active: inactive (dead)
     Docs: man:mysqld(8)
           http://dev.mysql.com/doc/refman/en/using-systemd.html

Active: inactive (dead) は停止

MySQLを起動

# systemctl start mysqld

以下のコマンドで確認
# systemctl status mysqld
● mysqld.service - MySQL Server
   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)
   Active: active (running) since Mon 2019-11-04 00:23:13 JST; 8s ago
     Docs: man:mysqld(8)
           http://dev.mysql.com/doc/refman/en/using-systemd.html
  Process: 5342 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS)
 Main PID: 5417 (mysqld)
   Status: "Server is operational"
   CGroup: /system.slice/mysqld.service
           └─5417 /usr/sbin/mysqld

Nov 04 00:22:59 localhost.localdomain systemd[1]: Starting MySQL Server...
Nov 04 00:23:13 localhost.localdomain systemd[1]: Started MySQL Server.

Active: active (running) で起動を確認

MySQLの自動起動設定を確認する(デフォルトは有効のはず)

# systemctl is-enabled mysqld
enabled

enable なので有効

変更は以下のコマンドで行う

無効にする
# systemctl disable mysqld
Removed symlink /etc/systemd/system/multi-user.target.wants/mysqld.service.

有効にする
# systemctl enable mysqld
Created symlink from /etc/systemd/system/multi-user.target.wants/mysqld.service to /usr/lib/systemd/system/mysqld.service.

composerをインストール

# curl -sS https://getcomposer.org/installer | php
コマンド設置
# mv composer.phar /usr/local/bin/composer

以下のコマンドで確認
# composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.9.1 2019-11-01 17:20:17

Laravelのインストーラーをダウンロード

# composer global require "laravel/installer"

プロジェクトを配置する場所のディレクトリのパーミッションを変更
(一旦、実行・書込・読込を許可)

# chmod 777 /var/www/html/

"sample"というプロジェクトを作成

$ composer create-project --prefer-dist laravel/laravel /var/www/html/sample

ドキュメントルート設定

# vi /etc/httpd/conf.d/***.conf(任意の名前)
<VirtualHost *:80>
    DocumentRoot /var/www/html/sample/public
    RewriteEngine On
    <Directory "/var/www/html/sample/public">
        AllowOverride All
    </Directory>
</VirtualHost>

confを修正したのでhttpd再起動

# systemctl restart httpd

ログを書込権限がないので、パーミッションを変更
(一旦、実行・書込・読込を許可)

$ chmod -R 777 /var/www/html/sample/storage/

http://192.168.33.10 にアクセス
(Vagrantで構築したため、デフォルトの192.168.33.10)

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

PHPerが久しぶりにRubyを書いて思ったこと

内容

  • PHPer(Laravler)が感じたRuby(Rails)への感想

私の背景

  • PHP成分が多い人生
    • 初期だとFuelPHPとか触ってた
    • ここ数年はLaravel使ってた
  • Rubyは2011年頃に遊びで触ってた
    • メタプログラミングRubyとか読んで興奮した記憶はある
    • この当時のRailsを触って「ほーん、なるほど」、みたいに思った記憶もある
    • 仕事では結局使わなかったけど、Project Euler をRubyで解いたりしてみてた

といった感じの背景です。
最近Railsで仕事することになったので、PHPer(というかLaraveler)が感じたRailsに対するギャップをいくつか書きたいと思います。
すでにRailsのプロジェクトがあり、そこにJOINした感じです。

Autoloadがわからない

PHPのロード

require_once __DIR__ . '/path/to/file.php'

Rubyのロード

require "path/to/file"

ここまではなんの問題もなくわかるんですが、autoloadがわからない。

PHPの場合

namespace \Foo;

use \Foo\Bar\Hoge;

class Xxx extends Hoge {
}

みたいな感じで「利用するクラスのフルパスを書かなければならない」っていう制約があるので、面倒ではあるんだけど「 (appRoot)/Foo/Bar/Hoge.php にあるんだな」っていうことが分かる。

最近はほとんどPSR-4のautoloadが多いと思うので、PSR-4読めばいいと思う

参考: https://qiita.com/inouet/items/0208237629496070bbd4

Ruby(Rails)の場合

module Foo do
  class Xxx extends Hoge
  end
end

なんなん!?!?!?急に Hoge どこから出てきたん!?!?!?!?
Hogeって誰なの!?!?!?!

ってすごく思った。

https://qiita.com/eggc/items/ae09d32df5d994522ca1
https://qiita.com/tachiba/items/5b293ca8e9430b9bd07e

最新のバージョンでどう動くかはわからんですけど「Railsが頑張って探してるんや」って言われて納得。

納得すると同時に「あぁ〜〜〜パスを明示したいんじゃ〜〜〜」という気持ちになる。

カッコがない

追記: 以下「カッコがない」の項については私の勘違いが多分に含まれてたのでスルーしてください。
コメントにてご指摘いただき、ありがとうございました :bow:

PHPのこれが

piyo(bar(foo($x)));

Rubyだとこうなる

piyo bar foo x

いいんだけど、「どんだけカッコ嫌いなん???」って気持ちで溢れた。
foo の引数が3つあるときは

piyo bar foo 1,2,3

ってなって、なんか、分かるんだけど、なんやねんカッコ書きたいやんって気持ちになる

piyo(bar(foo(1,2,3))

シックリくる〜〜!

って思うけどRobocopとか言うやつが「カッコ書くなや」みたいにいってくる。もうやだ。

blockに感じる違和感

[1,2,3].reject {|n| n > 2}.map {|n| n * 2}.map &:to_s

これと

[1,2,3].reject do |n|
  n > 2
end.map do |n| 
  n * 2
end.map &:to_s

これって同じ感じだけど、なんで構文2つ分けたん。どっちかひとつでいいじゃんね!

end.map とかにすごくなにか感じる。

returnが無い

def hoge(x)
  if x > 5
    return x * 3
  end
  x
end

returnつけたりつけなかったり!!!なんなの!!!

不要なのにreturnつけるとRobocopが「てめぇreturnつけてんじゃねぇよ!」とか言ってくるしもう。もう。

RubyDocが無い・・・?

最近のPHPは割と型がしっかり・・・とは言わないけど、少なくとも宣言する際の不便はかなりなくなってきた。
コンパイル用っていうよりは、プログラミングするときの手助けとしての型、という側面でとても便利。
PHP7より前の時代においてもPHPは PHPDoc で何となく「型が何なのか明示しろや」文化が多少なりとあった。

class Foo {
  /**
   * @param integer $x
   * @param integer $y
   * @return integer
   */
  public function something($x, $y) {
    return $x * $y;
  }
}

みたいな感じで、「俺は型を宣言したいんじゃ!!!」という欲求に応えてくれた。

class Foo {
  public function something(int $x, int $y): int {
    return $x * $y;
  }
}

最近はこんな感じにスマートにかけてよかったね^^という気持ち

JSにもTypeScript以前にJSDocあったし、当然RubyにもRubyDocがあると思ってた。

そしたら、RubyDocはなんか違うやつだった。ドキュメント作るやつ。
ちゃうねん、そうじゃないねん。

一応RDLってやつがあるみたいだけど、なんか違うというか・・・too muchというか・・・。

https://qiita.com/baban/items/0f782691b6e6e7213453

「Rubyの思想・文化的にそういう型宣言とは仲良くない」的なことだってのはわかってるんですが、「引数が何くるのかわからない状況ってのが怖くて、「え・・・string渡してくるバカがいたら死ぬの・・・?」みたいな気持ちになる。「string渡してくるやつがアホい」っていう思想なんだろうけど。

まとめ

なんか他にもいろいろあったと思うんですけど、思い出した範囲で感じた違和感をかきました。

別にRubyをディスってるわけじゃなくて「全然Rubyの世界観に馴染めてない俺」っていう感じです。

ワンライナー書いたりとかRspecのDSL感とか、書いててシックリくることも多いのですが、なんかやっぱ型システム周りの思想の違いは大きいなぁと思ってます。
ダックタイピングって誰得なのかがまだわかってないので、しっくりくる日を待ち望んでます。

かしこ

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

(Laravel6.x)laravel/ui で 5.8 以前の make:auth のscaffolding する

Laravel6.x から make:auth コマンドが消え、react と vue のscaffolding に変わった??

php artisan make:auth

  Command "make:auth" is not defined.

以前から laravel/ui に認証のscaffolding が移行されたから、という情報は認識していたのですが、最近 「laravel/ui の make:auth(ui:auth) では npm が必要で react が defaultになったらしい。」 というのを聞いて公式を調べたところ
image.png

vue が前提となっているように見えるので、今のプロジェクトではSPA用のJSは使っていないため bladeのscaffoldings がこのまま廃止されてしまったら、使いずらくなるなと思ったのですが、調べると、laravel/uiでも 5.8 以前の make:auth のscaffolding することができました

0. 準備

laravel new make-auth-demo

1. laravel/ui を取得

composer require laravel/ui --dev

2. ui:auth --views を実行

php artisan ui:auth --views

image.png

image.png

ui/src/AuthCommand.php にがっつり記載がありました。

https://github.com/laravel/ui/blob/master/src/AuthCommand.php

ui/src/AuthCommand.php
<?php
namespace Laravel\Ui;
use InvalidArgumentException;
use Illuminate\Console\Command;
class AuthCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'ui:auth
                    { type=bootstrap : The preset type (bootstrap) }
                    {--views : Only scaffold the authentication views}
                    {--force : Overwrite existing views by default}';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Scaffold basic login and registration views and routes';
    /**
     * The views that need to be exported.
     *
     * @var array
     */
    protected $views = [
        'auth/login.stub' => 'auth/login.blade.php',
        'auth/passwords/confirm.stub' => 'auth/passwords/confirm.blade.php',
        'auth/passwords/email.stub' => 'auth/passwords/email.blade.php',
        'auth/passwords/reset.stub' => 'auth/passwords/reset.blade.php',
        'auth/register.stub' => 'auth/register.blade.php',
        'auth/verify.stub' => 'auth/verify.blade.php',
        'home.stub' => 'home.blade.php',
        'layouts/app.stub' => 'layouts/app.blade.php',
    ];
    /**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {
        if (static::hasMacro($this->argument('type'))) {
            return call_user_func(static::$macros[$this->argument('type')], $this);
        }
        if (! in_array($this->argument('type'), ['bootstrap'])) {
            throw new InvalidArgumentException('Invalid preset.');
        }
        $this->ensureDirectoriesExist();
        $this->exportViews();
        if (! $this->option('views')) {
            $this->exportBackend();
        }
        $this->info('Authentication scaffolding generated successfully.');
    }
    /**
     * Create the directories for the files.
     *
     * @return void
     */
    protected function ensureDirectoriesExist()
    {
        if (! is_dir($directory = $this->getViewPath('layouts'))) {
            mkdir($directory, 0755, true);
        }
        if (! is_dir($directory = $this->getViewPath('auth/passwords'))) {
            mkdir($directory, 0755, true);
        }
    }
    /**
     * Export the authentication views.
     *
     * @return void
     */
    protected function exportViews()
    {
        foreach ($this->views as $key => $value) {
            if (file_exists($view = $this->getViewPath($value)) && ! $this->option('force')) {
                if (! $this->confirm("The [{$value}] view already exists. Do you want to replace it?")) {
                    continue;
                }
            }
            copy(
                __DIR__.'/Auth/'.$this->argument('type').'-stubs/'.$key,
                $view
            );
        }
    }
    /**
     * Export the authentication backend.
     *
     * @return void
     */
    protected function exportBackend()
    {
        file_put_contents(
            app_path('Http/Controllers/HomeController.php'),
            $this->compileControllerStub()
        );
        file_put_contents(
            base_path('routes/web.php'),
            file_get_contents(__DIR__.'/Auth/stubs/routes.stub'),
            FILE_APPEND
        );
    }
    /**
     * Compiles the "HomeController" stub.
     *
     * @return string
     */
    protected function compileControllerStub()
    {
        return str_replace(
            '{{namespace}}',
            $this->laravel->getNamespace(),
            file_get_contents(__DIR__.'/Auth/stubs/controllers/HomeController.stub')
        );
    }
    /**
     * Get full view path relative to the application's configured view path.
     *
     * @param  string  $path
     * @return string
     */
    protected function getViewPath($path)
    {
        return implode(DIRECTORY_SEPARATOR, [
            config('view.paths')[0] ?? resource_path('views'), $path,
        ]);
    }
}

以上です。

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

(Laravel6.x)laravel/ui で 5.8 以前の make:auth のscaffolding を する

Laravel6.x から make:auth コマンドが消え、react と vue のscaffolding に変わった??

php artisan make:auth

  Command "make:auth" is not defined.

以前から laravel/ui に認証のscaffolding が移行されたから、という情報は認識していたのですが、最近 「laravel/ui の make:auth(ui:auth) では npm が必要で react が defaultになったらしい。」 というのを聞いて公式を調べたところ
image.png

vue が前提となっているように見えるので、今のプロジェクトではSPA用のJSは使っていないため bladeのscaffoldings がこのまま廃止されてしまったら、使いずらくなるなと思ったのですが、調べると、laravel/uiでも 5.8 以前の make:auth のscaffolding をすることができました

0. 準備

laravel new make-auth-demo

1. laravel/ui を取得

composer require laravel/ui --dev

2. ui:auth --views を実行

php artisan ui:auth --views

image.png

image.png

ui/src/AuthCommand.php にがっつり記載がありました。

https://github.com/laravel/ui/blob/master/src/AuthCommand.php

ui/src/AuthCommand.php
<?php
namespace Laravel\Ui;
use InvalidArgumentException;
use Illuminate\Console\Command;
class AuthCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'ui:auth
                    { type=bootstrap : The preset type (bootstrap) }
                    {--views : Only scaffold the authentication views}
                    {--force : Overwrite existing views by default}';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Scaffold basic login and registration views and routes';
    /**
     * The views that need to be exported.
     *
     * @var array
     */
    protected $views = [
        'auth/login.stub' => 'auth/login.blade.php',
        'auth/passwords/confirm.stub' => 'auth/passwords/confirm.blade.php',
        'auth/passwords/email.stub' => 'auth/passwords/email.blade.php',
        'auth/passwords/reset.stub' => 'auth/passwords/reset.blade.php',
        'auth/register.stub' => 'auth/register.blade.php',
        'auth/verify.stub' => 'auth/verify.blade.php',
        'home.stub' => 'home.blade.php',
        'layouts/app.stub' => 'layouts/app.blade.php',
    ];
    /**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {
        if (static::hasMacro($this->argument('type'))) {
            return call_user_func(static::$macros[$this->argument('type')], $this);
        }
        if (! in_array($this->argument('type'), ['bootstrap'])) {
            throw new InvalidArgumentException('Invalid preset.');
        }
        $this->ensureDirectoriesExist();
        $this->exportViews();
        if (! $this->option('views')) {
            $this->exportBackend();
        }
        $this->info('Authentication scaffolding generated successfully.');
    }
    /**
     * Create the directories for the files.
     *
     * @return void
     */
    protected function ensureDirectoriesExist()
    {
        if (! is_dir($directory = $this->getViewPath('layouts'))) {
            mkdir($directory, 0755, true);
        }
        if (! is_dir($directory = $this->getViewPath('auth/passwords'))) {
            mkdir($directory, 0755, true);
        }
    }
    /**
     * Export the authentication views.
     *
     * @return void
     */
    protected function exportViews()
    {
        foreach ($this->views as $key => $value) {
            if (file_exists($view = $this->getViewPath($value)) && ! $this->option('force')) {
                if (! $this->confirm("The [{$value}] view already exists. Do you want to replace it?")) {
                    continue;
                }
            }
            copy(
                __DIR__.'/Auth/'.$this->argument('type').'-stubs/'.$key,
                $view
            );
        }
    }
    /**
     * Export the authentication backend.
     *
     * @return void
     */
    protected function exportBackend()
    {
        file_put_contents(
            app_path('Http/Controllers/HomeController.php'),
            $this->compileControllerStub()
        );
        file_put_contents(
            base_path('routes/web.php'),
            file_get_contents(__DIR__.'/Auth/stubs/routes.stub'),
            FILE_APPEND
        );
    }
    /**
     * Compiles the "HomeController" stub.
     *
     * @return string
     */
    protected function compileControllerStub()
    {
        return str_replace(
            '{{namespace}}',
            $this->laravel->getNamespace(),
            file_get_contents(__DIR__.'/Auth/stubs/controllers/HomeController.stub')
        );
    }
    /**
     * Get full view path relative to the application's configured view path.
     *
     * @param  string  $path
     * @return string
     */
    protected function getViewPath($path)
    {
        return implode(DIRECTORY_SEPARATOR, [
            config('view.paths')[0] ?? resource_path('views'), $path,
        ]);
    }
}

以上です。

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

(Laravel6.x)laravel/ui で 5.8 以前の make:auth のscaffolding をする

Laravel6.x から make:auth コマンドが消え、react と vue のscaffolding に変わった??

php artisan make:auth

  Command "make:auth" is not defined.

以前から laravel/ui に認証のscaffolding が移行されたから、という情報は認識していたのですが、最近 「laravel/ui の make:auth(ui:auth) では npm が必要で vue、react が defaultになったらしい。」 というのを聞いて公式を調べたところ
image.png

vue が前提となっているように見えるので、今のプロジェクトではSPA用のJSは使っていないため bladeのscaffoldings がこのまま廃止されてしまったら、使いずらくなるなと思ったのですが、調べると、laravel/uiでも 5.8 以前の make:auth のscaffolding をすることができました

0. 準備

laravel new make-auth-demo

1. laravel/ui を取得

composer require laravel/ui --dev

2. ui:auth --views を実行

php artisan ui:auth --views

image.png

image.png

image.png

ui/src/AuthCommand.php にがっつり記載がありました。

https://github.com/laravel/ui/blob/master/src/AuthCommand.php

ui/src/AuthCommand.php
<?php
namespace Laravel\Ui;
use InvalidArgumentException;
use Illuminate\Console\Command;
class AuthCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'ui:auth
                    { type=bootstrap : The preset type (bootstrap) }
                    {--views : Only scaffold the authentication views}
                    {--force : Overwrite existing views by default}';
    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Scaffold basic login and registration views and routes';
    /**
     * The views that need to be exported.
     *
     * @var array
     */
    protected $views = [
        'auth/login.stub' => 'auth/login.blade.php',
        'auth/passwords/confirm.stub' => 'auth/passwords/confirm.blade.php',
        'auth/passwords/email.stub' => 'auth/passwords/email.blade.php',
        'auth/passwords/reset.stub' => 'auth/passwords/reset.blade.php',
        'auth/register.stub' => 'auth/register.blade.php',
        'auth/verify.stub' => 'auth/verify.blade.php',
        'home.stub' => 'home.blade.php',
        'layouts/app.stub' => 'layouts/app.blade.php',
    ];
    /**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {
        if (static::hasMacro($this->argument('type'))) {
            return call_user_func(static::$macros[$this->argument('type')], $this);
        }
        if (! in_array($this->argument('type'), ['bootstrap'])) {
            throw new InvalidArgumentException('Invalid preset.');
        }
        $this->ensureDirectoriesExist();
        $this->exportViews();
        if (! $this->option('views')) {
            $this->exportBackend();
        }
        $this->info('Authentication scaffolding generated successfully.');
    }
    /**
     * Create the directories for the files.
     *
     * @return void
     */
    protected function ensureDirectoriesExist()
    {
        if (! is_dir($directory = $this->getViewPath('layouts'))) {
            mkdir($directory, 0755, true);
        }
        if (! is_dir($directory = $this->getViewPath('auth/passwords'))) {
            mkdir($directory, 0755, true);
        }
    }
    /**
     * Export the authentication views.
     *
     * @return void
     */
    protected function exportViews()
    {
        foreach ($this->views as $key => $value) {
            if (file_exists($view = $this->getViewPath($value)) && ! $this->option('force')) {
                if (! $this->confirm("The [{$value}] view already exists. Do you want to replace it?")) {
                    continue;
                }
            }
            copy(
                __DIR__.'/Auth/'.$this->argument('type').'-stubs/'.$key,
                $view
            );
        }
    }
    /**
     * Export the authentication backend.
     *
     * @return void
     */
    protected function exportBackend()
    {
        file_put_contents(
            app_path('Http/Controllers/HomeController.php'),
            $this->compileControllerStub()
        );
        file_put_contents(
            base_path('routes/web.php'),
            file_get_contents(__DIR__.'/Auth/stubs/routes.stub'),
            FILE_APPEND
        );
    }
    /**
     * Compiles the "HomeController" stub.
     *
     * @return string
     */
    protected function compileControllerStub()
    {
        return str_replace(
            '{{namespace}}',
            $this->laravel->getNamespace(),
            file_get_contents(__DIR__.'/Auth/stubs/controllers/HomeController.stub')
        );
    }
    /**
     * Get full view path relative to the application's configured view path.
     *
     * @param  string  $path
     * @return string
     */
    protected function getViewPath($path)
    {
        return implode(DIRECTORY_SEPARATOR, [
            config('view.paths')[0] ?? resource_path('views'), $path,
        ]);
    }
}

参考

下記の記事に詳しく書いて頂いておりましたので参考にさせて頂きました。
https://medium.com/@panjeh/laravel-changes-in-php-artisan-ui-auth-php-artisan-make-auth-82fdb8893726

以上です。

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

【Laravel】Todoリストアプリの作成

laravelでtodoアプリを作成

laravelでのCRUD処理理解のために、todoアプリを作成しました。その作成方法のメモです。

.envファイルの修正

.env
DB_CONNECTION=sqlite

データベースファイルの作成

$ cd database
$ touch database.sqlite

テーブル設計

  • モデル名は「Todo」とします
  • テーブル名は「todos」になります。
  • コントローラー名は「TodosController」とします。

Todosテーブルのカラム構成

  • id
  • body:todoの内容
  • created_at
  • updated_at

※idとcreated_atとupdate_atは自動で作成されます。

モデルとマイグレーションファイルの作成

$ cd .. (プロジェクトフォルダ直下に戻る)
$ php artisan make:model Todo --migration
Model created successfully.
Created Migration: 2019_12_14_015752_create_todos_table

マイグレーションファイルの修正

database/migrations/2019_12_14_015752_create_todos_table
    public function up()
    {
        Schema::create('todos', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->text('body'); //追加
            $table->timestamps();
        });
    }

マイグレーションの実行

$ php artisan migrate
Migration table created successfully.
Migrating: 2019_12_14_015752_create_todos_table
Migrated:  2019_12_14_015752_create_todos_table (0.01 seconds)

テーブルが作成されているか確認する

todosと表示されていれば、テーブルが作成されている。

$ sqlite3 database/database.sqlite
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> 

sqlite> .tables //テーブル一覧を取得
migrations  todos  

sqlite> PRAGMA table_info('todos'); //テーブル構造(カラム)を確認
0|id|integer|1||1
1|body|text|1||0
2|created_at|datetime|0||0
3|updated_at|datetime|0||0

sqlite> .q //sqliteを閉じる
$

コントローラーの作成

$ php artisan make:controller TodosController

ルーティングの設定

routes/web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

//以下をコメントアウトする
//Route::get('/', function () {
//    return view('welcome');
//});

Route::get('/','TodosController@index'); 
Route::resource('todos','TodosController');

resourcesでURIやコントローラーのメソッドは作成されるが、初期画面をindexにしたいので、Route::get・・・も追加する。

以下を参考にして表を作成
https://laraweb.net/knowledge/725/

HTTPメソッド URI コントローラのメソッド 用途
GET /todos index() 一覧表示
GET /todoss/create create() 追加ページ
POST /todos store() 追加
GET /todos/{id} show() 該当データ表示
GET /todos/{id}/edit edit() 更新ページ
PUT /todos/{id} update() 更新
DELETE /todos/{id} destroy() 削除

Todosコントローラーの修正

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

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TodosController extends Controller
{
    //追加
        public function index() {
      return view('todos.index');
    }
    //追加
}

todosテーブルに初期データを流し込む為のseederファイルを生成

$ php artisan make:seeder TodoTableSeeder
Seeder created successfully.

作成したTodoTableSeederに流し込むデータ記載する

database/seeds/TodoTableSeeder.php
<?php

use Illuminate\Database\Seeder;

class TodoTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        //
        $bodys = ['JCBギフト券を換金する','ゼルダの伝説をする','conohaのVPSで立ち上げをする'];
        foreach ($bodys as $body) {
          DB::table('todos')->insert([
            'body' => $body,
            'created_at' => new Datetime(),
            'updated_at' => new Datetime()
          ]);
        }
    }
}

seederを実行(テーブルへの初期データ書き込み)するためにDatabaseSeederに記載する

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);
        $this->call(TodoTableSeeder::class);
    }
}

seederを実行してtodosテーブルに書き込みする。

$ php artisan db:seed
Seeding: TodoTableSeeder
Database seeding completed successfully.

todosテーブルに書き込まれているか確認する

$ sqlite3 database/database.sqlite
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> select * from todos;
1|JCBギフト券を換金する||
2|ゼルダの伝説をする||
3|conohaのVPSで立ち上げをする||

Viewの部分を作成していきます。

index.balde.phpを作成する

$ cd resources/views
$ mkdir todos (todos関連のディレクトリを作成)
$ vi index.blade.php (viewファイルの作成)

bootstrap4を使います。

以下のリンクからコピーしましょう。
https://cccabinet.jpn.org/bootstrap4/getting-started/introduction
スクリーンショット 2019-12-15 9.01.27.png

resources/views/index.blade.php
<!doctype html>
<html lang="ja">
  <head>
    <title>Bootstrap基本テンプレート</title>
  <!-- 必要なメタタグ -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>
    <h1>こんにちは!</h1>

  <!-- オプションのJavaScript -->
  <!-- 最初にjQuery次にPopper.js次にBootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
  </body>
</html>

Todoリストの見た目の部分を作成します。

変更内容は以下

  • もろもろ
  • todo作成フォーム
  • todo追加ボタン
  • todo編集ボタン
  • todo削除ボタン
rescources/views/todos/index.blade.php
<!doctype html>
<html lang="ja">
  <head>
    <title>Jum Todoリスト</title>
  <!-- 必要なメタタグ -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>
  <body>
    <div class="container" style="margin-top:50px;">
    <h1>Todoリスト追加</h1>

    <form action='{{ url('/todos')}}' method="post">
      {{csrf_field()}}
  <div class="form-group">
    <label >やることを追加してください</label>
    <input type="text" name="body"class="form-control" placeholder="todo list" style="max-width:1000px;">
  </div>
  <button type="submit" class="btn btn-primary">追加する</button>  </form>

    <h1 style="margin-top:50px;">Todoリスト</h1>
    <table class="table table-striped" style="max-width:1000px; margin-top:20px;">
    <!-- <thead>
    <tr>
      <th></th><th></th><th></th>
    </tr>
  </thead> -->
  <tbody>
    @foreach ($todos as $todo)
    <tr>
      <td>{{$todo->body}}</td>
      <td><form action="{{ action('TodosController@edit', $todo) }}" method="post">
          {{ csrf_field() }}
          {{ method_field('get') }}
          <button type="submit" class="btn btn-primary">編集</button>
      </form>
      </td>

      <!-- 削除ボタン -->
      <td><form action="{{url('/todos', $todo->id)}}" method="post">
          {{ csrf_field() }}
          {{ method_field('delete') }}
          <button type="submit" class="btn btn-danger">削除</button>
      </form>
      </td>

      <!-- 削除した際にポップ画面で確認をする -->
      <!-- <td><a class="del" data-id="{{ $todo->id }}" href="#">削除</a>
        <form method="post" action='{{ url('/todos', $todo->id) }}' id="form_{{ $todo->id}}">
          {{ csrf_field() }}
          {{ method_field('delete') }}
        </form>
      </td> -->
    </tr>
    @endforeach
  </table>
</div>
  <!-- オプションのJavaScript -->
  <!-- 最初にjQuery次にPopper.js次にBootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    <script>
    (function() {
  'use strict';

  var cmds = document.getElementsByClassName('del');
  var i;

  for (i = 0; i < cmds.length; i++) {
    cmds[i].addEventListener('click', function(e) {
      e.preventDefault();
      if (confirm('are you sure?')) {
        document.getElementById('form_' + this.dataset.id).submit();
      }
    });
  }

})();
</script>
  </body>
</html>

コントローラーの修正

todo追加処理

app/Http/controllers/TodosController.php
    public function store(Request $request) {
      $todo = new Todo();
      $todo->body = $request->body;
      $todo->save();
      return redirect('/');
    }

削除処理

app/Http/controllers/TodosController.php
    public function destroy(todo $todo) {
        $todo->delete();
       return redirect('/');
     }

編集処理

編集する為のviewを作成

$ cd resources/views/todos
$ touch edit.blade.php

edit.blade.phpを編集

resources/views/todos/edit.blade.php
<!doctype html>
<html lang="ja">
  <head>
    <title>Jum Todoリスト</title>
  <!-- 必要なメタタグ -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
  </head>

  <body>
    <div class="container" style="margin-top:50px;">
    <h1>Todoリスト更新</h1>

    <form action='{{ url('/todos',$todo->id) }}' method="post">
      {{csrf_field()}}
      {{ method_field('patch')}}
  <div class="form-group">
    <label >やることを更新してください</label>
    <input type="text" name="body"class="form-control" value="{{ $todo->body }}" style="max-width:1000px;">
  </div>
  <button type="submit" class="btn btn-primary">更新する</button>
</form>

</div>
  <!-- オプションのJavaScript -->
  <!-- 最初にjQuery次にPopper.js次にBootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    <script>
    (function() {
  'use strict';

  var cmds = document.getElementsByClassName('del');
  var i;

  for (i = 0; i < cmds.length; i++) {
    cmds[i].addEventListener('click', function(e) {
      e.preventDefault();
      if (confirm('are you sure?')) {
        document.getElementById('form_' + this.dataset.id).submit();
      }
    });
  }

})();
</script>
  </body>
</html>

編集画面への遷移処理(コントローラー)

app/Http/controllers/TodosController.php
    public function edit(todo $todo) {
      return view('todos.edit')->with('todo',$todo);
    }

更新処理(コントローラー)

app/Http/controllers/TodosController.php
    public function update(Request $request,todo $todo) {
      $todo->body = $request->body;
      $todo->save();
      return redirect('/');
    }

とりあえず完了

ブラウザで確認

$ php artisan serve --host 192.168.33.10 --port 8000
Laravel development server started: <http://192.168.33.10:8000>

初期画面(index)

スクリーンショット 2019-12-15 10.52.39.png

編集画面(edit)

スクリーンショット 2019-12-15 10.54.28.png

その他の機能

バリデーション

フラッシュメッセージ

終わりに

以上です。
これからlaravelを始める方は参考にしてみてください。

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

Formファサードでバリデーションエラー時にCSSクラスをあてる

バリデーションエラー時に、Formファザードで作ったタグにエラー用のCSSクラスをあてる方法を調べました。


ルーティングと簡単なコントローラーを用意します。

routes/web.php
Route::get('/form', 'FormController@index');
Route::post('/form', 'FormController@post');
app/Http/Controllers/FormController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FormController extends Controller
{
    public function index()
    {
        $list = [
            ''  => '選択してください',
            '1' => 'ほげ',
            '2' => 'ふが',
        ];

        return view('form', compact('list'));
    }

    public function post(Request $request)
    {
        $request->flash();

        $request->validate([
            'text' => 'required',
            'id'   => 'required',
        ],[
            'text.required' => 'テキストを入力してください。',
            'id.required'   => 'リストを選択してください。',
        ]);

        return redirect('form')->with('flash_message', '送信完了しました。');
    }
}

テンプレートを用意します。
デフォルトのレイアウトとCSSを流用しています。

resources/views/Form.blade.php
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            @if (session('flash_message'))
            <div class="alert alert-success" role="alert">
                {{ session('flash_message') }}
            </div>
            @endif
            <div class="card">
                <div class="card-header">フォーム</div>
                <div class="card-body">
                {{Form::open(['url' => '/form'])}}
                    {{Form::token()}}
                    <div class="form-group">
                        {{Form::text('text',
                                      old('text'),
                                      $errors->has('text') ? ['class' => 'form-control is-invalid', 'placeholder' => '入力してください']
                                                           : ['class' => 'form-control', 'placeholder' => '入力してください'])}}
                        <span class="invalid-feedback" role="alert">@error('text'){{ $message }}@enderror</span>
                    </div>
                    <div class="form-group">
                        {{Form::select('id',
                                        $list,
                                        old('id'),
                                        $errors->has('id') ? ['class' => 'form-control is-invalid']
                                                           : ['class' => 'form-control']
                        )}}
                        <span class="invalid-feedback" role="alert">@error('id'){{ $message }}@enderror</span>
                    </div>
                    {{Form::submit('送信', ['class'=>'btn btn-primary'])}}
                {{Form::close()}}
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

三項演算子で、$errors->has('id') ? ['class' => 'form-control is-invalid'] : ['class' => 'form-control']のようにエラーの有無でクラスの配列引数を変えています。

バリデーションエラーがあるとこんな感じでエラーの装飾があたります。

image.png

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

LaravelのViewComposerで継承を使わずに共通化する

この記事はLaravel Advent Calendar 2019の15日目の記事です。

ViewComposerとは?

ビューコンポーザはビューがレンダーされる時に呼び出される、コールバックかクラスメソッドのことです。ビューがレンダーされるたびに結合したい情報があるなら、ビューコンポーザがロジックを一箇所にまとめるのに役立ちます。
ビュー 6.x Laravel

基本的な実装方法も上記Laravelのドキュメントで確認することができます。

ViewComposerの実装を共通化する

本記事ではサンタクロース(santa-claus)画面とトナカイ(reindeer)画面にそれぞれViewComposerを定義し、
それぞれ共通して isChristmas をViewから参照させます。
これを継承を使った方法を紹介した上で、継承を使わずに共通化する方法を紹介します。

継承を使った実装

isChristmas を設定する ChristmasViewComposer を実装し、 SantaClausViewComposerReindeerViewComposer がそのクラスを継承します。

namespace App\Http\View\Composers;

use Illuminate\View\View;

class ChristmasViewComposer
{
    public function compose(View $view)
    {
        $view->with('isChristmas', $this->isChristmas());
    }

    private function isChristmas()
    {
        return false; // FIXME 適当な実装
    }
}

class SantaClausViewComposer extends ChristmasViewComposer
{
    public function compose(View $view)
    {
        parent::compose($view);
        // FIXME サンタクロース(santa-claus)画面に必要な実装
    }
}

class ReindeerViewComposer extends ChristmasViewComposer
{
    public function compose(View $view)
    {
        parent::compose($view);
        // FIXME トナカイ(reindeer)画面に必要な実装
    }
}

コンポーザーの定義はこのようになります。

View::composer('santa-claus', 'App\Http\View\Composers\SantaClausViewComposer');
View::composer('reindeer', 'App\Http\View\Composers\ReindeerViewComposer');

継承を使わない実装

isChristmas を設定する ChristmasViewComposer を実装しますが、 SantaClausViewComposerReindeerViewComposer はそれを継承しません。

namespace App\Http\View\Composers;

use Illuminate\View\View;

class ChristmasViewComposer
{
    public function compose(View $view)
    {
        $view->with('isChristmas', $this->isChristmas());
    }

    private function isChristmas()
    {
        return false; // FIXME 適当な実装
    }
}

class SantaClausViewComposer
{
    public function compose(View $view)
    {
        // FIXME サンタクロース(santa-claus)画面に必要な実装
    }
}

class ReindeerViewComposer
{
    public function compose(View $view)
    {
        // FIXME トナカイ(reindeer)画面に必要な実装
    }
}

継承していないためコンポーザーをこのように定義します。

View::composer(
    ['santa-claus', 'reindeer'],
    'App\Http\View\Composers\ChristmasViewComposer'
);
View::composer('santa-claus', 'App\Http\View\Composers\SantaClausViewComposer');
View::composer('reindeer', 'App\Http\View\Composers\ReindeerViewComposer');

まとめ

ドキュメントには書かれていませんが、一つのViewに対して複数の複数のViewComposerを登録することも可能なため、継承を使わなくてもViewComposerの実装を共通化できます。

クラス継承にはデメリットが多いため、クラス継承を使わない共通化の方法を紹介しました。

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