20200124のlaravelに関する記事は8件です。

外為で勝つために その5 ~ Laravel 編

前回「外為で勝つために その4 ~ LINE 通知編」の続きです。

ご無沙汰してます。
ポータルサイトを作るための材料を用意したり、NISA で株の買い付けしたりしてました。
今年もどうぞよろしくお願いします。

ポータルサイトを作るにあたり、
チャートなどを表示するために、データベースに接続する必要があります。

データベース接続といえば PHP ですが
ここはオシャレに Laravel フレームワークを使いたいと思います。
PHP だけでも構築できますが、勉強も兼ねてということで。

PHP バージョン確認

早速問題になるのが PHP のバージョンです。
今回導入する Laravel は 6.7.0 で、土台として PHP 7.2 以上が必要なのだそう。

Raspberry Pi の標準的なインストール方法では PHP 7.0.33 までしか入りません。

$ php -v
PHP 7.0.33-0+deb9u6 (cli) (built: Oct 24 2019 18:50:20) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.0.33-0+deb9u6, Copyright (c) 1999-2017, by Zend Technologies

PHP 7.3 をインストール

ということで、裏技的に PHP 7.3 をインストールします。(執筆時点で安定した最新版)
まずは管理者ログインして、apt に必要なキー情報を登録します。

$ sudo -i
# wget -O - https://packages.sury.org/php/apt.gpg | apt-key add -
stdout へ出力完了 [1769/1769]

パッケージリストを更新して PHP 7.3 を探せるようにします。

# echo "deb https://packages.sury.org/php/ stretch main" >> /etc/apt/sources.list.d/php.list
# apt-get update                                     
パッケージリストを読み込んでいます... 完了

インストールできる PHP のバージョンを確認します。
7.3 の文字が見えますね。

# apt-cache policy php
php:
  インストールされているバージョン: 1:7.0+49
  候補:               2:7.3+70+0~20191118.18+debian9~1.gbp66b4ed
  バージョンテーブル:
     2:7.3+70+0~20191118.18+debian9~1.gbp66b4ed 500
        500 https://packages.sury.org/php stretch/main armhf Packages
 *** 1:7.0+49 500
        500 http://raspbian.raspberrypi.org/raspbian stretch/main armhf Packages
        100 /var/lib/dpkg/status

PHP 7.3 をインストールします。バージョンは必ず指定してあげましょう。
インストールはあっけなく終わるので、管理者からログアウトしてバージョンを確認します。

# apt-get install php7.3
# exit
ログアウト
$ php -v
PHP 7.3.12-1+0~20191128.49+debian9~1.gbp24559b (cli) (built: Nov 28 2019 07:38:18) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.12, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.12-1+0~20191128.49+debian9~1.gbp24559b, Copyright (c) 1999-2018, by Zend Technologies

PHP 7.3.12 が無事インストールされました。

Laravel インストールの下準備

Laravel にはいくつか必要なモジュールがあるそうで、
下記のコマンドを実行して表示されないモジュールをインストールするようなことが、どこかに書いてありました。(どこだったかはもう完全に忘れました)

$ php -m | grep -e openssl -e PDO -e mbstring -e tokenizer -e ^xml$ -e ctype -e json -e bcmath
ctype
json
openssl
PDO
tokenizer

mbstringxmlbcmath がないですね。
モジュール名それぞれの先頭に php- を付けてインストールしておきます。

インストールが終わったら、同じ確認コマンドを実行して再確認します。問題なさそうですね。

$ sudo apt-get install php-bcmath php-mbstring php-xml
$ php -m | grep -e openssl -e PDO -e mbstring -e tokenizer -e ^xml$ -e ctype -e json -e bcmath
bcmath
ctype
json
mbstring
openssl
PDO
tokenizer
xml

Composer のインストール

Laravel は PHP のフレームワークでありパッケージの1つです。
PHP 関係のパッケージは Composer で依存性を一元管理するらしいです(多分)。いわゆるパッケージマネージャーですね。これを先にインストールしておきます。

それにしても、やたら味のある Web サイトのトップだったのでスクショしておきました。
良いセンスを感じますねぇ。

Screenshot_20200124[1].png

それはさておき、
インストーラを HTTP GET して、そのままインストールします。

$ curl -sS https://getcomposer.org/installer | php
All settings correct for using Composer
Downloading...

Composer (version 1.9.1) successfully installed to: /home/pi/composer.phar
Use it: php composer.phar

$ sudo mv composer.phar /usr/local/bin/composer
$ composer -v
   ______
  / ____/___  ____ ___  ____  ____  ________  _____
 / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
/ /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
                    /_/
Composer version 1.9.1 2019-11-01 17:20:17

インストールはあっさり完了しました。

Laravel のインストール

いよいよ本命の Laravel をインストールします。
Composer 経由でインストールコマンドを実行します。

$ cd /home/pi/.config/composer
$ composer global require laravel/installer

  Problem 1
    - laravel/installer v3.0.1 requires ext-zip * -> the requested PHP extension zip is missing from your system.
    - laravel/installer v3.0.0 requires ext-zip * -> the requested PHP extension zip is missing from your system.
    - Installation request for laravel/installer ^3.0 -> satisfiable by laravel/installer[v3.0.0, v3.0.1].

Installation failed, deleting ./composer.json.

なんか失敗しましたね。
zip が足らんのでインストールが続行できないとのこと。

ならば php-zip をインストールしてリトライ。

$ sudo apt-get install php-zip
$ composer global require laravel/installer
Changed current directory to /home/pi/.config/composer
Using version ^3.0 for laravel/installer
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 14 installs, 0 updates, 0 removals
  - Installing symfony/process (v5.0.1): Downloading (100%)
  - Installing symfony/polyfill-ctype (v1.13.1): Downloading (100%)
  - Installing symfony/filesystem (v5.0.1): Downloading (100%)
  - Installing psr/container (1.0.0): Downloading (100%)
  - Installing symfony/service-contracts (v2.0.1): Downloading (100%)
  - Installing symfony/polyfill-php73 (v1.13.1): Downloading (100%)
  - Installing symfony/polyfill-mbstring (v1.13.1): Downloading (100%)
  - Installing symfony/console (v5.0.1): Downloading (100%)
  - Installing ralouphie/getallheaders (3.0.3): Downloading (100%)
  - Installing psr/http-message (1.0.1): Downloading (100%)
  - Installing guzzlehttp/psr7 (1.6.1): Downloading (100%)
  - Installing guzzlehttp/promises (v1.3.1): Downloading (100%)
  - Installing guzzlehttp/guzzle (6.5.0): Downloading (100%)
  - Installing laravel/installer (v3.0.1): Downloading (100%)
symfony/service-contracts suggests installing symfony/service-implementation
symfony/console suggests installing symfony/event-dispatcher
symfony/console suggests installing symfony/lock
symfony/console suggests installing psr/log (For using the console logger)
guzzlehttp/psr7 suggests installing zendframework/zend-httphandlerrunner (Emit PSR-7 responses)
guzzlehttp/guzzle suggests installing psr/log (Required for using the Log middleware)
guzzlehttp/guzzle suggests installing ext-intl (Required for Internationalized Domain Name (IDN) support)
Writing lock file
Generating autoload files

インストールできた感じは微塵もありませんが、無事完了したようです。

Laravel Web Artisan の作成

もう少しです。
インストールは完了したので、Laravel 実行環境である(プロジェクトとも呼ばれる)Web Artisan(ウェブ職人)を作成します。

事前に composer/vendor/bin にパスを通しておきます。
作成先フォルダは /var/www/html/MarketMonitor にしました。
laravel new <Project Name> で作成します。今回はひねりなく laravel にしました。

$ export PATH="~/.config/composer/vendor/bin:$PATH"
$ cd /var/www/html/MarketMonitor

$ laravel new laravel
Package manifest generated successfully.
Application ready! Build something amazing.

「アプリケーションの準備ができました!素晴らしいものを作りましょう」
と言われたらインストール完了です。もちろん作りますよ。素晴らしいものを。

Laravel のテスト

「すばらっ」なアプリケーションを作成する前に、実際動くかどうかをテストしましょう。
書き込みが発生する(であろう)ディレクトリ配下のファイルに、パーミッションを与えておきます。

php artisan serve を実行するとサーバーが起動します。

$ chmod -R 777 /var/www/html/MarketMonitor/laravel/storage
$ chmod -R 777 /var/www/html/MarketMonitor/laravel/bootstrap/cache
$ cd /var/www/html/MarketMonitor/laravel
$ php artisan --version
Laravel Framework 6.7.0
$ php artisan serve
Laravel development server started: http://127.0.0.1:8000
[Wed Dec 11 14:06:27 2019] 127.0.0.1:47120 [200]: /favicon.ico

ttp://127.0.0.1:8000 にアクセスしろ、とのことです。

Screenshot_20200124[2].png

成功ですね。
これでデータベースにアクセスし放題です!

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

LaravelからSalesforceへのデータ連携入門

これはなに

LaravelからSalesforceにデータを同期する方法について紹介します。

別システムで管理してるデータをSalesforceに同期させることで、
営業やカスタマーサクセスなど他部署でのデータ活用が可能になり、業務効率化が期待できます。

そういうわけでやってくれと依頼されたのでやってみました。
Salesforce何もわからない状況から始まってようやくデータ同期の道筋ができた状況です。

Salesforceにデータを同期する方法

別システムからSalesforceにデータを同期する方法にはいくつかありますが、
今回はおそらく一番一般的であろうREST APIを使います。

Lightning Platform REST API

ドキュメントはこちら https://developer.salesforce.com/docs/atlas.ja-jp.api_rest.meta/api_rest/intro_what_is_rest_api.htm

Lightning Platformってなに? って感じですが、
最近のSalesforceプラットフォームのことで、昔のはSalesforce Classicというようです。
あまり気にせず行きましょう。

APIいろいろあるんですが、正直あまり見てません。
次に紹介するラッパーが便利だったからです。

LaravelからSalesfoceにデータ同期するのに便利なパッケージ

LaravelからSalesfoce REST APIを便利に使うパッケージを探したところ
omniphx/forrestというものを見つかりました。

Salesforce REST API Client for Laravel
https://github.com/omniphx/forrest
Forrest.png

Forrestの使い方

Installation

Laravel6前提です

composer.json

"omniphx/forrest": "2.*"

を追加して

composer update

インストールが完了するとconfig/app.phpに以下のコードが追加されるようです。
私は追加されてなかったので自分で追加しました。
それぞれprovidersaliasesに追加します。

config/app.php
Omniphx\Forrest\Providers\Laravel\ForrestServiceProvider::class
'Forrest' => Omniphx\Forrest\Providers\Laravel\Facades\Forrest::class

これでLaravelの他のFacade(LogやCache)と同じように、コードのどこでもForrestと書くとFacadeが実行できるようになりました。

設定ファイルの外出しと環境変数

このままではforrestの設定ファイルがvender配下にあって変更できないので、
以下のコマンドでconfig/配下に設定ファイルを移動させます。

php artisan vendor:publish

他にも設定ファイルの外出しができる場合はどれを外出しするか聞かれますが、configってやつを選べばよかったと記憶してます。

次に.envに環境変数を追加します。

.env
CONSUMER_KEY=123455
CONSUMER_SECRET=ABCDEF
CALLBACK_URI=https://test.app/callback
LOGIN_URL=https://login.salesforce.com
USERNAME=mattjmitchener@gmail.com
PASSWORD=password123

どんな値を設定するかはあとで説明します。

私の場合は、PASSWORDみたいな変数名だと他の変数と被りそうだったので
config/forrest.phpをいじってSALESFORCE_PASSWORDのように名前を変えました。

認証方法の設定

Salesforceに接続する方法はOAuthとパスワード認証の2種類ありますが、
今回はパスワード認証を使用します。

config/forrest.php
    /*
     * Options include WebServer, UserPassword, and UserPasswordSoap
     */
-    'authentication' => 'WebServer',
+    'authentication' => 'UserPassword',

アクセストークンの保存先をキャッシュにしておきます。 

config/forrest.php
    /*
     * Where do you want to store access tokens fetched from Salesforce
     */
    'storage'        => [
-        'type'          => 'session', // 'session' or 'cache' are the two options
+        'type'          => 'cache', // 'session' or 'cache' are the two options

Salesforceに接続アプリケーションを追加する

今回はパスワード認証を使う前提で進めます。
1. Salsesforceプラットフォームにログインする
2. 設定 > アプリケーションマネージャ(左のメニュー) からアプリケーションマネージャの画面を表示する
3. 新規接続アプリケーションをクリック
4. 必須項目を埋める

接続アプリケーションができると、CONSUMER_KEYなどが発行されるのでこれをコピーして.envに貼り付けます。

.env
CONSUMER_KEY=123455
CONSUMER_SECRET=ABCDEF
# CALLBACK_URI=https://test.app/callback パスワード認証の場合これはいらない
LOGIN_URL=https://test.salesforce.com # 本番環境の場合はhttps://login.salesforce.com
USERNAME=mattjmitchener@gmail.com # Salesforceのログインアカウントと同じ
PASSWORD=password123

使い方

認証

認証はこれだけです。

Forrest::authenticate();

データ取得

データの取得にはSQLライクなSOQLというのを使います。

Forrest::query('SELECT Id FROM Account');
{
    "totalSize": 2,
    "done": true,
    "records": [
        {
            "attributes": {
                "type": "Account",
                "url": "\/services\/data\/v30.0\/sobjects\/Account\/001i000000xxx"
            },
            "Id": "001i000000xxx"
        },
        {
            "attributes": {
                "type": "Account",
                "url": "\/services\/data\/v30.0\/sobjects\/Account\/001i000000xxx"
            },
            "Id": "001i000000xxx"
        }
    ]
}

1度に2000件しか取れないという制限がありますが、次のデータを取得する方法も簡単です。詳しくはREADMEを見てください。

ドキュメント: https://developer.salesforce.com/docs/atlas.ja-jp.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select.htm

データ同期(Upsert)

データの同期は以下のpatchメソッドを使います。

$externalId = 'XYZ1234';

Forrest::sobjects('Account/External_Id__c/' + $externalId, [
    'method' => 'patch',
    'body'   => [
        'Name'  => 'Dunder Mifflin',
        'Phone' => '555-555-5555'
    ]
]);

Upsert(Salesfoce上にすでにデータがあれば上書き、なければ新規作成) をこれだけで実現できます。

そのためにはSalesforce上のオブジェクトに、外部ID属性のカラムを追加する必要があります。

外部IDとpatchを使った同期の例

Salesforceのオブジェクトには、外部IDという属性のカラムを追加することができます。
外部システムからは外部キーを使って簡単にオブジェクトを特定し、書き込むことができます。
外部IDには、外部システム上のオブジェクトのプライマリーキーを使うと、
外部システム上のレコードとSalesforce上のレコードが一対一の関係になって都合がいいと思います。

前提

Salesforceに同期したいLarave上のオブジェクトをUserというEloquentモデルだとします。
Userモデルはidというプライマリーキーを持つものとします。

Salesforce側の準備

  1. 設定 > オブジェクトマネージャ > 新規 からUserオブジェクトを作ります
  2. 設定 > オブジェクトマネージャ >User > 項目とリレーション > 新規から数値型の user_idという項目を作ります。
  3. 作成時のオプションでユニーク外部IDというフラグをONにします。
  4. これでオブジェクトの型ができました。

Laravel側からUpsertする

$user_id = $user->id;

Forrest::sobjects('User__c/user_id__c/' . $user_id, [
    'method' => 'patch',
    'body'   => [
        'Name'  => 'This is new user!',
    ]
]);

User__cは、UserオブジェクトのAPI参照名です。
user_id__cは、user_idカラムの項目名です。
$user_idはあなたが書き込みたいUserオブジェクトのIDです。
Salesforce上に$user->idと一致するuser_idを持つUserオブジェクトが存在しなければ新規追加し、存在すれば上書きをしてくれます。

patchメソッドを使うことで、データの同期を簡単に行うことができました。

参照の同期について

Salesforceには、オブジェクトの参照関係があり、「どのオブジェクトを参照するか」という情報の同期が必要なケースがあります。
どのオブジェクトを参照するかの指定も外部IDを使えば簡単にできます。
詳しくは述べませんが、以下の資料が参考になるはずです。
https://developer.salesforce.com/docs/atlas.ja-jp.api_rest.meta/api_rest/dome_upsert.htm

まとめ

  • LaravelからSalesforceへのデータ同期はForrestが便利です
  • Salesforceの外部IDという仕組みを使えばデータ同期(Upsert)が簡単です

以上、参考になりましたら幸いです。

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

artisanコマンドの引数にハイフン始まりの値を渡したい

引数にハッシュ値を指定するartisanコマンドを作っていたのですが、
ハッシュ値がハイフン始まりのときにオプション扱いされてエラーになりました。。

現象

例)ハッシュ値が-LA12_ed

php artisan hoge:fuga -LA12_ed

エラーになる。The "-L" option does not exist.

対策

そんなときはエスケープすればいいよね。
エスケープ文字は -- でした。ハイフンハイフン半角スペース!

php artisan hoge:fuga -- -LA12_ed

これで無事引数を受け取れます。

参考

https://stackoverflow.com/questions/35493936/laravel-custom-command-argument-with-dash

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

Laravelでトランザクションをネストせずに新しいトランザクションを切る

毎度同じことをやろうと思った時にどうやればいいか忘れてしまうので備忘も兼ねて

はじめに

Laravelでは以下のコードのようにするとトランザクションをネストしてSAVEPOINTを作成するため、やんごとなき事情で新しいトランザクションを切りたいなーと思ってもうまくいきません。
(ID発番が連番ではない場合に別テーブルでID発番を管理しトランザクションを分けることでロックする時間を短くするとか)

    DB::beginTransaction();
    {
        DB::beginTransaction();
        Animal::create(['name' => 'ぺんぎんさん']);
        DB::commit();
    }
    DB::rollBack(); // ぺんぎんさんの挿入もロールバックされる!

SAVEPOINTってなんだ!って方は以下の記事が非常にわかりやすかったです。
●トランザクションのネストの使い方まとめた(初心者向け)
https://qiita.com/_natsu_no_yuki_/items/e1db2a132cbff740896d

実装方法

トランザクションを乱立させることはそう多くないと思うので、簡単にできる別コネクションを作成する方法を記載します。

準備

config/database.phpにデフォルトで利用している接続情報をまるっとコピーします。

database.php
        // 既存の接続情報
        'mysql' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            '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' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

        // ★mysqlの項目を丸コピした接続情報
        'mysql_2' => [
            'driver' => 'mysql',
            'url' => env('DATABASE_URL'),
            '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' => '',
            'prefix_indexes' => true,
            'strict' => true,
            'engine' => null,
            'options' => extension_loaded('pdo_mysql') ? array_filter([
                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
            ]) : [],
        ],

実装

新しいトランザクションを切りたい場合は、DBファサードのconnectionで追加した接続情報のキーを設定します。
Eloquentを利用する場合は、その中でModelのインスタンスを作成し、setConnectionで追加した接続情報のキーを設定します。

  • 手動トランザクションの場合
    // デフォルトのコネクションでトランザクションの開始
    DB::beginTransaction();
    {
        // 追加した接続情報をセットして、トランザクションの開始
        $mysql2 = \DB::connection('mysql_2');
        $mysql2->beginTransaction();

        // Modelのインスタンスを作成し、追加した接続情報をセットする
        $animal = new Animal();
        $animal->setConnection('mysql_2');
        $animal->create(['name' => 'ぺんぎんさん']);

        // mysql_2のコネクションをコミットする
        $mysql2->commit();
    }
    // デフォルトのコネクションをロールバックする
    DB::rollBack(); // ぺんぎんさんのデータもコミットされている!
  • トランザクションメソッドの場合
    DB::transaction(function () {
        DB::connection('mysql_2')->transaction(function () {
            $animal = new Animal();
            $animal->setConnection('mysql_2');
            $animal->create(['name' => 'ぺんぎんさん']);
        });
        throw new Exception(); // ぺんぎんさんのデータもコミットされている!
    });

おわりに

なんだかんだネストではなく、別トランザクションを切りたくなることはあるので、newTransactionとかできると嬉しいんだろうなというお気持ちでした。

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

お問い合わせ管理システムの設計 Laravel + Docker

はじめに

こちらの記事は課題を取り組むために書いています。どのように自分で課題のアプリを設計するかを大まかに示します。

Laravelは初めて使うことになるので、ここ二日間Dockerで環境を構築しLaravelを勉強していました。ですので、今からどのようにアプリを設計するかを書いていきたいと思います。

技術選定

課題では、Laravel / Docker / MySQL という組み合わせでシステムを構築するということでした。しかしながら、MySQLのコンテナーのステータスがrestartingの状態から変わらないというエラーが発生し直せませんでした。

ですので、Laravel / Docker / Postgres / Sass-bootstrapで始めたいと思います。

MySQLを諦める理由

エラーログを確認したところ、

mysql initializing databsase error -- initialize specified but the data directory has files in it. aborting

と書いてありました。ですので、そのディレクトリーに何らかの形で発生したファイルを削除したいのですがコンテナーもUPの状態ではないのでdocker-compose.ymlからコマンドを指定して空の状態にしようと試してみました。

    volumes:
        - "./mysql:/var/lib/mysql"
    command: --innodb-use-native-aio=0

Github上の参考フォーラム: https://github.com/docker-library/mysql/issues/69

ですが状況は変わらず、同じエラーログが出ておりMySQLの使用を断念することにしました。

半日くらい解決策を調べていましたが、見つかりませんでした。フォーラムでは同じエラーを経験し直せない方たちはmariaDBなどを代わりに使うということでした。自分も他のDBを使おうと思い、Postgresのコンテナーをセットアップしてみました。

Postgresの方はコンテナもUPの状態になり、使えそうなのでまず開発ではそちらを使うことにしました。

Sass-Bootstrap

Laravelと言えばVueをフロントで使いたいところですが、LaravelとVueを一辺に勉強ができるかまだ分からないのでBootstrapで簡潔にページを作っていくことにしました。

実はBootstrap-Material-Designでグーグルっぽくしたいと思ったのですが、Laravelでは一部のスタイリングに不具合が生じることが分かったので通常のBootstrapを使い、余裕があればMaterial Designっぽくアレンジしていけたらと思っています。

アプリの設計

お問い合わせ管理システムは、クライアント側のフォーム画面とAdmin側の管理画面の大きく二つのページに分けます。

すなわちデータベースは最低、クレーム用件と管理者の情報をまとめるものが必須となります。

その二つができれば、管理者システム内でのクレーム用件へのコメント機能を追加します。その際には、コメントの情報をまとめるテーブルが必要となります。

クレーム要件をまとめるdata schema:

create_complaints_table.php
    public function up()
    {
        Schema::create('complaints', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedInteger('admin_id');
            $table->string('firstname');
            $table->string('lastname');
            $table->string('email');
            $table->string('phone');
            $table->string('product');
            $table->string('title');
            $table->text('content');
            $table->string('status')->default('not addressed');
            $table->timestamps();
        });
    }

フォームから (姓 / 名 / メールアドレス / 電話番号 / 不良製品番号 / タイトル / 内容 ) を取得します。

(管理者ID / ステータス)については、管理者側の行動により左右されます。

クレーム用件が特定の管理者に渡れば、管理者IDが付与されます。

またステータスは、提出されてまだ管理者に渡っていない状態が'not addressed'

特定の管理者に渡ることによって'in progress'

そして管理者からの返信が済んだら'completed'に変わります。

管理者の情報をまとめるdata schema:

create_admins_table.php
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->integer('batches')->default('0');
            $table->string('status')->default('available');
            $table->timestamps();
        });
    }

管理者たちの (姓 / 名 / メールアドレス / パスワード ) を予め設定します。

Batchesには一日に対応したクレームの数を保存します。

Statusにはその管理者が働ける状態かを示します。(available / unavailable)

クレーム用件と管理者情報の関係

二つのテーブルの関係は、one to one relaitionshipとなります。一人の管理者が一つのクレームを管理します。お問い合わせ管理システムでは、誰かが対応中のクレームには、他の管理者が対応できないようにします。ですので対応中のクレームは一人の管理者によって所有されます。

Admin.php
class Admin extends Model
{
    protected $guarded = [];

    public function complaint()
    {
        return $this->hasOne(Complaint::class);
    }

}
Complaint.php
class Complaint extends Model
{
    protected $guarded = [];

    public function admin()
    {
        return $this->belongsTo(Admin::class);
    }
}

管理者のコメント機能

こちらは、管理者が対応中の状態にしたクレームに対してコメントができるという機能です。クレームに関わった管理者のコメントが表示されます。(初めに対応した管理者がコメントをしクレームを未対応に戻した場合、新しく対応している管理者がコメントもできる)

コメントをまとめるdata schema:

create_comments_table.php
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->text('body');
            $table->integer('admin_id')->references('id')->on('admins')->onDelete('cascade');
            $table->integer('complaint_id')->references('id')->on('complaints')->onDelete('cascade');
            $table->timestamps();
        });
    }

クレーム用件・管理者とコメントテーブルの関係

コメントテーブルとの関係はそれぞれ、one to many relationshipとなります。

Comment.php
class Comment extends Model
{
  protected $fillable = ['body', 'admin_id', 'complaint_id'];

  public function claim()
  {
    return $this->belongsTo('Complaint::class');
  }

  public function admin()
  {
    return $this->belongsTo('Admin::class');
  }
}
Complaint.php
class Complaint extends Model
{
    protected $guarded = [];

    public function admin()
    {
        return $this->belongsTo(Admin::class);
    }

    public function comments()
    {
      return $this->hasMany('App::Comment');
    }

}
Admin.php
class Admin extends Model
{
    protected $guarded = [];

    public function complaint()
    {
        return $this->hasOne(Complaint::class);
    }

    public function comments()
    {
      return $this->hasMany('Comment::class');
    }
}

ユーザビリティについて

管理システムやフォーム入力についてのユーザビリティを調査しました。

フォームや投稿でのバリデーションの結果が、サーバーから返答が来てからまた編集をさせられるというのがユーザーにとって一番イライラくるそうです。

それを解決するためには、フロント側でユーザーが入力最中に何がダメかを表示してあげる機能があると便利だと思いました。

管理システム側も同様で、仕事の効率化のためになるべく悪いリクエストはフロント側で拒否したほうが良いと思いました。

クレームの対応状態の更新をページがリアルタイムではできないので、websocketを使うのもありかと思いました。

おわり

月曜日からコードをしていこうと思います。この設計が上手くいかなければ、また記事を書きます。最後までありがとうございました。

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

EloquentCollectionを活用するメモ

概要

モデルをget()後に加工しなければならないのためのメモ。
eagerロードの制約を加えた値は動的プロパティを使わないと受け取れないので、コレクションを活用して加工など行う必要がある。
備忘録。

laravel 6.X

リレーション先の先の条件

fillter()を使う

ビューでクエリを行わずに、制約をかけた後のデータを渡すとスマートで安心する。

// ある記事に紐づく、公開中の評価がついた公開中のコメントが欲しい

// 制約をかける
$article->load([
    'comments' => function ($query) {
        $query->where('is_public', 1);
    },
    'comments.ratings' => function ($query) {
        $query->where('is_public', 1);
    }
]);

$commnts = $article->comments->filter(function ($comment, $key) {
    // 動的プロパティでアクセス
    return $comment->ratings->count() > 0;
});

reject()

除去する条件を入れる

$commnts = $article->comments->reject(function ($comment, $key) {
    return $comment->ratings->count() === 0;
});

コレクションを合成する

concat()で他のモデルから取得したget()と合成する

ちなみに中身はコレクションをforeachで回して、push()しているだけのようです。
なので

$articles = collect(\App\Models\Article::public()->take(3)->get());
$concat = $articles->concat(
    \App\Models\Service::query()->take(5)->get()
);

slice()を使ってページング

Illuminate\Pagination\LengthAwarePaginato はページング対象引数をコレクションで取ることが出来るので、合成後のコレクションで独自にページネーション出来る。

$offset = ($page * $limit) - $limit;
$slice = $concat->slice($offset, $limit);
$paginate = new LengthAwarePaginator($slice, count($concat), $limit, $page);

(仕様に従ってみましたが、これはsliceせずにどうにかならないんでしょうか....)

追記

@nunulk さんにコメント頂き追記
forPage()を使ってスマートになりました。

$paginate = new LengthAwarePaginator($concat->forPage($page, $limit), count($concat), $limit, $page);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelで非同期チャットアプリを作ろう(2)

はじめに

この記事では、片方がコメントを送信したら、もう片方の人はリロードしなくても、コメントが表示されるチャットアプリを作っていきたいと思います。

前回:Laravelで非同期チャットアプリを作ろう(1)

完成物

chat2.gif

ソースコード:https://github.com/Alesion30/ChatApp

非同期通信とは

非同期通信とは、ネットワークなどでつながれているコンピュータ間で、送信者のデータ送信タイミングと受信者のデータ受信タイミングを合わせずに通信を行う通信方式のこと。

参照: 非同期通信 - @IT リッチクライアント用語事典

日常生活の例でいうとこんな感じ。

同期通信

スクリーンショット 2020-01-22 17.47.07.png

非同期通信

スクリーンショット 2020-01-22 17.47.26.png

同期通信の場合は、リクエストを送信したらリロードが入り、他の処理を受け付けなくなるが、非同期通信の場合は、リクエストを送信しても、リロードが入らず別の処理を送信することもできる。

方針

  • APIを作る。(URLにアクセスしたら、jsonデータを返すやつ)
  • JavaScriptで、APIを叩きjsonデータを取得する。
  • ループ処理を用いてjsonデータを、bladeファイルに埋め込む。

APIを作る

HomeController.phpにjsonを返すgetData()という関数を定義する。

app/Http/Controllers/HomeController.php
public function getData()
{
    $comments = Comment::orderBy('created_at', 'desc')->get();
    $json = ["comments" => $comments];
    return response()->json($json);
}

web.phpに、getData()を登録する。

routes/web.php
Route::get('/result/ajax', 'HomeController@getData');

http://localhost/ChatApp/public/result/ajax にアクセスすると、jsonデータが返ってくる。

スクリーンショット 2020-01-22 18.03.40.png

ajaxを使って、jsonを取得する

ajaxを使えるようにするために、app.blade.phpのheadの部分でスクリプトを読み込む。

resources/views/layouts/app.blade.php
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>

これはajaxの基本形である。dataTypeには、jsonだけでなく、csvやhtmlも指定できる。

$.ajax({
    url: "result/ajax/",
    dataType: "json",
    success: data => {
      // 成功時の処理
    },
    error: () => {
      // エラー時の処理
    }
})

public/jsフォルダにcomment.jsを新規作成する。5秒ごとに、jsonデータを取得している。

public/js/comment.js
$(function() {
    get_data();
});

function get_data() {
    $.ajax({
        url: "result/ajax/",
        dataType: "json",
        success: data => {
            console.log(data);
        },
        error: () => {
            alert("ajax Error");
        }
    });

    setTimeout("get_data()", 5000);
}

app.blade.phpのbodyタグの一番下に、@yield('js')を入れる。

resources/views/layouts/app.blade.php
        <main class="py-4">
            @yield('content')
        </main>
    </div>

    @yield('js')
</body>

home.blade.phpの一番下で、comment.jsを読み込む。

resources/views/home.blade.php
@section('js')
<script src="{{ asset('js/comment.js') }}"></script>
@endsection

http://localhost/ChatApp/public/home にアクセスして、検証ツールを使うと、5秒ごとにjsonデータが送られてきていることがわかる。

スクリーンショット 2020-01-22 18.16.23.png

comment.jsのsuccessの処理を以下のように書き換えてみる。

public/js/comment.js
success: data => {
    console.log(data.comments);
    for (var i = 0; i < data.comments.length; i++) {
        console.log(data.comments[i].name);
        console.log(data.comments[i].comment);
    }
},

スクリーンショット 2020-01-22 18.29.17.png

ループ処理を用いてjsonデータを、bladeファイルに埋め込む。

home.blade.phpでコメントを表示している部分を書き換える。

resources/views/home.blade.php
<div class="chat-container row justify-content-center">
    <div class="chat-area">
        <div class="card">
            <div class="card-header">Comment</div>
            <div class="card-body chat-card">
                <div id="comment-data"></div>
            </div>
        </div>
    </div>
</div>

JavaScriptで、<div id="comment-data"></div>の中にコメントを埋め込んでいく形になる。

comment.jsのsuccessの処理を以下のように書き換える。

public/js/comment.js
success: data => {
    $("#comment-data")
        .find(".comment-visible")
        .remove();

    for (var i = 0; i < data.comments.length; i++) {
        var html = `
                    <div class="media comment-visible">
                        <div class="media-body comment-body">
                            <div class="row">
                                <span class="comment-body-user" id="name">${data.comments[i].name}</span>
                                <span class="comment-body-time" id="created_at">${data.comments[i].created_at}</span>
                            </div>
                            <span class="comment-body-content" id="comment">${data.comments[i].comment}</span>
                        </div>
                    </div>
                `;

        $("#comment-data").append(html);
    }
},

http://localhost/ChatApp/public/home にアクセスして、コメントを追加するとうまく反映している。

スクリーンショット 2020-01-22 18.32.38.png

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

Laravel5.8 サブクエリー(leftJoinSub)

説明

QueryBuilderがかなり使えるので、メモです。
サブクエリーもかんたんに書けます。

サブクエリーで合計をとります。

ユーザーへのメッセージがあって、それに対していいねをする想定のテーブル構成です。
以下、カラムは最小限で、適当です。
あと、正規化してない部分もあります。

テーブル

1.users :ユーザーテーブル

論理名 物理名
id PK
company_id 企業ID
name 名前

2.user_messages :ユーザーメッセージテーブル

論理名 物理名
id PK(メッセージのID)
user_id ユーザーテーブルのID(貰った人)
message メッセージ

3. user_message_likes :ユーザーメッセージへのいいねテーブル

論理名 物理名
id PK(いいねID)
company_id 企業ID
user_message_id ユーザーメッセージテーブル のID

メッセージと、メッセージ毎のいいね数を取得する

<?php
// ユーザーメッセージIDでグループ化し、メッセージ毎のいいね数を取得するサブクエリー
$like_count = DB::table('users_message_likes')
->select('user_message_id', DB::raw('count(user_message_id) AS like_cnt'))
->groupBy('user_message_id');

// =====
// メッセージと、いいね数を取得する処理
// =====
$messages = DB::table('user_messages')
// ユーザー情報取得用のjoin
->join('users', 'users.id', '=', 'users_messages.user_id')
// いいね数取得用のサブクエリー(所属企業を絞る)
->leftJoinSub($like_count, 'count_table', function ($join) use ($company_id) {
    $join->on('users_messages.id', '=', 'count_table.user_message_id')->where('count_table.company_id', '=', $company_id);
})
->orderBy('users_messages.id', 'DESC')
->where(['users.company_id', '=',  $company_id])
->select([
    'users.name AS name',
    'user_messages.id AS id',
    'user_messages.message AS message',
    'count_table.like_cnt  AS like_cnt',
])
->get();

// レコードあるか
if (count($messages) == 0) {
    // 抜けるなどの処理
}

// レコード存在時は、ループして取得する
foreach ($messages as $message) {
    // データ操作
}

追伸

結局、カウントは集計をテーブルに持ったので使いませんでしたが、
覚えておいたら使えると思います。

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