20200928のPHPに関する記事は27件です。

[Laravel] Composer require で依存関係によるエラーを回避する方法

概要

Composer でインストールを行う際、以下のようなエラーが出た時に対処した方法をまとめる。

Installation failed, reverting ./composer.json to its original content.

環境

Laravelの versionは 6.18.40

$ php artisan --version
Laravel Framework 6.18.40

解決方法

本エラーはインストールするライブラリが他パッケージで別versionで使用している依存関係によるエラーであるため、composer.jsonでインストールを行うversionを指定することで回避可能。
今回は、laravel/ui1.0として追記している。

composer.json
    "require": {
        "php": "^7.2",
        "fideloper/proxy": "^4.0",
        "laravel/framework": "^6.18.35",
        "laravel/tinker": "^2.0",
        "laravel/ui": "^1.0",
        "laravelcollective/html": "^6.0"
    },

その後、versionを指定してインストールを行う。

$ composer require laravel/ui:^1.0

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

[PHP]文字列を固定長の他の文字列で埋める(マルチバイト対応)

文字列を規定の桁数になるまで任意の文字で埋めるために、PHPのstr_pad()を使うことがあるかと思います。
ただし、以下のようなマルチバイト文字をstr_pad()にかけて指定した桁数の文字列を出力しようとしても、バイト数で計算されるため意図した通りに出力されません。

echo str_pad('東京', 8, '●', STR_PAD_RIGHT).PHP_EOL;
echo str_pad('名古屋', 8, '●', STR_PAD_LEFT).PHP_EOL;
echo str_pad('大阪', 8, '●', STR_PAD_BOTH);

出力結果は以下の通り…
8桁の文字列が出力されてほしいところですが、先に述べたように意図した通りになりません。
その上、文字化けになってしまいました。

東京�
名古屋
�大阪�

そこで、マルチバイト文字にも対応したmb_str_pad()を作成しました。
使い方はstr_pad()とほとんど変わりありませんが、バイト数ではなく文字数で計算するようになっています。

/**
 * 文字列を固定長の他の文字列で埋める(マルチバイト対応)
 *
 * @param string $input    対象の文字列
 * @param int    $pad_len  埋める文字数
 * @param string $pad_str  埋める文字列
 * @param int    $pad_type 埋め方(STR_PAD_RIGHT, STR_PAD_LEFT, STR_PAD_BOTH)
 * @param string $encoding 文字列のエンコーディング
 * @return string
 */
function mb_str_pad($input, $pad_len, $pad_str=' ', $pad_type=STR_PAD_RIGHT, $encoding=null)
{
    if (!$encoding || strtolower($encoding) == 'auto') {
        $encoding = mb_internal_encoding();
    }

    $input_len   = mb_strlen($input, $encoding);
    $pad_str_len = mb_strlen($pad_str, $encoding);
    $target_len  = $pad_len - $input_len;

    if (!$input_len || !$pad_str_len || $target_len <= 0) {
        return $input;
    }

    if ($pad_type === STR_PAD_BOTH) {
        $target_len /= 2;
    }

    $repeat = str_repeat($pad_str, ceil($target_len / $pad_str_len));

    if ($pad_type === STR_PAD_BOTH || $pad_type === STR_PAD_LEFT) {
        $left = mb_substr($repeat, 0, floor($target_len), $encoding);
    } else {
        $left = '';
    }

    if ($pad_type === STR_PAD_BOTH || $pad_type === STR_PAD_RIGHT) {
        $right = mb_substr($repeat, 0, ceil($target_len), $encoding);
    } else {
        $right = '';
    }

    return $left.$input.$right;
}

先ほどと同じ文字列を、mb_str_pad()にかけてみます。

echo mb_str_pad('東京', 8, '●', STR_PAD_RIGHT).PHP_EOL;
echo mb_str_pad('名古屋', 8, '●', STR_PAD_LEFT).PHP_EOL;
echo mb_str_pad('大阪', 8, '●', STR_PAD_BOTH);

出力結果は以下の通りです。

東京●●●●●●
●●●●●名古屋
●●●大阪●●●

今度は意図した通りの結果になりました!

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

Project ICKX Webアプリケーション開発・運用ガイドライン 参考版

概要

この文書はProject ICKXにおけるWebアプリケーションの開発・運用におけるガイドライン(以降、本ガイドラインという)の議論用参考版です。

注意事項

元が弊サークル内向けの制限無し版のため、最初から全てを網羅・把握することは困難な内容となっています。

ご自身の環境において「今出来ていない」「良く判らない」項目があった場合でも、ご自身の環境において「これら全てを実施しなければならない」という話ではありませんのでご留意ください。

仮にご自身の環境に取り込む場合は段階を分けて出来る所からやり特に意識しなくても出来るようになったら次の段階を実施することがコツとなります。

本ガイドラインの趣旨

本ガイドラインはWebアプリケーションの開発・運用に対する従来の理解に対して、弊サークルの解釈を加えることにより、議論の活発化を図ることを趣旨としています。

現時点においてセキュリティが「重視する・しないなど、プロダクトの価値の外に置かれている」「プログラマ個人の技能に立脚するもの」などの言説が多くみられます。

それらに対しての我々が持つ異なる価値観からの提案を考えたことが本ガイドラインを公開するに至った当初の経緯となります。

本ガイドラインが要求することでも触れているように、セキュリティはプロダクトの価値と不可分です。

「重視する・しない」ではなく、セキュリティが成された上で初めてプロダクトが成り立つものとなります。

また、個人の技能・判断で行った蓄積では継続的にセキュリティを確保することはできません。

よくある例としては「ここはデータベースから取得した値しか出力されないからescapeは要らない」などです。

技能が足りていない場合はデータベースに保存された値が汚染されているケースの見落としが発生、攻撃が成立するなどし、また、判断については逐次「ここはOK、ここはNG」といった判断が必要となるため、判断の誤りや見落としがあった場合に攻撃が成立するなどします。

この例においてはガイドラインとして一律にコンテキストにそったescapeを行うことを定め、実施していれば攻撃を防げました。

"くさりの丈夫さは一番弱い輪で決まる"とも言う通り、ムラのある対応はムリを生み、防護がムダになります。

そのため、セキュリティを確保するためには規約を定め、それが実施されている事を検証し続けることが要求されます。

また、規約を適宜改定することで個人の技能に寄らない、ムラの無い防護を継続的に提供することが出来ます。

これが実現できれば、システム全体で一律の防護を展開することが可能となります。

その前提において、本ガイドラインをご一読頂ければと思います。

なお、本来はトップレベルのガイドラインはフレームワークとして具体を持たず指針のみとし、具体的な内容は参照として疎結合にするべきなのですが、本ガイドラインにおいては見通しを優先し、具体を直接記載しています。

本ガイドラインが要求すること

本ガイドラインは次の情報セキュリティにおける七つの性質を確保することを要求します。

  • 機密性:許可された対象に対してのみ情報が公開され、使用可能であり、許可されていない対象に対しては情報が非公開、使用不可である状態を確保すること。
  • 完全性:情報が正確であり、改ざん・破壊・消去などされていない、完全な状態であることを確保すること。
  • 可用性:許可された対象が要求した時点で中断することなく情報にアクセス・使用ができる状態を確保すること。
  • 真正性:情報がその通りのものであることの確実性を確保すること。
  • 責任追跡性:情報に対する操作が操作者自身及び操作の一意な追跡性を確保すること。
  • 否認防止:操作や事象に対して、否認が出来ない証明を確保すること。
  • 信頼性:意図した動作、意図した結果を得られる状態を確保すること。

開発における要求

本ガイドラインは次の原則の実施を要求します。

  • リスクアセスメントの実施
  • 信頼性設計の適用
  • エラープルーフ化の実施
  • 明示的な許可が無い物はすべて拒否・禁止
  • コンテクストに立脚した信頼境界線の定義と境界線入出力の定義
  • 認証・認可に対するレベルの定義と実施

リスクアセスメントの実施

本ガイドラインはリスクアセスメントの実施を要求します。

本ガイドラインで言うリスクとは、人やシステム、ビジネスに対して悪影響を与える可能性の積です。
そのため、予測・想定されるリスクの可能性・頻度・規模を割り出す必要があります。

次のプロセスを経ることで、リスク対応が可能な状態を確保します。

  • リスク特定:リスクの発見、認識、記録
  • リスク分析:特定したリスクの特質の解明、リスクレベルの決定
  • リスク評価:分析したリスクがリスク基準に対して受け入れ可能かどうかを決定

リスク評価後は「発生源自体の除去」「発生頻度の漸減」「発生後の結果の変更」「状態を保有し続ける」などのリスク軽減・回避施策をとり、より安全な状態を実現します。

信頼性設計の適用

本ガイドラインは次の手法を考慮し、信頼性のある設計を要求します。

  • フェイルセーフ

    • 故障時は安全な側へ移行するようにする
      例)攻撃と見なせるリクエスト場合、例外として処理を終了する
      リリース直後のみあり得るユーザエラーとの識別が困難な場合は異常検出用ログに記載の上、以降の処理で最終判断させる
  • フォールトトレラント

    • 構成要素中それぞれでエラーが発生しても、システム全体としては機能し続けるようにする
      例)トランザクションを達成するために不要な要素(広告など)でエラーが発生しても、トランザクションを完了できるようにする
  • フールプルーフ

    • 誤った操作を行っても致命的な結果が起こらないようにする
  • フェイルソフト

    • 動作しない機能が発生した場合に機能を切り離し、被害を最小化かつ機能が縮退した状態でもサービス提供を続行できるようにする
  • フェイルオーバー

    • システムで異常が発生した場合に冗長待機系に切り替え、サービス提供を続行できるようにする
      例)APIが停止した場合にセカンダリに接続しに行くようにする
  • アクティブセーフティ(予防安全)

    • エラーを未然に防げるようにする
      例)かもしれない実装、値に対する警告、変数型の定義、値オブジェクト・エンティティの活用
  • パッシブセーフティ(受動安全)

    • エラーが発生した場合にシステムの健全性、トランザクションの進行などへの影響を最小限にする
  • 損傷許容度設定

    • 「どの程度の内容までならば、対応する、しない」の判断をできるようにする
      頻度、深刻度で構成された四象限マトリックスが必要となる
      例)過去5年に一回(低頻度)かつ悪影響のある発火はない(低深刻度)のため今回のリリースには含めない

エラープルーフ化の実施

本ガイドラインはヒューマンエラーの発生防止と、それでも発生した場合の影響波及を防止するために、あらかじめしくみや手順を設計に織り込むことを要求します。

  • 発生防止

    • 排除:エラーの原因を事前に取り除くこと
      例)エラーの原因あるいは可能性のある業務そのものを不要にする
    • 代替化:人の作業を機器やシステムなどに代替させること
      例)ログ集計やBI向けデータ取得
    • 容易化:作業をしやすくこと
      例)ルールに準じた色の使用により高速に判断させたり、shellの使用ではなくWebツールを使用する
  • 波及防止

    • 異常検出:エラーを可能な限り速やかに検出し、迅速な対応につなげること
      迅速な対応につなげるために「狼少年」にならないようにすること
      エラーは「出ているか」「出ていないか」で判断できるようにすること
      パフォーマンスなど累積し、兆候を見つけて初めて異常を検出できる対象があることに注意すること
      兆候から検出する場合は閾値を設け「出ているか」「出ていないか」の判断ができるようにすること
    • 影響緩和:エラーが発生しても、その影響を可能な限り小さく、他に波及しないようにすること
      迅速に非常態勢に移行することで他部署への影響を最小化するなど

明示的な許可が無い入出力はすべて拒否・禁止

本ガイドラインは明示的な許可が無い入出力はすべて拒否・禁止することを要求します。

コンテクストに立脚した信頼境界線の定義と境界線入出力の定義

本ガイドラインはコンテクストに立脚した信頼境界線の定義と境界線入出力の定義及びその検証を要求します。

原則として入力、出力が信頼境界線となります。

認証・認可に対するレベルの定義と実施

本ガイドラインは認証(Authentication)、認可(Authorization)を分離しそれぞれに対するレベルを定義し、運用することを要求します。

単一のエンティティに対して、同時に複数の認可が与えられる場合も考慮した設計が推奨されます。

規約

本ガイドラインは次の規約の遵守を要求します。

  • 入力値検証
  • データの正規化
  • 出力
  • 認証・認可
  • SESSION・COOKIE管理
  • TLSの利用
  • 監視

入力値検証

本ガイドラインはシステム外部からの入力値は全て検証(Validation)を行い、検証失敗の場合は検証エラーレベルに応じた応答をすることを要求します。

フロントエンドインターフェースでの入力内容確認はUX向上のための施策でしかなく、それを検証としてはなりません。

そのため、検証は必ずバックエンドで行うことを要求します。

また、システム内部でも信頼性境界を超える入力の場合、検証(Validation)が必要となる場合があります。

入力値検証細則

  • 規定値を出力し、受け取る項目の場合、規定値に存在する値かどうかで検証することを要求します。
  • 項目そのものが送られてきていない場合、攻撃を受けているとみなします。
  • 入力値のエンコーディングが適切か検証することを要求します。
  • データベースを利用する場合はプリペアドステートメントを利用することを要求します。
    • 実装に依存しますが、ユーザランド実装によるプリペアドステートメントの使用は推奨されません。
    • LIKEの構築など、不可避な場合を除き生クエリを使用してはなりません。
    • カラムの自由指定を行いたい場合は、入力値をそのまま使うのではなく、カラムリストに該当のカラムがある事を検証し、存在した場合はカラムリスト側の文字列をクエリに使用することを要求します。

検証エラーレベル

検証エラーレベルは次の4レベルの使用が推奨されます。

  • ユーザエラー
    ユーザ入力時にルールにマッチしなかった場合のエラー
    前提として次の画面に遷移させるためにエラーを修正して貰う必要があるため「次の画面へ進むにはどうしたらよいか」をエラーメッセージとして提示することを要求します。
  • ロギングユーザエラー
    ユーザエラーとしての動作に加え、ログの保存を要求します。
  • ロギングパススルー
    ユーザエラーとしての検証は行うが、ユーザエラーの表示はせず、ログの保存を要求します。
  • 例外
    来るべき値が来ていないなど、仕様に則っていないまたは攻撃と見なせるリクエストに対するエラー
    何が原因でそれが起きたのかを知る必要があるため、「何を与えられた結果どうなった」をエラーメッセージとして提示することを要求します。

データの正規化

本ガイドラインはシステム外部からの入力値は検証した値を元に適切なキャストを行い、以降の処理に用いることを要求します。

出力

本ガイドラインは規格に則ったフォーマッターを用いた出力を行うことを要求します。

出力細則

  • 出力する箇所においてコンテキストに応じたエンクロージング及びエスケープを行うことを要求します。
  • CSRFが成立しない実装にする事を要求します。
  • iframeやクリックジャッキングが成立しないhttp response header出力を行うことを要求します。
  • エスケープにおいて、コンテキスト境界をまたぐ内容の出力は行ってはなりません。  例)HTMLタグそのものの出力
    name="value"などHTML属性名と属性値がセットとなった出力
  • URLに付与するパラメータはhttp_build_queryなど、システム標準かつ安全が確認されている処理で作成、付与することを要求します。
  • 連続する画面間のデータの受け渡しにhiddenフィールドを使用してはなりません。

認証・認可

認証(Authentication)

認証にはTLSが利用可能な前提において、次の認証方式を利用できます。

  • BASIC認証
  • Bearer認証
  • HTMLフォーム認証
  • クライアント認証

実現が不可能な場合を除き、タイムアウトとログアウトを実装する事を要求します。

パスワード

認証に用いるパスワードは次の項目を満たす必要があります。

  • パスワードの文字種は半角英数字記号スペースを利用できる事を要求します
  • パスワードは最低長は8文字以上であることを要求します
    • パスワードは最低長は8文字よりも長い事が推奨されます
  • パスワードの最大長はストレージ側が持つ最大長未満に制約してはなりません
  • パスワードはSHA256以上の強度を持つアルゴリズムでハッシュ化し保存する事を要求します
  • ハッシュを作成する場合はソルトの設定とストレッチングを行うことを要求します
  • パスワードは変更可能である事を要求します
    • パスワード変更時には既存のパスワードの確認を要求します
    • パスワードの変更があった旨を被認証者に通知する事を要求します
  • 定期的なパスワード更新の強制は行ってはなりません
  • 特定の文字種の使用の強制は行ってはなりません
  • 「任意の語を複数種類混在させて利用する」事を推奨します

認可(Authorization)

本ガイドラインは認可(Authorization)は認証が成功したのちに設定する事を要求します。

SESSION・COOKIE管理

本ガイドラインは全てのクッキーにおいて次の設定を要求します。

  • session_nameは標準から変更することを要求します
  • session_idは標準から変更することを要求します
  • session_idなどの文字列を除き、クッキーに記載するデータはAES256以上の強度をもつアルゴリズムで暗号化することを要求します
  • 無期限クッキーを発行してはなりません

TLSの利用

本ガイドラインは信頼性境界を超える全ての通信をTLSで保護することを要求します。

監視

本ガイドラインは操作に対するログの保持を要求します。

また、標準から離れた操作が行われた場合の警報発報を要求します。

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

【webpack】sassをバンドルする際に出たエラーの対処

環境

PHP 7.3.8
Laravel 6.18.40
webpack 4.27.1

TypeError: text.forEach is not a function ... というエラー

Docker開発環境下で、npm run watch-poll でビルドを試みるも以下エラーで失敗する。

ERROR in ./resources/scss/app.scss
Module build failed (from ./node_modules/css-loader/dist/cjs.js):
TypeError: text.forEach is not a function
    at /var/www/node_modules/extract-text-webpack-plugin/dist/loader.js:145:16
(略)

結論から言うと、sassのバンドルにはcss-loadernode-sasssass-loaderstyle-loaderを用いているが、それぞれのバージョンによる互換性の問題であった。

今回はcss-loaderのバージョンを4.3から下げる事で改善した。

npm uninstall --save-dev css-loader
npm install --save-dev css-loader@3.5.3

最終的に以下の構成でビルドを確認した。

package.json
"css-loader": "^3.5.3",
"laravel-mix": "^4.0.7",
"lodash": "^4.17.13",
"node-sass": "^4.14.1",
"resolve-url-loader": "^2.3.1",
"sass": "^1.15.2",
"sass-loader": "^7.1.0",
"style-loader": "^1.2.1",

参考にした記事

npm がどうしてもエラーになる
Laravel6でVueを使おうとnpm run watchをするとエラーが出る

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

PHP date UNIXタイムスタンプ

date

構文
date(フォーマット, UNIXタイムスタンプ)
[例]
date('Y-m-d', time());   2020-09-28
Yは、年を4桁で表します。
mは、月を2桁で表します。
dは、日を2桁で表します。

[例2]
date('H:i:s',time();   21:51:30
Hは、時を2桁で表します。
iは、分を2桁で表します。
sは、秒を2桁で表します。

[例3]
date('l', time());   Monday
lは、曜日をを表します。

[例4]
date('W', time());   20
一年に内の何週目かを表します。

[例5]
echo date('Y-m-d l') . PHP_EOL;
現在日時を表示したい場合は、time()を省略する事が可能です。

その他も沢山あるみたいなので、必要な時に調べて使いましょう。

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

【PHP】【エラー】Notice: Array to string conversion in~

PHPについて学習内容を備忘録としてまとめます。
実装中に起きたエラーについて解決法まで記載します。

下記のようなマイページでユーザーの投稿数を表示させる際にエラーが発生しました。
image.png

起きた問題

ユーザーの投稿数を取得し表示したら下記のようなエラーが発生しました。

Notice: Array to string conversion in C:\xampp3\htdocs\user\user_mypage.php on line 22
Array

投稿数を表示させる箇所にArrayと表示されおり、投稿数は表示されていませんでした。
該当のコードはこちらになります。

<?php
print'<p>投稿数:'.get_user_count('post',$current_user['id']).'</p>';
?>

get_user_countは引数の値によって投稿数を取得しています。

原因

Array to string conversionとは配列が文字列に変換されてしまうというエラーメッセージです。
なので今回の場合はget_user_countで取得した配列が文字列に変換されているためにエラーが発生したと思われます。

解決法

配列の中の要素を返すようにすれば良いので、currentを使用します。
currentは配列の現在参照している要素を返します。

<?php
$a[] = 1;
$a[] = 2;
$a[] = 3;
print current( $a ); // 結果:1
?>

currentを先ほどのコードに追記してみます。

<?php
print'<p>投稿数:'.current(get_user_count('post',$current_user['id'])).'</p>';
?>

正常に動きました。
image.png

参考URL

https://php-beginner.com/function/array/current.html
https://marycore.jp/prog/php/notice-array-to-string-conversion/

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

SJIS-macに変換したはずなのにSJIS-winになる

SJIS-macに変換したはずなのにSJIS-winになる

StackOverflowにmb_detect_encoding does not detect SJIS-mac?という投稿がありました。

01.png

ということらしいので、UTF-8からSJIS-macに変換してみましょう。

$utf8Str = "❶❷❸❹❺";

$sjisStr = mb_convert_encoding($utf8Str, 'SJIS-mac');

echo(mb_detect_encoding($sjisStr, ['UTF-8','SJIS-mac', 'SJIS-win', 'SJIS'])); // SJIS-win ←

SJIS-macに変換したはずなのに、何故かSJIS-winと判定されてしまいます。

そもそもSJIS-macってなんだよって話ですが、単にMacJapaneseのエイリアスです。
従ってMacJapaneseと書いても同じく、正しく誤判定されます。

そしてコメント欄This is a bug in PHP's mbstring extension『mbstringエクステンションのバグじゃよ』という人が現れています。
間違ったコードを書いたときに自分のせいではなく言語・ライブラリのせいにする人はよくいますが、この人の場合は適当言ってるわけではなく本当にバグです。

https://github.com/php/php-src/pull/6052

コメント主のalexdowadは先日、PHPソースにプルリクを送り付けました。
中身はmbstringエクステンションに関するもので、バグ修正、不要なコードの削除、トリッキーなコードのリファクタリング、高速化、テストカバレッジ上昇など多くの改善が含まれています。

これがなかなか大規模で、91コミット5万行以上のごつい変更となっています。
さすがに大半はテスト用の変換テーブルみたいですが、それでも実コードの修正も数千行は入っています。

mbstringなので日本語に影響するかもしれないということで少し覗いてみました。

https://github.com/php/php-src/pull/6052#issue-475811327

このプルリクはmbstringの改善を目的としていいる。

・不要なコードを削除。
・曖昧なコードを読みやすくする。
・残ってしまったトリッキーなコードにはコメントを追加する。
・全てを高速化する。
・バグの修正。
・テストカバレッジの向上。
・一貫性を向上させるため、セマンティクスを調整した。

もしかしたらパフォーマンスのためにあえてそのように書かれているのではないかと考えていた部分もあったが、実際はそうでもなかった。
リファクタリングすることは、パフォーマンスのためにも良いことだった。
性能チェックのために591のベンチマークを走らせた。
幾つか性能劣化が見つかったが、それも修正した。
最適化の余地はまだまだ大量に残されているので、いずれそれらも改善する予定である。

https://github.com/php/php-src/pull/6052/commits/0880fc00094f363dcaac6762f2ec94a02e380baf

mb_check_encodingを最適化したよ
これによってmb_check_encodingが50%高速化されます。

https://github.com/php/php-src/pull/6052/commits/6197b8e7a2ec81e63e1079812741dccdab0b6417

HTMLの数値エンティティ変換を最適化したよ。
これによってmb_encode_numericentityが10%高速化されます。

https://github.com/php/php-src/pull/6052/commits/e3d5a05a97c5ef8d60489ca3491930bf75d79647

mb_str_splitの本体をmbfl_str_splitに移したよ。あとバグも修正した。

https://github.com/php/php-src/pull/6052/commits/4d9999674ec9fc298e3fe26e7445f24c50575792

ISO-8859-xエンコーディングの変換テーブルとテストスイートを追加したよ。

ちなみにISO-8859-3(Latin-3)ISO-8859-6 (Latin/Arabic)ISO-8859-7 (Latin/Greek)ISO-8859-8 (Latin/Hebrew)ISO-8859-16 (Latin-10)が追加されてる。

https://github.com/php/php-src/pull/6052/commits/322bf79b92ab98d54fde1b30c9ebb633b71d8391

MBFL_CHP_{CTL,DIGIT,UALPHA,LALPHA,MSPECIAL}使ってないから削除したよ。

https://github.com/php/php-src/pull/6052/commits/5b0984a5a513cdc7b7a76986c7dc391c90725d3f

意味のないポインタ参照を削除したよ。

    (const char *(*)[])&mbfl_language_uni_aliases,

というか何これ。

https://github.com/php/php-src/pull/6052/commits/136a03103e8566fa89189c8dc65cb6bffe5c92a4

IMAPのために、UTF-7の改良版Modified UTF-7を追加したよ。

https://github.com/php/php-src/pull/6052/commits/fb98c004cb71fe0707ce35bc46c9cfb8465b9891

Shift-JIS-2004を追加したよ。

https://github.com/php/php-src/pull/6052/commits/2621a78593d8bc1ef5131521b56ca1d98a86776e

ISO-2022-JP-2004の識別を厳密にしたよ。

https://github.com/php/php-src/pull/6052/commits/26204445a7e8b08ee5a0dbbbe247e970e75df8dc

mb_convert_kanaが存在しない変換オプションに例外を出すようにしたよ。

https://github.com/php/php-src/pull/6052

動作検証のためにテストコードを追加したら、既存コードに多くのバグを発見してしまったよ。

https://github.com/php/php-src/pull/6052#issuecomment-695835716

mb_strposに面白い問題を発見したよ。
mb_strposには$needleの長さ > $haystackの長さだったら即falseにするって処理が入ってるんだけど、同じ文字を複数の形式で表すことができるエンコーディングもあって、その場合$needleの長さ > $haystackの長さになることがあるんだよね。

他にも大量のコミットが存在します。

Nikita

レビュアーとしてNikitaが現れる
いや本当、何処に行ってもこの人がいるな。

mbstringは元々libmbflというライブラリを使ってたんだけど、これはもう長いことメンテされていない。
なのでPHP7.3/7.4でこのあたりに大幅なメスを入れたんだけど、まだ概念的には別のライブラリとして扱っている。
このプルリクではlibmbflを捨て、直接Zend APIとして使用するようにしてるけど、それによるデメリットはないので今後はそうする方向がいいと思う。

あとヘッダ部分の書式、何処から来てるのかはわからないけど3スペースのインデントが使われているけど、これはそのままのほうがいいだろう。

ものすごい勢いで突っ込み・コミットを入れているものの、全体的には反対ではなさそうです。
おそらくこの変更はそのうちマージされそうな雰囲気です。

感想

いやあ、この人やばいね。
日本人でも全然わかってない人も多い(私とか)Shift_JIS-2004やらISO-2022-JP-2004やらも的確に対応して大量のテストを追加してさらに最適化・高速化まで成し遂げてしまう。
すごすぎる熱量と能力です。

さらにこのプルリク、(part 1)とか書いてありますからね。
パート2では今回あまり手の入っていないmb_convert_encodingあたりにも最適化が入りそうですね。

しかし、mbstringの主戦場である日本や中国からのコントリビュートが全くないのが気になります。
極端な話英語圏ではmbstringがなくてもなんとかなりますから、切り捨てられないためにも日本からも有識者が参加して存在感を出してもらいたいところですね。

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

PHP キー、値で配列をソートしよう

sort rsort

sort

配列の値を小さい順に並び変えてくれます。

rsort

配列の値を多い順に並び変えてくれます。

※キーがある場合は消されて連番になってしまいます。

そこで!↓

asort arsort

asort arsortはキーを保持したまま、並び替えてくれます。

ksort krsort

ksort krsortはキーの方を並び変えてくれます

array_column

引数に渡した配列の中から、単一のカラムを取り出すための関数です。
[例]
array_colimn($配列, $カラム)
スクリーンショット 2020-09-28 18.40.40.png
スクリーンショット 2020-09-28 18.40.52.png

array_multisort

[例]
array_multisort($scores, $names)
配列の中をデフォルトで、小さい順にしてくれます。
アルファベットの場合は、a~z順に並び替えてくれます。

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

【PHP】文字列を固定長に揃える(マルチバイト文字)

はじめに

マルチバイト文字列を指定の長さで揃えたい

sprintf

sprintf('%-10s', 'abc'); // "abc       "
sprintf('%-10s', 'あいうえお') // "あいうえお"

sprintfではマルチバイト文字のときに上手く対応できない

参考:PHP: sprintf - Manual

str_pad

str_pad('abc', 10, ' ',STR_PAD_RIGHT) // "abc       "
str_pad('あいう', 10, ' ',STR_PAD_RIGHT) // "あいう "

バイト数換算で計算されるので、マルチバイト文字の場合、指定が上手く対応できない

参考:PHP: str_pad - Manual

以下のような関数を作ってマルチバイト文字列の長さを揃える

/**
 * 指定の文字列の長さまで全角で埋める
 * @return string $input 埋めた後の文字列
 */
 public function mbStrPad($input, $pad_length) {
     while(mb_strlen($input) < $pad_length) {
         $input .= ' ';
     }
     return $input;
 }

mbStrPad('あいうえお', 10); // "あいうえお     "
mb_strlen(mbStrPad('あいうえお', 10)); // 10

上記のような関数を作ってあげてマルチバイト文字列の長さを揃えた

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

LINEBOTでFlex Messageをつかってリッチに見せよう

スクリーンショット 2020-09-28 2.06.49.png
以前飲食店の予約LINE BOTを作成したのですが、Flex Messageがかなり好評だったので共有したいと思います
Flex Messageってこんなやつでとりあえず見た目が洒落になる
カスタマイズ性も抜群だからLINE BOT作るときは是非使ってみて欲しい

公式にシュミレータがあるので、そちらを参考にして
この記事のコードを少し変えれば簡単にリッチなメッセージを楽しめると思いますー
https://developers.line.biz/flex-simulator/

公式ドキュメントはこちらから
https://developers.line.biz/en/docs/messaging-api/
リッチメニューは完全にGUIでできるので下のページを確認してください
https://manager.line.biz/account/

目次

  1. 概要
    1. コンテナ
    2. ブロック
    3. コンポーネント
  2. 雛形の導入
  3. カルーセルメッセージ編
    1. 地元のお店サーチ
    2. アパレル
    3. ショッピング
  4. バブルメッセージ編
    1. レストラン
    2. QRつき映画チケット
    3. 乗り換え案内
    4. メニュー
    5. ホテル
    6. 不動産
  5. 注意

概要

Flex Messageの構成要素はコンテナ、ブロック、コンポーネントの三層です
ブロックはヘッダー、ヒーロー、ボディ、フッターを含みます
こんな感じ
スクリーンショット 2020-09-28 2.32.16.png

コンテナ

コンテナは最上位の構造で、バブルカルーセルを選択します
今回コードを使って説明するときもこの二つに大分しています
バブルは単体のメッセージバブルを表示できて
カルーセルは複数のメッセージバブルを横に並べて表示することができます

もちろん送信するときにバブルを連想配列にすれば複数個のバブルを縦に連結して送ることもできます
その場合は通知が2件以上になってしまうので注意しましょう

ブロック

バブルを構成する構造

タイプ 説明
ヘッダー メッセージの件名や見出しを表示するブロック
ヒーロー 画像などの主要なコンテンツを表示するブロック
ボディ 主要なメッセージコンテンツを表示するブロック
フッター ボタンや補足情報を表示するブロック

この順番でバブルの上から表示されて
どのブロックもバブル内で一つだけ指定できる

コンポーネント

ブロックを構成する要素

タイプ 説明
ボックス コンポーネントのレイアウトを定義するコンポーネント
ボタン ボタンを表すコンポーネントでユーザーがタップすると指定したアクションが実行される
画像 画像を描画するコンポーネント
アイコン アイコンを描画するコンポーネント
テキスト 一行の文字列を描画するコンポーネント
スパン 一行にデザインが異なる複数の文字列を描画するコンポーネントで色、サイズ、太さ、及び装飾を指定できる
セパレータ 分割線を描画するコンポーネント
フィラー スペースを作るためのコンポーネント
スペーサー(非推奨) ボックス内の先頭または末尾に固定サイズのスペースを配置する不可視のコンポーネント

ボックス

メッセージのレイアウトを定義するためのコンポーネント
めっちゃ使う。他のコンポーネントを含めることができるから
子要素はボックスのプロパティと子要素のプロパティに従ってレイアウトされる

ボタン

以下の三つのスタイルがあり、それぞれ色を変更できる
スクリーンショット 2020-09-28 2.44.49.png

example.php
[
    "type" => "bubble",
    "body" => [
        "type" => "box",
        "layout" => "vertical",
        "spacing" => "md",
        "contents" => [
            [
                "type" => "button",
                "style" => "primary",
                "action" => [
                    "type" => "uri",
                    "label" => "Primary style button",
                    "uri" => "https://example.com"
                ]
            ],
            [
                "type" => "button",
                "style" => "secondary",
                "action" => [
                    "type" => "uri",
                    "label" => "Secondary style button",
                    "uri" => "https://example.com"
                ]
            ],
            [
                "type" => "button",
                "style" => "link",
                "action" => [
                    "type" => "uri",
                    "label" => "Link style button",
                    "uri" => "https://example.com"
                ]
            ]
        ]
    ]
]

指定できるアクションは

アクション名 説明
ポストバックアクション 特定の文字列を含むポストバックイベントをサーバーに返す。ユーザーが入力したテキストを含めることもできる
メッセージアクション ユーザーからのテキストメッセージとして特定の文字列をかえす
URIアクション ユーザーを特定のURIにリダイレクトさせる
日時選択アクション ユーザーにメニューから特定の日付、時刻、また日時を選択させる。ユーザーが選択した日時はWebhookを介してポストバックイベントで返される
カメラアクション クイックリプライボタンにのみ設定でき、LINE内のカメラが起動する
カメラロールアクション クイックリプライボタンにのみ設定でき、LINE内のカメラロール画面が開く
位置情報アクション クイックリプライボタンにのみ設定でき、LINE内の位置情報画面が開く

アイコン

隣接するテキストを装飾するためにアイコンを描画するコンポーネントでベースライン内でのみ使うことができる
あとで出てくるレビューの星のマークとかが好例
地元のお店サーチ
レストラン
ホテル

テキスト

一行の文字列を描画するコンポーネントで色サイズ及太さを指定できる

テキストを折り返す方法

デフォルトではテキストの幅を超える部分は省略記号...に置き換えられてしまうが
textプロパティの下にwrapプロパティにtrueを指定(追加)し
改行コード\nを入力すればテキストを任意の場所で折り返すことができる

example.php
[
    "type" => "bubble",
    "body" => [
        "type" => "box",
        "layout" => "vertical",
        "contents" => [
            [
                "type" => "text",
                "text" => "Closing the distance",
                "size" => "md",
                "align" => "center",
                "color" => "#ff0000",
            ],
            [
                "type" => "text",
                "text" => "Closing the distance",
                "size" => "lg",
                "align" => "center",
                "color" => "#00ff00",
            ],
            [
                "type" => "text",
                "text" => "Closing the distance",
                "size" => "xl",
                "align" => "center",
                "weight" => "bold",
                "color" => "#0000ff",
            ]
        ]
    ]
]

スパン

イメージが湧かないと思うのでコードを混ぜて解説していこうと思います
一行にデザインが異なる複数の文字列を描画するコンポーネントで
色、サイズ、太さ、及び装飾を指定できる
テキストのcontentsプロパティに設定する
スクリーンショット 2020-09-28 3.22.47.png

example.php
[
    "type" => "bubble",
    "body" => [
        "type" => "box",
        "layout" => "horizontal",
        "contents" => [
            [
                "type" => "text",
                "text" => "hello, world",
                "contents" => [
                    [
                        "type" => "span",
                        "text" => "Hello, world!",
                        "decoration" => "line-through"
                    ],
                    [
                        "type" => "span",
                        "text" => "\nClosing",
                        "color" => "#ff0000",
                        "size" => "sm",
                        "weight" => "bold",
                        "decoration" => "none"
                    ],
                    [
                        "type" => "span",
                        "text" => " "
                    ],
                    [
                        "type" => "span",
                        "text" => "the",
                        "size" => "lg",
                        "color" => "#00ff00",
                        "decoration" => "underline",
                        "weight" => "bold"
                    ],
                    [
                        "type" => "span",
                        "text" => " "
                    ],
                    [
                        "type" => "span",
                        "text" => "distance",
                        "color" => "#0000ff",
                        "weight" => "bold",
                        "size" => "xxl"
                    ]
                ],
                "wrap" => true,
                "align" => "center"
            ]
        ]
    ]
]

セパレータ

こちらもイメージが湧きにくいと思うのでコード混じりで解説していこうと思います
ボックス内に分割線を描画するコンポーネントで水平ボックスに含めた場合は垂直線、垂直ボックスに含めた場合は水平線が描画される
スクリーンショット 2020-09-28 3.23.00.png

example.php
[
    "type" => "bubble",
    "body" => [
        "type" => "box",
        "layout" => "vertical",
        "spacing" => "md",
        "contents" => [
            [
                "type" => "box",
                "layout" => "horizontal",
                "spacing" => "md",
                "contents" => [
                    [
                        "type" => "text",
                        "text" => "orange"
                    ],
                    [
                          "type" => "separator"
                    ],
                    [
                        "type" => "text",
                        "text" => "apple"
                    ]
                ]
            ],
            [
                "type" => "separator"
            ],
            [
                "type" => "box",
                "layout" => "horizontal",
                "spacing" => "md",
                "contents" => [
                    [
                        "type" => "text",
                        "text" => "grape"
                    ],
                    [
                        "type" => "separator"
                    ],
                    [
                        "type" => "text",
                        "text" => "lemon"
                    ]
                ]
            ]
        ]
    ]
]

雛形の導入

おうむ返しはできている前提で話を進めていきます
以下のコードを雛形として使用してください
今回はテストをしやすくするためにpush通知を使用しています。
適宜読み換えて使用してください
Configファイルを使用していますがベタ書きでも大丈夫です
Configファイルを使用する場合はこちらの記事を
https://qiita.com/satorunooshie/items/ca41f7c824c7ea747708

template.php
<?php
ini_set("display_errors", "On");
ini_set("error_reporting", E_ALL);
require_once "./vendor/autoload.php";
require_once "Config.php";
Config::setConfigDirectory(__DIR__ . "/config");
$access_token = Config::get("ACCESSTOKEN");
$headers = [
    "Authorization => Bearer " . $access_token,
    "Content-Type => application/json; charset=utf-8",
];
$post = [
    "to" => Config::get("TESTLINEID"), //user id
    "messages" => [
        //ここに実装したいFlex Messageのコードを挿入
    ],
];
$post = json_encode($post);
$ch = curl_init("https://api.line.me/v2/bot/message/push");
$options = [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_HTTPHEADER => $headers,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_BINARYTRANSFER => true,
    CURLOPT_HEADER => true,
    CURLOPT_POSTFIELDS => $post,
];
curl_setopt_array($ch, $options);

$result = curl_exec($ch);

if (curl_errno($ch)) return;
$info = curl_getinfo($ch);
$http_status = $info['http_code'];

$response_header_size = $info['header_size'];
$body = substr($result, $response_header_size);

echo $http_status . " " . $body; //200 {} is ok

カルーセルメッセージ

地元のお店サーチ

スクリーンショット 2020-09-28 2.05.40.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "carousel",
        "contents" => [
            [
                "type" => "bubble",
                "size" => "micro",
                "hero" => [
                    "type" => "image",
                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip10.jpg",
                    "size" => "full",
                    "aspectMode" => "cover",
                    "aspectRatio" => "320:213",
                ],
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "brown cafe",
                            "weight" => "bold",
                            "size" => "sm",
                            "wrap" => true,
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png",
                                ],
                                [
                                    "type" => "text",
                                    "text" => "4.0",
                                    "size" => "xs",
                                    "color" => "#8c8c8c",
                                    "margin" => "md",
                                    "flex" => 0,
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "box",
                                    "layout" => "baseline",
                                    "spacing" => "sm",
                                    "contents" => [
                                        [
                                            "type" => "text",
                                            "text" => "東京旅行",
                                            "wrap" => true,
                                            "color" => "#8c8c8c",
                                            "size" => "xs",
                                            "flex" => 5,
                                        ]
                                    ]
                                ]
                            ]
                        ]
                    ],
                    "spacing" => "sm",
                    "paddingAll" => "13px",
                ]
            ],
            [
                "type" => "bubble",
                "size" => "micro",
                "hero" => [
                    "type" => "image",
                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip11.jpg",
                    "size" => "full",
                    "aspectMode" => "cover",
                    "aspectRatio" => "320:213",
                ],
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "Brown&Cony Restaurant",
                            "weight" => "bold",
                            "size" => "sm",
                            "wrap" => true,
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png",
                                ],
                                [
                                    "type" => "text",
                                    "text" => "4.0",
                                    "size" => "sm",
                                    "color" => "#8c8c8c",
                                    "margin" => "md",
                                    "flex" => 0,
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "box",
                                    "layout" => "baseline",
                                    "spacing" => "sm",
                                    "contents" => [
                                        [
                                            "type" => "text",
                                            "text" => "東京旅行",
                                            "wrap" => true,
                                            "color" => "#8c8c8c",
                                            "size" => "xs",
                                            "flex" => 5,
                                        ]
                                    ]
                                ]
                            ]
                        ]
                    ],
                    "spacing" => "sm",
                    "paddingAll" => "13px",
                ]
            ],
            [
                "type" => "bubble",
                "size" => "micro",
                "hero" => [
                    "type" => "image",
                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip12.jpg",
                    "size" => "full",
                    "aspectMode" => "cover",
                    "aspectRatio" => "320:213",
                ],
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "Tata",
                            "weight" => "bold",
                            "size" => "sm",
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                                ],
                                [
                                    "type" => "icon",
                                    "size" => "xs",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png",
                                ],
                                [
                                    "type" => "text",
                                    "text" => "4.0",
                                    "size" => "sm",
                                    "color" => "#8c8c8c",
                                    "margin" => "md",
                                    "flex" => 0,
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "box",
                                    "layout" => "baseline",
                                    "spacing" => "sm",
                                    "contents" => [
                                        [
                                            "type" => "text",
                                            "text" => "東京旅行",
                                            "wrap" => true,
                                            "color" => "#8c8c8c",
                                            "size" => "xs",
                                            "flex" => 5,
                                        ]
                                    ]
                                ]
                            ]
                        ]
                    ],
                    "spacing" => "sm",
                    "paddingAll" => "13px",
                ]
            ]
        ]
    ]
],

アパレル

スクリーンショット 2020-09-28 2.06.49.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "carousel",
        "contents" => [
            [
                "type" => "bubble",
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "contents" => [
                        [
                            "type" => "image",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip1.jpg",
                            "size" => "full",
                            "aspectMode" => "cover",
                            "aspectRatio" => "2:3",
                            "gravity" => "top",
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "box",
                                    "layout" => "vertical",
                                    "contents" => [
                                        [
                                            "type" => "text",
                                            "text" => "Brown T-shirts",
                                            "size" => "xl",
                                            "color" => "#ffffff",
                                            "weight" => "bold",
                                        ]
                                    ]
                                ],
                                [
                                    "type" => "box",
                                    "layout" => "baseline",
                                    "contents" => [
                                        [
                                            "type" => "text",
                                            "text" => "\35,800",
                                            "color" => "#ebebeb",
                                            "size" => "sm",
                                            "flex" => 0,
                                        ],
                                        [
                                            "type" => "text",
                                            "text" => "\75,000",
                                            "color" => "#ffffffcc",
                                            "decoration" => "line-through",
                                            "gravity" => "bottom",
                                            "flex" => 0,
                                            "size" => "sm",
                                        ]
                                    ],
                                    "spacing" => "lg"
                                ],
                                [
                                    "type" => "box",
                                    "layout" => "vertical",
                                    "contents" => [
                                        [
                                            "type" => "filler",
                                        ],
                                        [
                                            "type" => "box",
                                            "layout" => "baseline",
                                            "contents" => [
                                                [
                                                    "type" => "filler",
                                                ],
                                                [
                                                    "type" => "icon",
                                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip14.png",
                                                ],
                                                [
                                                    "type" => "text",
                                                    "text" => "Add to cart",
                                                    "color" => "#ffffff",
                                                    "flex" => 0,
                                                    "offsetTop" => "-2px",
                                                ],
                                                [
                                                    "type" => "filler",
                                                ]
                                            ],
                                            "spacing" => "sm"
                                        ],
                                        [
                                            "type" => "filler"
                                        ]
                                    ],
                                    "borderWidth" => "1px",
                                    "cornerRadius" => "4px",
                                    "spacing" => "sm",
                                    "borderColor" => "#ffffff",
                                    "margin" => "xxl",
                                    "height" => "40px",
                                ]
                            ],
                            "position" => "absolute",
                            "offsetBottom" => "0px",
                            "offsetStart" => "0px",
                            "offsetEnd" => "0px",
                            "backgroundColor" => "#03303Acc",
                            "paddingAll" => "20px",
                            "paddingTop" => "18px",
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "SALE",
                                    "color" => "#ffffff",
                                    "align" => "center",
                                    "size" => "xs",
                                    "offsetTop" => "3px",
                                ]
                            ],
                            "position" => "absolute",
                            "cornerRadius" => "20px",
                            "offsetTop" => "18px",
                            "backgroundColor" => "#ff334b",
                            "offsetStart" => "18px",
                            "height" => "25px",
                            "width" => "53px",
                        ]
                    ],
                    "paddingAll" => "0px"
                ]
            ],
            [
                "type" => "bubble",
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "contents" => [
                        [
                            "type" => "image",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip2.jpg",
                            "size" => "full",
                            "aspectMode" => "cover",
                            "aspectRatio" => "2:3",
                            "gravity" => "top",
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "box",
                                    "layout" => "vertical",
                                    "contents" => [
                                        [
                                            "type" => "text",
                                            "text" => "Cony T-shirts",
                                            "size" => "xl",
                                            "color" => "#ffffff",
                                            "weight" => "bold",
                                        ]
                                    ]
                                ],
                                [
                                    "type" => "box",
                                    "layout" => "baseline",
                                    "contents" => [
                                        [
                                            "type" => "text",
                                            "text" => "\35,800",
                                            "color" => "#ebebeb",
                                            "size" => "sm",
                                            "flex" => 0,
                                        ],
                                        [
                                            "type" => "text",
                                            "text" => "\75,000",
                                            "color" => "#ffffffcc",
                                            "decoration" => "line-through",
                                            "gravity" => "bottom",
                                            "flex" => 0,
                                            "size" => "sm",
                                        ]
                                    ],
                                    "spacing" => "lg"
                                ],
                                [
                                    "type" => "box",
                                    "layout" => "vertical",
                                    "contents" => [
                                        [
                                            "type" => "filler",
                                        ],
                                        [
                                            "type" => "box",
                                            "layout" => "baseline",
                                            "contents" => [
                                                [
                                                    "type" => "filler",
                                                ],
                                                [
                                                    "type" => "icon",
                                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip14.png",
                                                ],
                                                [
                                                    "type" => "text",
                                                    "text" => "Add to cart",
                                                    "color" => "#ffffff",
                                                    "flex" => 0,
                                                    "offsetTop" => "-2px",
                                                ],
                                                [
                                                    "type" => "filler",
                                                ]
                                            ],
                                            "spacing" => "sm",
                                        ],
                                        [
                                            "type" => "filler",
                                        ]
                                    ],
                                    "borderWidth" => "1px",
                                    "cornerRadius" => "4px",
                                    "spacing" => "sm",
                                    "borderColor" => "#ffffff",
                                    "margin" => "xxl",
                                    "height" => "40px",
                                ]
                            ],
                            "position" => "absolute",
                            "offsetBottom" => "0px",
                            "offsetStart" => "0px",
                            "offsetEnd" => "0px",
                            "backgroundColor" => "#9C8E7Ecc",
                            "paddingAll" => "20px",
                            "paddingTop" => "18px",
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "SALE",
                                    "color" => "#ffffff",
                                    "align" => "center",
                                    "size" => "xs",
                                    "offsetTop" => "3px",
                                ]
                            ],
                            "position" => "absolute",
                            "cornerRadius" => "20px",
                            "backgroundColor" => "#ff334b",
                            "offsetStart" => "18px",
                            "height" => "25px",
                            "width" => "53px",
                        ]
                    ],
                    "paddingAll" => "0px"
                ]
            ]
        ]
    ]
],
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "bubble",
        "size" => "mega",
        "header" => [
            "type" => "box",
            "layout" => "vertical",
            "contents" => [
                [
                    "type" => "box",
                    "layout" => "vertical",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "FROM",
                            "color" => "#ffffff66",
                            "size" => "sm"
                        ],
                        [
                            "type" => "text",
                            "text" => "Akihabara",
                            "color" => "#ffffff",
                            "size" => "xl",
                            "flex" => 4,
                            "weight" => "bold"
                        ]
                    ]
                ],
                [
                    "type" => "box",
                    "layout" => "vertical",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "TO",
                            "color" => "#ffffff66",
                            "size" => "sm"
                        ],
                        [
                            "type" => "text",
                            "text" => "Shinjuku",
                            "color" => "#ffffff",
                            "size" => "xl",
                            "flex" => 4,
                            "weight" => "bold"
                        ]
                    ]
                ]
            ],
            "paddingAll" => "20px",
            "backgroundColor" => "#0367D3",
            "spacing" => "md",
            "height" => "154px",
            "paddingTop" => "22px"
        ],
        "body" => [
            "type" => "box",
            "layout" => "vertical",
            "contents" => [
                [
                    "type" => "text",
                    "text" => "Total => 1 hour",
                    "color" => "#b7b7b7",
                    "size" => "xs"
                ],
                [
                    "type" => "box",
                    "layout" => "horizontal",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "20:30",
                            "size" => "sm",
                            "gravity" => "center"
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "filler"
                                ],
                                [
                                    "type" => "box",
                                    "layout" => "vertical",
                                    "contents" => [
                                        [
                                            "type" => "filler"
                                        ]
                                    ],
                                    "cornerRadius" => "30px",
                                    "height" => "12px",
                                    "width" => "12px",
                                    "borderColor" => "#EF454D",
                                    "borderWidth" => "2px"
                                ],
                                [
                                    "type" => "filler"
                                ]
                            ],
                            "flex" => 0
                        ],
                        [
                            "type" => "text",
                            "text" => "Akihabara",
                            "gravity" => "center",
                            "flex" => 4,
                            "size" => "sm"
                        ]
                    ],
                    "spacing" => "lg",
                    "cornerRadius" => "30px",
                    "margin" => "xl"
                ],
                [
                    "type" => "box",
                    "layout" => "horizontal",
                    "contents" => [
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "filler"
                                ]
                            ],
                            "flex" => 1
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "box",
                                    "layout" => "horizontal",
                                    "contents" => [
                                        [
                                            "type" => "filler"
                                        ],
                                        [
                                            "type" => "box",
                                            "layout" => "vertical",
                                            "contents" => [
                                                [
                                                    "type" => "filler"
                                                ]
                                            ],
                                            "width" => "2px",
                                            "backgroundColor" => "#B7B7B7"
                                        ],
                                        [
                                            "type" => "filler"
                                        ]
                                    ],
                                    "flex" => 1
                                ]
                            ],
                            "width" => "12px"
                        ],
                        [
                            "type" => "text",
                            "text" => "Walk 4min",
                            "gravity" => "center",
                            "flex" => 4,
                            "size" => "xs",
                            "color" => "#8c8c8c"
                        ]
                    ],
                    "spacing" => "lg",
                    "height" => "64px"
                ],
                [
                    "type" => "box",
                    "layout" => "horizontal",
                    "contents" => [
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "20:34",
                                    "gravity" => "center",
                                    "size" => "sm"
                                ]
                            ],
                            "flex" => 1
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "filler"
                                ],
                                [
                                    "type" => "box",
                                    "layout" => "vertical",
                                    "contents" => [
                                        [
                                            "type" => "filler"
                                        ]
                                    ],
                                    "cornerRadius" => "30px",
                                    "width" => "12px",
                                    "height" => "12px",
                                    "borderWidth" => "2px",
                                    "borderColor" => "#6486E3"
                                ],
                                [
                                    "type" => "filler"
                                ]
                            ],
                            "flex" => 0
                        ],
                        [
                            "type" => "text",
                            "text" => "Ochanomizu",
                            "gravity" => "center",
                            "flex" => 4,
                            "size" => "sm"
                        ]
                    ],
                    "spacing" => "lg",
                    "cornerRadius" => "30px"
                ],
                [
                    "type" => "box",
                    "layout" => "horizontal",
                    "contents" => [
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "filler"
                                ]
                            ],
                            "flex" => 1
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "box",
                                    "layout" => "horizontal",
                                    "contents" => [
                                        [
                                            "type" => "filler"
                                        ],
                                        [
                                            "type" => "box",
                                            "layout" => "vertical",
                                            "contents" => [
                                                [
                                                    "type" => "filler"
                                                ]
                                            ],
                                            "width" => "2px",
                                            "backgroundColor" => "#6486E3"
                                        ],
                                        [
                                            "type" => "filler"
                                        ]
                                    ],
                                    "flex" => 1
                                ]
                            ],
                            "width" => "12px"
                        ],
                        [
                            "type" => "text",
                            "text" => "Metro 1hr",
                            "gravity" => "center",
                            "flex" => 4,
                            "size" => "xs",
                            "color" => "#8c8c8c"
                        ]
                    ],
                    "spacing" => "lg",
                    "height" => "64px"
                ],
                [
                    "type" => "box",
                    "layout" => "horizontal",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "20:40",
                            "gravity" => "center",
                            "size" => "sm"
                        ],
                        [
                            "type" => "box",
                            "layout" => "vertical",
                            "contents" => [
                                [
                                    "type" => "filler"
                                ],
                                [
                                    "type" => "box",
                                    "layout" => "vertical",
                                    "contents" => [
                                        [
                                            "type" => "filler"
                                        ]
                                    ],
                                    "cornerRadius" => "30px",
                                    "width" => "12px",
                                    "height" => "12px",
                                    "borderColor" => "#6486E3",
                                    "borderWidth" => "2px"
                                ],
                                [
                                    "type" => "filler"
                                ]
                            ],
                            "flex" => 0
                        ],
                        [
                            "type" => "text",
                            "text" => "Shinjuku",
                            "gravity" => "center",
                            "flex" => 4,
                            "size" => "sm"
                        ]
                    ],
                    "spacing" => "lg",
                    "cornerRadius" => "30px"
                ]
            ]
        ]
    ]
],

ショッピング

スクリーンショット 2020-09-28 6.00.00.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "carousel",
        "contents" => [
            [
                "type" => "bubble",
                "hero" => [
                    "type" => "image",
                    "size" => "full",
                    "aspectRatio" => "20:13",
                    "aspectMode" => "cover",
                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_5_carousel.png"
                ],
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "Arm Chair, White",
                            "wrap" => true,
                            "weight" => "bold",
                            "size" => "xl"
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "$49",
                                    "wrap" => true,
                                    "weight" => "bold",
                                    "size" => "xl",
                                    "flex" => 0
                                ],
                                [
                                    "type" => "text",
                                    "text" => ".99",
                                    "wrap" => true,
                                    "weight" => "bold",
                                    "size" => "sm",
                                    "flex" => 0
                                ]
                            ]
                        ]
                    ]
                ],
                "footer" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "button",
                            "style" => "primary",
                            "action" => [
                                "type" => "uri",
                                "label" => "Add to Cart",
                                "uri" => "https://linecorp.com"
                            ]
                        ],
                        [
                            "type" => "button",
                            "action" => [
                                "type" => "uri",
                                "label" => "Add to wishlist",
                                "uri" => "https://linecorp.com"
                            ]
                        ]
                    ]
                ]
            ],
            [
                "type" => "bubble",
                "hero" => [
                    "type" => "image",
                    "size" => "full",
                    "aspectRatio" => "20:13",
                    "aspectMode" => "cover",
                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_6_carousel.png"
                ],
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "Metal Desk Lamp",
                            "wrap" => true,
                            "weight" => "bold",
                            "size" => "xl"
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "flex" => 1,
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "$11",
                                    "wrap" => true,
                                    "weight" => "bold",
                                    "size" => "xl",
                                    "flex" => 0
                                ],
                                [
                                    "type" => "text",
                                    "text" => ".99",
                                    "wrap" => true,
                                    "weight" => "bold",
                                    "size" => "sm",
                                    "flex" => 0
                                ]
                            ]
                        ],
                        [
                            "type" => "text",
                            "text" => "Temporarily out of stock",
                            "wrap" => true,
                            "size" => "xxs",
                            "margin" => "md",
                            "color" => "#ff5551",
                            "flex" => 0
                        ]
                    ]
                ],
                "footer" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "button",
                            "flex" => 2,
                            "style" => "primary",
                            "color" => "#aaaaaa",
                            "action" => [
                                "type" => "uri",
                                "label" => "Add to Cart",
                                "uri" => "https://linecorp.com"
                            ]
                        ],
                        [
                            "type" => "button",
                            "action" => [
                                "type" => "uri",
                                "label" => "Add to wish list",
                                "uri" => "https://linecorp.com"
                            ]
                        ]
                    ]
                ]
            ],
            [
                "type" => "bubble",
                "body" => [
                    "type" => "box",
                    "layout" => "vertical",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "button",
                            "flex" => 1,
                            "gravity" => "center",
                            "action" => [
                                "type" => "uri",
                                "label" => "See more",
                                "uri" => "https://linecorp.com"
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]
],

バブルメッセージ

レストラン

スクリーンショット 2020-09-28 2.16.57.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "bubble",
        "hero" => [
            "type" => "image",
            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png",
            "size" => "full",
            "aspectRatio" => "20:13",
            "aspectMode" => "cover",
            "action" => [
                "type" => "uri",
                "uri" => "http:/linecorp.com/",
            ]
        ],
        "body" => [
            "type" => "box",
            "layout" => "vertical",
            "contents" => [
                [
                    "type" => "text",
                    "text" => "Brown Cafe",
                    "align" => "center",
                    "weight" => "bold",
                    "size" => "xl",
                ],
                [
                    "type" => "box",
                    "layout" => "baseline",
                    "margin" => "md",
                    "contents" => [
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png",
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png",
                        ],
                        [
                            "type" => "text",
                            "text" => "4.0",
                            "size" => "sm",
                            "color" => "#999999",
                            "margin" => "md",
                            "flex" => 0
                        ]
                    ]
                ],
                [
                    "type" => "box",
                    "layout" => "vertical",
                    "margin" => "lg",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "spacing" => "sm",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Place",
                                    "color" => "#aaaaaa",
                                    "size" => "sm",
                                    "flex" => 1,
                                ],
                                [
                                    "type" => "text",
                                    "text" => "Miraina tower",
                                    "wrap" => true,
                                    "color" => "#666666",
                                    "size" => "sm",
                                    "flex" => 5,
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "spacing" => "sm",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Time",
                                    "color" => "#aaaaaa",
                                    "size" => "sm",
                                    "flex" => 1,
                                ],
                                [
                                    "type" => "text",
                                    "text" => "10:00 ~ 23:00",
                                    "wrap" => true,
                                    "color" => "#666666",
                                    "size" => "sm",
                                    "flex" => 5,
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ],
        "footer" => [
            "type" => "box",
            "layout" => "vertical",
            "spacing" => "sm",
            "contents" => [
                [
                    "type" => "button",
                    "style" => "link",
                    "height" => "sm",
                    "action" => [
                        "type" => "uri",
                        "label" => "CALL",
                        "uri" => "https://linecorp.com",
                    ]
                ],
                [
                    "type" => "button",
                    "style" => "link",
                    "height" => "sm",
                    "action" => [
                        "type" => "uri",
                        "label" => "WEBSITE",
                        "uri" => "https://linecorp.com",
                    ]
                ],
                [
                    "type" => "spacer",
                    "size" => "sm",
                ]
            ],
            "flex" => 0
        ]
    ]
],

レシート

スクリーンショット 2020-09-28 2.18.16.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "bubble",
        "body" => [
            "type" => "box",
            "layout" => "vertical",
            "contents" => [
                [
                    "type" => "text",
                    "text" => "RECEIPT",
                    "weight" => "bold",
                    "color" => "#1DB446",
                    "size" => "sm"
                ],
                [
                    "type" => "text",
                    "text" => "Brown Store",
                    "weight" => "bold",
                    "size" => "xxl",
                    "margin" => "md"
                ],
                [
                    "type" => "text",
                    "text" => "Miraina Tower, 4-1-6 Shinjuku, Tokyo",
                    "size" => "xs",
                    "color" => "#aaaaaa",
                    "wrap" => true
                ],
                [
                    "type" => "separator",
                    "margin" => "xxl"
                ],
                [
                    "type" => "box",
                    "layout" => "vertical",
                    "margin" => "xxl",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Energy Drink",
                                    "size" => "sm",
                                    "color" => "#555555",
                                    "flex" => 0
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$2.99",
                                    "size" => "sm",
                                    "color" => "#111111",
                                    "align" => "end"
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Chewing Gum",
                                    "size" => "sm",
                                    "color" => "#555555",
                                    "flex" => 0
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$0.99",
                                    "size" => "sm",
                                    "color" => "#111111",
                                    "align" => "end"
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Bottled Water",
                                    "size" => "sm",
                                    "color" => "#555555",
                                    "flex" => 0
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$3.33",
                                    "size" => "sm",
                                    "color" => "#111111",
                                    "align" => "end"
                                ]
                            ]
                        ],
                        [
                            "type" => "separator",
                            "margin" => "xxl"
                        ],
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "margin" => "xxl",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "ITEMS",
                                    "size" => "sm",
                                    "color" => "#555555"
                                ],
                                [
                                    "type" => "text",
                                    "text" => "3",
                                    "size" => "sm",
                                    "color" => "#111111",
                                    "align" => "end"
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "TOTAL",
                                    "size" => "sm",
                                    "color" => "#555555"
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$7.31",
                                    "size" => "sm",
                                    "color" => "#111111",
                                    "align" => "end"
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "CASH",
                                    "size" => "sm",
                                    "color" => "#555555"
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$8.0",
                                    "size" => "sm",
                                    "color" => "#111111",
                                    "align" => "end"
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "horizontal",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "CHANGE",
                                    "size" => "sm",
                                    "color" => "#555555"
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$0.69",
                                    "size" => "sm",
                                    "color" => "#111111",
                                    "align" => "end"
                                ]
                            ]
                        ]
                    ]
                ],
                [
                    "type" => "separator",
                    "margin" => "xxl"
                ],
                [
                    "type" => "box",
                    "layout" => "horizontal",
                    "margin" => "md",
                    "contents" => [
                        [
                            "type" => "text",
                            "text" => "PAYMENT ID",
                            "size" => "xs",
                            "color" => "#aaaaaa",
                            "flex" => 0
                        ],
                        [
                            "type" => "text",
                            "text" => "#743289384279",
                            "color" => "#aaaaaa",
                            "size" => "xs",
                            "align" => "end"
                        ]
                    ]
                ]
            ]
        ],
        "styles" => [
            "footer" => [
                "separator" => true
            ]
        ]
    ]
],

QRつき映画チケット

スクリーンショット 2020-09-28 2.07.40.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "bubble",
        "hero" => [
            "type" => "image",
            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_3_movie.png",
            "size" => "full",
            "aspectRatio" => "20:13",
            "aspectMode" => "cover",
            "action" => [
                "type" => "uri",
                "uri" => "http://linecorp.com/"
            ]
        ],
        "body" => [
            "type" => "box",
            "layout" => "vertical",
            "spacing" => "md",
            "contents" => [
                [
                    "type" => "text",
                    "text" => "BROWNS ADVENTURE\nIN MOVIE",
                    "wrap" => true,
                    "weight" => "bold",
                    "gravity" => "center",
                    "size" => "xl"
                ],
                [
                    "type" => "box",
                    "layout" => "baseline",
                    "margin" => "md",
                    "contents" => [
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                        ],
                        [
                            "type" => "icon",
                            "size" => "sm",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
                        ],
                        [
                            "type" => "text",
                            "text" => "4.0",
                            "size" => "sm",
                            "color" => "#999999",
                            "margin" => "md",
                            "flex" => 0
                        ]
                    ]
                ],
                [
                    "type" => "box",
                    "layout" => "vertical",
                    "margin" => "lg",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "spacing" => "sm",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Date",
                                    "color" => "#aaaaaa",
                                    "size" => "sm",
                                    "flex" => 1
                                ],
                                [
                                    "type" => "text",
                                    "text" => "Monday 25, 9:00PM",
                                    "wrap" => true,
                                    "size" => "sm",
                                    "color" => "#666666",
                                    "flex" => 4
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "spacing" => "sm",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Place",
                                    "color" => "#aaaaaa",
                                    "size" => "sm",
                                    "flex" => 1
                                ],
                                [
                                    "type" => "text",
                                    "text" => "7 Floor, No.3",
                                    "wrap" => true,
                                    "color" => "#666666",
                                    "size" => "sm",
                                    "flex" => 4
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "spacing" => "sm",
                            "contents" => [
                                [
                                    "type" => "text",
                                    "text" => "Seats",
                                    "color" => "#aaaaaa",
                                    "size" => "sm",
                                    "flex" => 1
                                ],
                                [
                                    "type" => "text",
                                    "text" => "C Row, 18 Seat",
                                    "wrap" => true,
                                    "color" => "#666666",
                                    "size" => "sm",
                                    "flex" => 4
                                ]
                            ]
                        ]
                    ]
                ],
                [
                    "type" => "box",
                    "layout" => "vertical",
                    "margin" => "xxl",
                    "contents" => [
                        [
                            "type" => "spacer"
                        ],
                        [
                            "type" => "image",
                            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/linecorp_code_withborder.png",
                            "aspectMode" => "cover",
                            "size" => "xl"
                        ],
                        [
                            "type" => "text",
                            "text" => "You can enter the theater by using this code instead of a ticket",
                            "color" => "#aaaaaa",
                            "wrap" => true,
                            "margin" => "xxl",
                            "size" => "xs"
                        ]
                    ]
                ]
            ]
        ]
    ]
],

乗り換え案内

スクリーンショット 2020-09-28 2.06.20.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        [
            "type" => "bubble",
            "size" => "mega",
            "header" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "FROM",
                                "color" => "#ffffff66",
                                "size" => "sm"
                            ],
                            [
                                "type" => "text",
                                "text" => "Akihabara",
                                "color" => "#ffffff",
                                "size" => "xl",
                                "flex" => 4,
                                "weight" => "bold"
                            ]
                        ]
                    ],
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "TO",
                                "color" => "#ffffff66",
                                "size" => "sm"
                            ],
                            [
                                "type" => "text",
                                "text" => "Shinjuku",
                                "color" => "#ffffff",
                                "size" => "xl",
                                "flex" => 4,
                                "weight" => "bold"
                            ]
                        ]
                    ]
                ],
                "paddingAll" => "20px",
                "backgroundColor" => "#0367D3",
                "spacing" => "md",
                "height" => "154px",
                "paddingTop" => "22px"
            ],
            "body" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "text",
                        "text" => "Total => 1 hour",
                        "color" => "#b7b7b7",
                        "size" => "xs"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "20:30",
                                "size" => "sm",
                                "gravity" => "center"
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "vertical",
                                        "contents" => [],
                                        "cornerRadius" => "30px",
                                        "height" => "12px",
                                        "width" => "12px",
                                        "borderColor" => "#EF454D",
                                        "borderWidth" => "2px"
                                    ],
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 0
                            ],
                            [
                                "type" => "text",
                                "text" => "Akihabara",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "sm"
                            ]
                        ],
                        "spacing" => "lg",
                        "cornerRadius" => "30px",
                        "margin" => "xl"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 1
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "box",
                                        "layout" => "horizontal",
                                        "contents" => [
                                            [
                                                "type" => "filler"
                                            ],
                                            [
                                                "type" => "box",
                                                "layout" => "vertical",
                                                "contents" => [],
                                                "width" => "2px",
                                                "backgroundColor" => "#B7B7B7"
                                            ],
                                            [
                                                "type" => "filler"
                                            ]
                                        ],
                                        "flex" => 1
                                    ]
                                ],
                                "width" => "12px"
                            ],
                            [
                                "type" => "text",
                                "text" => "Walk 4min",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "xs",
                                "color" => "#8c8c8c"
                            ]
                        ],
                        "spacing" => "lg",
                        "height" => "64px"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "horizontal",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "20:34",
                                        "gravity" => "center",
                                        "size" => "sm"
                                    ]
                                ],
                                "flex" => 1
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "vertical",
                                        "contents" => [],
                                        "cornerRadius" => "30px",
                                        "width" => "12px",
                                        "height" => "12px",
                                        "borderWidth" => "2px",
                                        "borderColor" => "#6486E3"
                                    ],
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 0
                            ],
                            [
                                "type" => "text",
                                "text" => "Ochanomizu",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "sm"
                            ]
                        ],
                        "spacing" => "lg",
                        "cornerRadius" => "30px"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 1
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "box",
                                        "layout" => "horizontal",
                                        "contents" => [
                                            [
                                                "type" => "filler"
                                            ],
                                            [
                                                "type" => "box",
                                                "layout" => "vertical",
                                                "contents" => [],
                                                "width" => "2px",
                                                "backgroundColor" => "#6486E3"
                                            ],
                                            [
                                                "type" => "filler"
                                            ]
                                        ],
                                        "flex" => 1
                                    ]
                                ],
                                "width" => "12px"
                            ],
                            [
                                "type" => "text",
                                "text" => "Metro 1hr",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "xs",
                                "color" => "#8c8c8c"
                            ]
                        ],
                        "spacing" => "lg",
                        "height" => "64px"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "20:40",
                                "gravity" => "center",
                                "size" => "sm"
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "vertical",
                                        "contents" => [],
                                        "cornerRadius" => "30px",
                                        "width" => "12px",
                                        "height" => "12px",
                                        "borderColor" => "#6486E3",
                                        "borderWidth" => "2px"
                                    ],
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 0
                            ],
                            [
                                "type" => "text",
                                "text" => "Shinjuku",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "sm"
                            ]
                        ],
                        "spacing" => "lg",
                        "cornerRadius" => "30px"
                    ]
                ]
            ]
        ]
    ]
]

メニュー

スクリーンショット 2020-09-28 2.15.09.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        "type" => "bubble",
        "hero" => [
            "type" => "image",
            "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_2_restaurant.png",
            "size" => "full",
            "aspectRatio" => "20:13",
            "aspectMode" => "cover",
            "action" => [
                "type" => "uri",
                "uri" => "https://linecorp.com"
            ]
        ],
        "body" => [
            "type" => "box",
            "layout" => "vertical",
            "spacing" => "md",
            "action" => [
                "type" => "uri",
                "uri" => "https://linecorp.com"
            ],
            "contents" => [
                [
                    "type" => "text",
                    "text" => "Browns Burger",
                    "size" => "xl",
                    "weight" => "bold"
                ],
                [
                    "type" => "box",
                    "layout" => "vertical",
                    "spacing" => "sm",
                    "contents" => [
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "icon",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/restaurant_regular_32.png"
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$10.5",
                                    "weight" => "bold",
                                    "margin" => "sm",
                                    "flex" => 0
                                ],
                                [
                                    "type" => "text",
                                    "text" => "400kcl",
                                    "size" => "sm",
                                    "align" => "end",
                                    "color" => "#aaaaaa"
                                ]
                            ]
                        ],
                        [
                            "type" => "box",
                            "layout" => "baseline",
                            "contents" => [
                                [
                                    "type" => "icon",
                                    "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/restaurant_large_32.png"
                                ],
                                [
                                    "type" => "text",
                                    "text" => "$15.5",
                                    "weight" => "bold",
                                    "margin" => "sm",
                                    "flex" => 0
                                ],
                                [
                                    "type" => "text",
                                    "text" => "550kcl",
                                    "size" => "sm",
                                    "align" => "end",
                                    "color" => "#aaaaaa"
                                ]
                            ]
                        ]
                    ]
                ],
                [
                    "type" => "text",
                    "text" => "Sauce, Onions, Pickles, Lettuce & Cheese",
                    "wrap" => true,
                    "color" => "#aaaaaa",
                    "size" => "xxs"
                ]
            ]
        ],
        "footer" => [
            "type" => "box",
            "layout" => "vertical",
            "contents" => [
                [
                    "type" => "spacer",
                    "size" => "xxl"
                ],
                [
                    "type" => "button",
                    "style" => "primary",
                    "color" => "#905c44",
                    "action" => [
                        "type" => "uri",
                        "label" => "Add to Cart",
                        "uri" => "https://linecorp.com"
                    ]
                ]
            ]
        ]
    ]
],

ホテル

スクリーンショット 2020-09-28 2.16.26.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        [
            "type" => "bubble",
            "body" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "image",
                        "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip3.jpg",
                        "size" => "full",
                        "aspectMode" => "cover",
                        "aspectRatio" => "1:1",
                        "gravity" => "center"
                    ],
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "contents" => [],
                        "position" => "absolute",
                        "background" => [
                            "type" => "linearGradient",
                            "angle" => "0deg",
                            "endColor" => "#00000000",
                            "startColor" => "#00000099"
                        ],
                        "width" => "100%",
                        "height" => "40%",
                        "offsetBottom" => "0px",
                        "offsetStart" => "0px",
                        "offsetEnd" => "0px"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "box",
                                        "layout" => "horizontal",
                                        "contents" => [
                                            [
                                                "type" => "text",
                                                "text" => "Brown Grand Hotel",
                                                "size" => "xl",
                                                "color" => "#ffffff"
                                            ]
                                        ]
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "baseline",
                                        "contents" => [
                                            [
                                                "type" => "icon",
                                                "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                                            ],
                                            [
                                                "type" => "icon",
                                                "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                                            ],
                                            [
                                                "type" => "icon",
                                                "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                                            ],
                                            [
                                                "type" => "icon",
                                                "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
                                            ],
                                            [
                                                "type" => "icon",
                                                "url" => "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
                                            ],
                                            [
                                                "type" => "text",
                                                "text" => "4.0",
                                                "color" => "#a9a9a9"
                                            ]
                                        ],
                                        "spacing" => "xs"
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "horizontal",
                                        "contents" => [
                                            [
                                                "type" => "box",
                                                "layout" => "baseline",
                                                "contents" => [
                                                    [
                                                        "type" => "text",
                                                        "text" => "¥62,000",
                                                        "color" => "#ffffff",
                                                        "size" => "md",
                                                        "flex" => 0,
                                                        "align" => "end"
                                                    ],
                                                    [
                                                        "type" => "text",
                                                        "text" => "¥82,000",
                                                        "color" => "#a9a9a9",
                                                        "decoration" => "line-through",
                                                        "size" => "sm",
                                                        "align" => "end"
                                                    ]
                                                ],
                                                "flex" => 0,
                                                "spacing" => "lg"
                                            ]
                                        ]
                                    ]
                                ],
                                "spacing" => "xs"
                            ]
                        ],
                        "position" => "absolute",
                        "offsetBottom" => "0px",
                        "offsetStart" => "0px",
                        "offsetEnd" => "0px",
                        "paddingAll" => "20px"
                    ]
                ],
                "paddingAll" => "0px"
            ]
        ]
    ]
]

不動産

スクリーンショット 2020-09-28 5.08.25.png

example.php
[
    "type" => "flex",
    "altText" => "This is a flex message",
    "contents" => [
        [
            "type" => "bubble",
            "size" => "mega",
            "header" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "FROM",
                                "color" => "#ffffff66",
                                "size" => "sm"
                            ],
                            [
                                "type" => "text",
                                "text" => "Akihabara",
                                "color" => "#ffffff",
                                "size" => "xl",
                                "flex" => 4,
                                "weight" => "bold"
                            ]
                        ]
                    ],
                    [
                        "type" => "box",
                        "layout" => "vertical",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "TO",
                                "color" => "#ffffff66",
                                "size" => "sm"
                            ],
                            [
                                "type" => "text",
                                "text" => "Shinjuku",
                                "color" => "#ffffff",
                                "size" => "xl",
                                "flex" => 4,
                                "weight" => "bold"
                            ]
                        ]
                    ]
                ],
                "paddingAll" => "20px",
                "backgroundColor" => "#0367D3",
                "spacing" => "md",
                "height" => "154px",
                "paddingTop" => "22px"
            ],
            "body" => [
                "type" => "box",
                "layout" => "vertical",
                "contents" => [
                    [
                        "type" => "text",
                        "text" => "Total => 1 hour",
                        "color" => "#b7b7b7",
                        "size" => "xs"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "20:30",
                                "size" => "sm",
                                "gravity" => "center"
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "vertical",
                                        "contents" => [],
                                        "cornerRadius" => "30px",
                                        "height" => "12px",
                                        "width" => "12px",
                                        "borderColor" => "#EF454D",
                                        "borderWidth" => "2px"
                                    ],
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 0
                            ],
                            [
                                "type" => "text",
                                "text" => "Akihabara",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "sm"
                            ]
                        ],
                        "spacing" => "lg",
                        "cornerRadius" => "30px",
                        "margin" => "xl"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 1
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "box",
                                        "layout" => "horizontal",
                                        "contents" => [
                                            [
                                                "type" => "filler"
                                            ],
                                            [
                                                "type" => "box",
                                                "layout" => "vertical",
                                                "contents" => [],
                                                "width" => "2px",
                                                "backgroundColor" => "#B7B7B7"
                                            ],
                                            [
                                                "type" => "filler"
                                            ]
                                        ],
                                        "flex" => 1
                                    ]
                                ],
                                "width" => "12px"
                            ],
                            [
                                "type" => "text",
                                "text" => "Walk 4min",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "xs",
                                "color" => "#8c8c8c"
                            ]
                        ],
                        "spacing" => "lg",
                        "height" => "64px"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "horizontal",
                                "contents" => [
                                    [
                                        "type" => "text",
                                        "text" => "20:34",
                                        "gravity" => "center",
                                        "size" => "sm"
                                    ]
                                ],
                                "flex" => 1
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "vertical",
                                        "contents" => [],
                                        "cornerRadius" => "30px",
                                        "width" => "12px",
                                        "height" => "12px",
                                        "borderWidth" => "2px",
                                        "borderColor" => "#6486E3"
                                    ],
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 0
                            ],
                            [
                                "type" => "text",
                                "text" => "Ochanomizu",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "sm"
                            ]
                        ],
                        "spacing" => "lg",
                        "cornerRadius" => "30px"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "box",
                                "layout" => "baseline",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 1
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "box",
                                        "layout" => "horizontal",
                                        "contents" => [
                                            [
                                                "type" => "filler"
                                            ],
                                            [
                                                "type" => "box",
                                                "layout" => "vertical",
                                                "contents" => [],
                                                "width" => "2px",
                                                "backgroundColor" => "#6486E3"
                                            ],
                                            [
                                                "type" => "filler"
                                            ]
                                        ],
                                        "flex" => 1
                                    ]
                                ],
                                "width" => "12px"
                            ],
                            [
                                "type" => "text",
                                "text" => "Metro 1hr",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "xs",
                                "color" => "#8c8c8c"
                            ]
                        ],
                        "spacing" => "lg",
                        "height" => "64px"
                    ],
                    [
                        "type" => "box",
                        "layout" => "horizontal",
                        "contents" => [
                            [
                                "type" => "text",
                                "text" => "20:40",
                                "gravity" => "center",
                                "size" => "sm"
                            ],
                            [
                                "type" => "box",
                                "layout" => "vertical",
                                "contents" => [
                                    [
                                        "type" => "filler"
                                    ],
                                    [
                                        "type" => "box",
                                        "layout" => "vertical",
                                        "contents" => [],
                                        "cornerRadius" => "30px",
                                        "width" => "12px",
                                        "height" => "12px",
                                        "borderColor" => "#6486E3",
                                        "borderWidth" => "2px"
                                    ],
                                    [
                                        "type" => "filler"
                                    ]
                                ],
                                "flex" => 0
                            ],
                            [
                                "type" => "text",
                                "text" => "Shinjuku",
                                "gravity" => "center",
                                "flex" => 4,
                                "size" => "sm"
                            ]
                        ],
                        "spacing" => "lg",
                        "cornerRadius" => "30px"
                    ]
                ]
            ]
        ]
    ]
]

注意

メッセージは一度に5件しか送れないのでテストで送信するときは気をつけてください

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

Laravelディレクトリ構成+DBマイグレーション

今回はLaravelディレクトリ構成+DBマイグレーションを説明していきます。

ディレクトリ構成

image.png

ディレクトリ名 役割
app アプリのメインとなるところ
bootstrap 初期処理やキャッシュなど
config アプリの設定
database データベース(マイグレーション)
public 画像、JS、CSSなど
resources bladeなど(HTML)
routes アプリのURL設定
storage セッションやログ
tests テスト用
vendor Comoposerの依存内容

マイグレーション

マイグレーションとは

SQLを直接使わなくても、データベースを管理できるLaravelの仕組みです。

マイグレーションファイル

マイグレーションファイルは「どんなテーブルを作るか」というテーブルの設計書です。設計書を作ったらそれを実行しなければテーブルはできないので、この実行にあたるのがマイグレーションです。

テーブル作成の流れ

1. マイグレーションファイルを生成
2. ファイルにテーブル定義を書く
3. マイグレーションファイルを実行して、DBに反映

ファイルの場所とファイル名ルール

image.png

ファイル生成コマンド

 $ php artsan make:migration create_users_table 

(例)カラム追加ver

$ add_カラム名_to_テーブル名_table

実行コマンド

$ php artisan migrate

マイグレーションの中身

・upとdownメソッド
 upが実行(作成)で、downが元に戻す(削除)
・Schemaファサードを使っている
 例) Schema::create テーブルを作成
・カラムの作成はBlueprintオブジェクトのtableメソッドを使う
 例) $table->string('name')名前カラムを作成

   public function up()//作成
    {
        if(!Schema::hasTable('blogs')) {//もしblogsというtableがなかったら
            Schema::create('blogs', function (Blueprint $table) {
                $table->id();//idのカラム
                $table->string('title',100);//titleというカラムをstring型で100文字の制限をつけた状態で作る
                $table->text('content');//contentというカラムをtext型で作る
                $table->timestamps();
            });
        }

    }

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

追加したいカラムなどを書き終えたらmigrateしていきます。

$ php artisan migrate

終わりに

参考サイトを貼らせて頂きます。

公式ドキュメント
https://readouble.com/laravel/7.x/ja/migrations.html

参考サイト
https://www.youtube.com/watch?v=wkW1spp_LO8

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

AWSとPHPでTwitterOAuthでログイン機能を実装

TWitterOAuthでログイン機能実装が思ったより苦戦したので、エラーになったポイントを伝えていきます。
AWS上でやっていきます。

AWSでComposerをインストール

①下記手順を実施してPHPを入れる。(実施済みの方は飛ばす。)
AWS EC2 AmazonLinux2 PHPをインストールする

②公式のインストール方法に記載されているコマンドを実行してcomposer本体を取得する。
コマンドは変更になる可能性があるので最新のコマンドは公式ページをご確認してください。

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '795f976fe0ebd8b75f26a6dd68f78fd3453ce79f32ecb33e7fd087d39bfeb978342fb73ac986cd4f54edd0dc902601dc') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

③下記コマンドを実行してcomposerの実行ファイルを移動する。

$ sudo mv composer.phar /usr/local/bin/composer

④下記コマンドを実行してcomposerの実行ファイルを実行する。

$ php /usr/local/bin/composer

⑤下記コマンドを実行してパスを通す。(ユーザ名にはssh接続しているEC2インスタンスのユーザ名を入力する。Apacheのディレクトリルートで実行することを考えフルパスでパスを通す。)

$ echo "export PATH=/home/ec2-user/.config/composer/vendor/bin:$PATH" >> ~/.bash_profile
$ source ~/.bash_profile

Twitteroauthをインストール

⑥公式Twitteroauthサイトで書いてある通りに実行する。

composer require abraham/twitteroauth

⑦vendorのフォルダの中に実行ファイルのautoload.phpがある為、htmlフォルダに移動。

mv vendor /var/www/html/

Twitter OAuth認証のPHP実装

⑧動作環境
PHP Version 5.3以上であること(phpinfo();関数で確認)
cURL support enabledであること(phpinfo();関数で確認)
デフォルトで多分、大丈夫です。

⑨Callback URLは忘れずに
Twitterアプリケーションの「Settings」→「Application Details」→「Callback URLs」には、「callback.php」のURLを指定します。以下のサンプルコードでは、「'http://' . $_SERVER['HTTP_HOST'] . 'callback.php」が「Callback URLs」に指定されています。

⑩config.php

<?php
//アプリケーションの Consumer Key と Consumer Secret
$sTwitterConsumerKey = '***********************************'; //Consumer Key (API Key)
$sTwitterConsumerSecret = '***********************************'; //Consumer Secret (API Secret)

//アプリケーションのコールバックURL
$sTwitterCallBackUri = 'http://' . $_SERVER['HTTP_HOST'] . '/callback.php'; //コールバックURL

//変数初期化
$objTwitterConection = NULL; //TwitterOAuthクラスのインスタンス化
$aTwitterRequestToken = array(); //リクエストトークン
$sTwitterRequestUrl = ''; //認証用URL
$objTwitterAccessToken = NULL; //アクセストークン
$objTwUserInfo = NULL; //ユーザー情報
?>

⑪login.php

<?php
##############################################
### 初期設定
//エラー詳細
ini_set('display_errors', 1);

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8");

//インクルード
require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/vendor/autoload.php');

//インポート
use Abraham\TwitterOAuth\TwitterOAuth;

##############################################
### twitter oauth request token 取得

//TwitterOAuthクラスをインスタンス化
$objTwitterConection = new TwitterOAuth($sTwitterConsumerKey, $sTwitterConsumerSecret);

//oauthリクエストトークンの取得
$aTwitterRequestToken = $objTwitterConection->oauth('oauth/request_token', array('oauth_callback' => $sTwitterCallBackUri));

//oauthリクエストトークンをセッションに格納
$_SESSION['twOauthToken'] = $aTwitterRequestToken['oauth_token'];
$_SESSION['twOauthTokenSecret'] = $aTwitterRequestToken['oauth_token_secret'];

##############################################
### twitter 認証へ

//Twitter認証URLの作成
$sTwitterRequestUrl = $objTwitterConection->url('oauth/authenticate', array('oauth_token' => $_SESSION['twOauthToken']));

//Twitter認証画面へリダイレクト
header('location: '.$sTwitterRequestUrl);
?>

⑫logout.php

<?php
##############################################
### 初期設定

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8");

//セッション変数を全て解除
$_SESSION = array();
$_COOKIE = array();

//クッキー削除
setcookie("PHPSESSID", '', time() - 1800, '/');

//セッションを破棄する
session_destroy();
?>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タイトル</title>
<meta http-equiv="Content-Style-Type" content="text/css">
</head>

<h2>Twitter アカウント ログアウト</h2>

<?php
echo "ログアウトしました。"; 
echo "<a href='https://wepicks.net/code-example/twitter-restapi/login/login.php'>ログインへ</a>";
?>

</body>
</html>

⑬member.php

<?php
##############################################
### 初期設定

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8"); 

//インクルード
require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/vendor/autoload.php');

//インポート
use Abraham\TwitterOAuth\TwitterOAuth;

##############################################
### アクセストークン確認
if(empty($_SESSION['twAccessToken'])){
 echo 'error access token!!';
 exit;
}

##############################################
### ユーザー情報の取得

//TwitterOAuthクラスをインスタンス化
$objTwitterConection = new TwitterOAuth
 (
 $sTwitterConsumerKey,
 $sTwitterConsumerSecret,
 $_SESSION['twAccessToken']['oauth_token'],
 $_SESSION['twAccessToken']['oauth_token_secret']
 );

//ユーザー情報を取得
$objTwUserInfo = $objTwitterConection->get("account/verify_credentials");
?>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タイトル</title>
<meta http-equiv="Content-Style-Type" content="text/css">
</head>

<h2>Twitter アカウント ログイン完了!</h2>

<?php echo $_SERVER['REQUEST_URI']; ?><br/>
<a href="logout.php">ログアウト</a>

<pre>
<?php var_dump($_SESSION['twAccessToken']); ?>
</pre>

<pre>
<?php var_dump($objTwUserInfo); ?>
</pre>

</body>
</html>

⑭callback.php

<?php
##############################################
### 初期設定

//セッションスタート
session_start();

//文字セット
header("Content-type: text/html; charset=utf-8"); 

//インクルード
require_once(__DIR__ . '/config.php');
require_once(__DIR__ . '/vendor/autoload.php');

//インポート
use Abraham\TwitterOAuth\TwitterOAuth;

##############################################
### oauthトークン確認
if(empty($_SESSION['twOauthToken']) || empty($_SESSION['twOauthTokenSecret']) || empty($_REQUEST['oauth_token']) || empty($_REQUEST['oauth_verifier'])){
 echo 'error token!!';
 exit;
}
if($_SESSION['twOauthToken'] !== $_REQUEST['oauth_token']) {
 echo 'error token incorrect!!';
 exit;
}

##############################################
### アクセストークン作成

//取得したoauthトークンでTwitterOAuthクラスをインスタンス化
$objTwitterConection = new TwitterOAuth
 (
 $sTwitterConsumerKey,
 $sTwitterConsumerSecret,
 $_SESSION['twOauthToken'],
 $_SESSION['twOauthTokenSecret']
 );

//アクセストークンの取得
$_SESSION['twAccessToken'] = $objTwitterConection->oauth("oauth/access_token", array("oauth_verifier" => $_REQUEST['oauth_verifier']));

//メンバーページへリダイレクト
header('location: member.php');

⑮確認
完成したのか、確認をします。
login.phpにアクセスしてみます。
こんなになってたら、完成です。
api.png

参照させていただきました。
PHP で Twitter API OAuth 認証 「ログイン」
twitteroauthのインストール方法
AWS EC2 AmazonLinux2 composerをインストールする

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

OSS PHP SDKを使ってSwooleで機能を掘り下げる

SwooleはPHPの非同期・並列リクエストのための高性能なネットワーク通信エンジンです。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

image.png

序章

Swooleは、最近ではコーダーの世界で新たな "最高の言語 "として定着しています。

公式に説明されているように、PHPの非同期・並列リクエストのための高性能なネットワーク通信エンジンです。そのコンパイルは、PHPの非同期マルチスレッドサーバ、非同期TCP/UDPネットワーククライアント、非同期MySQLを提供するC言語を純粋に利用しています。さらに、非同期Redis、データベース接続プール、AsyncTask、メッセージキュー、ミリ秒タイマー、非同期ファイルの読み書き、非同期DNSクエリ機能を使用しています。HTTP/WebSocket サーバ/クライアント、HTTP 2.0 サーバと入れ子になっています。

Swoole 2.0はGo言語などのコアーチンをサポートしており、完全に同期化されたコードを使用して非同期プログラムを実装することができます。PHPのコードは、追加のキーワードを必要としません。さらに、ボトムレイヤーは、非同期処理を実装するためのコルーチンを自動的にスケジュールします。この記事では、OSS PHP SDKを使用してAlibaba Cloud上でSwooleを使用する方法を発見します。

Swooleのメリット

その利点を見てみましょう。

  • 純粋にC言語でコンパイルされており、非常に強力なパフォーマンスを発揮。
  • シンプルで使いやすく、開発効率が高い。
  • イベントドリブンでノンブロッキングの非同期処理。
  • 数百万件の同時TCP接続に対応。
  • TCP/UDP/UnixSockに対応。
  • サーバ/クライアント。
  • 非同期/同期/コルーチンに対応。
  • マルチプロセッシング/マルチスレッド対応。
  • CPUアフィニティ/デーモンプロセス対応。
  • IPv4/IPv6ネットワーク対応。

アプリケーションシナリオ

Swooleをより深く理解するために、Swooleのユースケースを見てみましょう。

現在、プログラム開発者は、C++やJavaなどの複雑なプログラミング言語の代わりに、Swooleを利用してネットワークサーバプログラムを実装しています。いくつかのモバイルインターネット、モノのインターネット、オンラインゲーム、モバイルゲームなどで利用されています。PHPとSwooleを組み合わせることで、開発効率を大幅に向上させることができます。

公式に提供されているPHPネットワークフレームワークは、HTTP、FastCGI、WebSocket、FTP、SMTP、RPCなどのネットワークプロトコルをサポートしており、Swooleをベースに拡張・開発されています。

Swooleのユーザーはどこにでもいて、アメリカ、イギリス、フランス、インド、その他の国に広がっています。テンセント、百度、アリババ、YY言語など、中国の有名なインターネット企業もこの製品を使用しています。

ここでは、OSS PHP SDKを使ってSwooleの機能を掘り下げてみましょう。

1. Swooleを設置

最初のステップとして、Swooleをシステムにインストールする必要があります。PHP7とPECLがインストールされている場合は、以下のコマンドを直接使用することができます。
pecl install swoole

次に、php -info | grep php.ini を使用して特定のロードされた設定ファイルを探します。
環境によって異なるかもしれませんが、私たちの環境では /etc/php/7.0/cli/php.ini です。
次に、以下のように実行します。
vim /etc/php/7.0/cli/php.ini

次に、追加します。
拡張子 = "swoole.so"

あるいは、Swooleをコンパイルしてシステムにインストールしても良いでしょう。ただし、以下のスクリーンショットのようなテキストが表示されるまでは、これを行う必要があります。

image.png

2. OSS PHP SDKを使用

OSS PHP SDKでSwooleを使用するためには、PHP SDKのセットアップが成功し、準備ができていることを確認する必要があります。手順は以下の通りです。

  • git clone https://github.com/aliyun/aliyun-oss-php-sdk.git # OSS PHP SDKのコードアドレスからコードをダウンロードします。
  • cd aliyun-oss-php-sdk/を実行します。
  • 設定サンプル/Config.php #.
  • サンプルプログラムを実行します。
  • curl 127.0.0.1:9503を実行します。
  • 画面に「Hello Swoole」と表示されるはずです。

image.png

表示されたら、SwooleとOSS PHP SDKを一緒に使う準備ができました。

3. Nginxをリバースプロキシに設定

上記の手順を踏めば、Swoole経由でOSSにアクセスできるようになります。しかし、サーバー側では、フロントエンドプロキシとしてnginxを使用するのが一般的です。そして、nginxリバースプロキシサーバを経由して、起動したSwooleサーバにアクセスします。

具体的な手順は以下の通りです(すでにnginxをインストールしている場合は省略しても構いません)。

  • wget http://nginx.org/download/nginx-1.11.3.tar.gz から最新版をダウンロードします。
  • 次に、tar -zxvf ngin-1.11.3.tar.gzで解凍します。
  • 次に、cd nginx-1.11.3 のコマンドを入力します。
  • ./configure --prefix=/usr/local/nginx # で設定します。
  • このステップで「pcre.h No such file or directory」というエラーが発生する場合がありますのでご注意ください。
  • このエラーの詳細はこちらをご覧ください。
  • さらに、以下のコマンドで libpcre3-dev をインストールする必要があります。
  • sudo make install
  • sudo /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
  • 注意:「-c」は設定ファイルのパスを指定します。追加しないと、nginxは自動的にデフォルトのパスで設定ファイルを読み込みます。「h」でヘルプコマンドを見ることができます。
  • ps -ef | grep nginx
  • nginxの進捗状況はこちらで確認できます。

image.png

上記のスクリーンショットが画面に表示されていれば、nginxが正常に動作していることを示しています。

次に、nginxリバースプロキシ127.0.0.1:9503を使用して、/usr/local/nginx/conf/nginx.confを修正します。

以下のように実行します。

http {
    include       mime.types;
    default_type  application/octet-stream;
    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';
    #access_log  logs/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    #gzip  on;
        upstream swoole{
                server 127.0.0.1:9503;
                keepalive 4;
        }
    server {
        listen       80;
        server_name  www.swoole.com;
        #charset koi8-r;
        #access_log  logs/host.access.log  main;
        location / {
            proxy_pass http://swoole;
            proxy_set_header Connection "";
            proxy_http_version 1.1;
            root   html;
            index  index.html index.htm;
        }
        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
}
  • 次に、設定を有効にするために nginx をリロードする必要があります。

  • 次に、vim /etc/hosts に 127.0.0.1 www.swoole.com を追加します。

  • 結果を確認するには、curl www.swoole.com またはブラウザでドメイン名を開き、「Hello Swoole」が表示されているかどうかを確認します。

4. Swoole を使用した体験談

今までの手順を踏んでも「Hello Swoole」しか結果が出ない場合は、何かが足りないということになります。

先に進んで、何が足りないのかを確認してみましょう。./aliyun-oss-php-sdkディレクトリにsample.jpgファイルがありますので、参考にして使ってみてください。そのファイルをアップロードするためのサンプルコードは以下の通りです。

<?php
require_once __DIR__ . '/Common.php';

use OSS\OssClient;
use OSS\Core\OssException;

$bucket = Common::getBucketName();
$ossClient = Common::getOssClient();
if (is_null($ossClient)) exit(1);
//*******************************Simple use***************************************************************
$options = array(
    OssClient::OSS_FILE_DOWNLOAD => "example_download.jpg",
);

$ossClient->uploadFile($bucket, "example.jpg", "example.jpg");
$ossClient->getObject($bucket, "example.jpg", $options);

コマンドを実行した後に結果を確認する必要があります。

image.png

OSSで最も簡単なファイルのアップロード/ダウンロード方法は以下の通りです。

上記の手順で、Swooleサーバ内のOSS PHP SDKを呼び出します。そして、そのサーバーをnginxプロキシとして使用します。次に、最もシンプルなOSSのアップロード/ダウンロードのコードを以下のように変更します。:

<?php
require_once __DIR__ . '/Common.php';

use OSS\OssClient;
use OSS\Core\OssException;

$bucket = Common::getBucketName();
$ossClient = Common::getOssClient();
if (is_null($ossClient)) exit(1);
//*******************************Simple use***************************************************************

$options = array(
    OssClient::OSS_FILE_DOWNLOAD => "example_download.jpg",
);

$serv = new swoole_http_server("127.0.0.1", 9503);

$serv->set(array(
        'worker_num' => 16,
        'daemonize' => true,
        'max_request' => 10000,
        'dispatch_mode' => 2,
        'debug_mode'=> 1,
        'log_file' => '/tmp/swoole_http_server.log',
));

$serv->on('Request', function($request, $response) use($ossClient, $bucket, $options){

        $ossClient->uploadFile($bucket, "example.jpg", "example.jpg");
        $ossClient->getObject($bucket, "example.jpg", $options);

        $response->end("Hello Swoole\n");
});

$serv->start();
  • php sample/Swoole.php
  • 今、コマンドを実行する必要があります: curl www.swoole.com。

長時間応答がなく、nginxがエラーを報告していませんか?
nginxのアクセスログを確認すると504エラーが出ています。Swooleで設定したswoole_http_server.logを開くと以下のように表示されます。

image.png

遭遇する可能性のある問題は以下の2つです。

  • 問題1:エラーは、OSS PHP SDKがこのexample.jpgファイルを見つけるのに失敗したことを示しています。しかし、example.jpgはローカルファイルです。なぜ、OSS PHP SDKはそれを見つけることができないのでしょうか?
  • 問題2:このエラーが本当だとしても、なぜnginxが504エラーを報告するようになったのか?

さて、この2つの問題を解決するにはどうすればいいのか、疑問に思うかもしれません。

問題1は、Swooleのバグに起因する可能性があります。実際のテストでは、Swooleはカレントディレクトリではなく、ルートディレクトリの「/」から相対パスを計算しています。そうすると、ファイルの解決パスを書くことができます。

問題2については、Swooleのユーザーは、呼び出されたインターフェースによって投げられたエラーをカプセル化しなければなりません。つまり、この記事の場合のOSS PHP SDKが投げたエラーは、nginxに正しく識別させるためのものです。

結論

Swoole のイベントベースのネットワーク層は、基礎となる epoll/kqueue の実装を利用して、数千の接続に対応します。リクエストの後、割り当てられたメモリは、レガシーのapache/php-fpmのように自分自身を解放しないため、パフォーマンスが大幅に向上します。Swoole を使用すると、より多くの制御が可能な強化されたウェブアプリケーションや、リアルタイムチャットサーバなどを作成することができます。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

Laravel8.0入門してみた感想

ララベル.かわいいなまえだね.
でも「Web職人のためのフレームワーク」みたいなキャッチコピーでかっこいいね.

ちゅき(._.)

というわけで入門した感想を述べます.

感想1. 環境の構築 → めちゃ簡単

僕はこれまでRuby on RailsとReactに触れたことがあります.
使ってるOSはWindows10.
するとどうなるか.

環境構築で死す

コマンドプロンプトにerrorの嵐

僕はもう疲れました.
こんなことならフレームワークなんか使わない方が早いや.
どうせザコだし僕.

そう思ってたある日,なんとなくPHPを使い始めて,それとなくLaravelにたどり着きました.
「どうせまた環境構築で死すんだろうな...」
半ばあきらめながらネットに落ちている記事を見ながらXAMPPやらLaravelやらを入れました.

...は?

一度もエラーせず構築できた.

こんなことがあるんですね.
もうこの時点で僕のハートはLaravel様の者でした.

「Windows想いなLaravel,好きです.」
僕はそう呟いて,そっと眠りについた.(え?)

感想2. ネット記事より書籍を読め

「プログラミングの勉強に金はかけない」
そんな当たり前の常識の元,Laravel入門初日の僕はネット記事で学習をしていました.

なんかよくわからん

そもそもルートとかわからん.
データベースとか触ったことない.

うん,わからん.

公式ドキュメントもよくわからん.

この時点で僕は,少しだけLaravelに嫌気がさしていました.
だっていい記事全然ないんだもん.

すると,
メール「Kindle Unlimitedの定期購読,更新したで!」
僕「あ,Laravelの書籍あるんでね??」

ありました.
世間で「青本」と呼ばれているよさげなLaravelの本は,Kindle Unlimitedでは読めなかったけど,別のがありました.
でも全然わかる.なんか道筋めちゃ見える.テンション爆上げ.

Laravelめちゃええやん

環境構築で死してばかりの僕がフレームワークを扱っている.
その喜びを握られてまた,僕はLaravelに恋をしたのである.

感想3.何をすればいいかが分かりやすい

そろそろLaravelのシステムについて言及したい.

フレームワークでよく思うことは,
「どのファイルに何書けばいいかわからん.」
ということです.
まあこればっかりはいかにちゃんと学ぶかみたいなところはあるかもしれませんが.

ただ,Laravelはめちゃわかりやすかった.
ざっくり言えば,

  1. コントローラー(○○Controller.php)で諸々の機能を作る.
  2. Webページのテンプレート(○○.blade.php)を作って機能を埋め込む.
  3. ルート(web.php)で,さっき作ったテンプレートを表示できるようにする.

というステップをひたすら踏んでいくだけです.
多分.

まあまだ入門二日目なので,まだ難関は色々あるんだろうけど.

感想4. Bladeが直感的で便利

前項でWebページのテンプレートを作ると言いましたが,これはBlade(ブレード)と呼ばれるのだそうです.
Bladeを使うと何がいいかというと,普通のHTMLの構文を使いつつ,制御構文(ifやfor文)も使えるということ.

例えば,$userという変数の値が'boyfriend'だったときに<p>私だけを見て</p>を表示するテンプレートlovemessage.blade.phpを作るとすれば,

lovemessage.blade.php
//...
<body>
    @if($user == 'boyfriend')
        <p>私だけを見て</p>
    @endif
</body>
//...

と書けば,彼氏だけに愛のメッセージを伝えることができます.

直感的!!便利!!

ちなみに,@~で続く構文のことをディレクティブと呼び,@whileやら@switchやら@untillやら,一通りそろってます.

感想5. コントローラー ≒ Unityのスクリプト

前々項で
「コントローラー(○○Controller.php)で諸々の機能を作る」
と言いました.

でも入門者からしたら
「コントローラーって何?機能?は?」
って感じですよね.

個人的な意見ですが,
コントローラー≒Unityのスクリプト
です.(Unityしたことない方ごめんなさい)

Unityでプレイヤーのカメラを制御するスクリプトPlayerCamera.csとか作りますよね?
そんな感じです.

Unity
1つの役割(カメラの制御,プレイヤーの制御,敵の生成など)に対して1つのスクリプト
Laravel
1つの役割(トップページ,ログインページ,ブログページなど)に対して1つのコントローラー(多分)

というイメージです.

Unity
スクリプトにグローバル変数(`public int enemySpeed;`とか)をつくる → 敵キャラのスピードいじる
Laravel
コントローラーのpublicな関数がテンプレートに変数を渡す → ページに表示される数字いじる

という...イメージです...。(あれ,なんか微妙?)
とりあえず例を見てください.(お願いします)

次の例は,game/enemy.blade.php(gameディレクトリにあるenemyという名前のテンプレート)に,10から100までの乱数を持った変数$speedを渡す関数randomSpeed()を持つコントローラーEnemyspeedControllerです.

EnemyspeedController.php
<?php
//略
class EnemyspeedController extends Controller{

    public function randomSpeed(){
        return view('game.enemy',[
                'speed' => random_int(10,100)
        ]);
    }

    //略
}

...。

なんかUnityっぽくないですか?(ゴリ押し)

感想6. テンプレート → Bladeじゃなくてもいい

なんだ.Bladeじゃなくてもいいのね.

テンプレートをテンプレートたらしめる定義は,そのphpファイルが`resources/views'ディレクトリにあることなんでしょうね.多分.

終わり

とりあえずここまでです.
また感想があれば追加します.
さらば!!

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

Laravel初期設定

Laravelでプロジェクトを作成していく際の初期設定をやっていきます。
ここではLaravel ,Composerを導入し終えた状態で進めていきます。また、使うPCはMacです。

1.初期設定(ターミナル)

まず、ターミナルでMAMPのhtdocs内にプロジェクトを作成していきます。

$ cd /Applications/MAMP/htdocs

移動できたら、下記のコマンドを入力してblogというアプリを作成します。

$ composer create-project laravel/laravel --prefer-dist blog

最後に Application set key successfully.と表示されれば、
MAMPのhtdocs内にblogというプロジェクトを作成できました。
次に作成したmyblogに移動して、初期設定を行います。

$ cd blog

移動できたら初期設定を行います。
権限の設定です。Laravelではログファイルはデフォルトでapp/storage/logsディレクトリの下に作成されます。このディレクトリ以下にnginxはapacheユーザでの書き込み権限が必要です
権限に関してはこちら

$ chmod -R 777 storage
$ chmod -R 777 bootstrap/cache

storageとcacheがこれで権限が一番緩い状態になります。以上でターミナルによる初期設定は終了です。

2.初期設定(MAMP)

次にMAMPの設定です。localhostに接続した際のトップページを設定していきます。ここは設定してもしなくても大丈夫です。
1. MAMPを開く。
2. Desktopの左上のバーのところのMAMPを押してPreferenceを押す。
3. 真ん中のselectを押して、/Application/MAMP/htdocs/blog/publicを選択。
4. OKをクリック
5. start serverをクリック
6. localhostにアクセス
image.png
これはLaravelのトップページがpublic配下に存在するためです。localhostに接続した際に以下の画面が表示されれば正しく接続できています。

image.png

3.DB作成

webブラウザ上のMAMPの中でTOOLSのPHPmyadminを開きます。
image.png
1. サイドバーのnewを押して新しくDBを押す。
2. Create Databaseと出てくるので名前のところにblogと追加。
3. 右のセレクトボックスをutf8mb4_unicode_ciとする。
4. Createボタンを押す。
左側のサイドバーにblogと追加されれば完了です。

次にご自身のテキストエディターでblogを開いてください。.env.exampleというファイルが存在するのでひらいてください。.envファイルという似たようなファイルも存在しますが、.env.exampleの方に記載する事でGitで管理できるので複数人で開発をする際にはこちらに記載する方が良いらしいです。間違ってたらすいません。

image.png
1. 一行目のAPP_NAMEをlocalhostからblogに変更。
2. DB_PORTをご自身のPORT番号に変更。
3. DB_DATABASEをblogに変更。
4. DB_PASSWORDをrootに変更。
5. 15行目にDB_SOCKET=/Applications/MAMP/tmp/mysql/mysql.sockを追加。
(ご自身のPORT番号やパスワードはweb上のMAMPを開いていただいて、少し下にスクロールしていただくと書かれています。)

次に、mysqlのバージョンによっては文字化けする可能性があるのでそれを防ぐためのコードを追加していきます。テキストエディターで、 app/Providers/AppServerProviders.phpを開きます。以下の様に変更を加えてください。
1. 6行目にuse Illuminate\Support\Facades\Schema;を追加。
2. 27行目にSchema::defaultStringlength(191);を追加。
image.png

Timezone

Timezoneの変更をしていきます。
1. config/app.phpに移動
2. 70行目のUTCをAsia/Tokyoに変更
3. 83行目をlocaleを「ja」に変更

image.png

これで初期設定は終了になります。

終わりに

以上が初期設定でした。次回はLaravelでMVCの作成をしていきます。参考サイトを貼っておきますので是非確認してみてください。

https://www.youtube.com/watch?v=yaitzPzBzuI&t=600s
https://www.ritolab.com/entry/49
https://readouble.com/laravel/7.x/ja/database.html
https://qiita.com/shisama/items/5f4c4fa768642aad9e06

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

フリーランスとしてアリババクラウドにWebアプリケーションを導入する方法

この記事を通して、私がクラウド上にWebアプリケーションをデプロイする際に使用しているベストプラクティスを共有したいと思います。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

この記事はMedium Blogging Platformでも公開されています。

この記事では、私がウェブアプリケーションをクラウド上にデプロイする際に使用しているベストプラクティスを共有したいと思います。私はフリーランスとして働いていますが、最近、クライアントの一人から小さな組織のためにSuiteCRMのセットアップを依頼されました。私はAlibaba Cloudのチュートリアルを頻繁に書いているので、クライアントには同じクラウドプラットフォームを使うことを勧めました。100人近いユーザーと30人以上の同時使用ユーザーに対して、私が推奨した構成は以下の通りです。

1、PHP-FPMでNginxをインストールするためのvCPU2台、RAM4GBのECSインスタンス。
2、MySQL用のRDSインスタンスにApsaraDB for RDS、コア1GB、RAM1GB、ストレージ10GB。
3、メール送信用のDirect Mail。

私が行った手順は非常にシンプルで、ほぼ全てのPHPベースのアプリケーションに採用することができます。

初めてAlibaba Cloudを利用する方は、こちらのリンクからAlibaba Cloudにサインアップしてください。300米ドル相当の新規ユーザークレジットを無料で取得することができ、これを使って様々なAlibaba Cloud製品を試すことができます。

ECSインスタンスの作成

Alibaba Cloudでは、クラウドプラットフォームの利用を開始するために必要なほぼすべての情報を文書化しています。Alibaba Cloudの利用開始方法については、Getting Started TutorialsTech Share Blogを利用することができます。最もわかりやすい手順はクイックスタートガイドに記載されており、ECSインスタンスを作成する際に使用するベストプラクティスを説明します。

Alibaba Cloudコンソールにログインし、Elastic Compute Serviceインターフェースに移動します。Create Instanceボタンをクリックすると、簡単にインスタンスを作成することができます。注意すべき点は以下の通りです。

1、リージョン:アリババクラウドは世界中にデータセンターを持っているので、アプリケーションのユーザーに地理的に近い地域を常に選択してください。データセンターがユーザーの近くにあると、ネットワークの遅延が少ないため、ウェブサイトの読み込みが非常に速くなります。私の場合、組織がムンバイに拠点を置いていたので、ムンバイ地域を選択しました。
2、課金方法:インスタンスを24時間365日継続的に運用する場合は、月額課金を選択すると、従量課金に比べて価格が半分以下になるため、常に月額課金を選択する必要があります。例えば、2つのvCPUと4GB RAMの共有タイプのECSインスタンスの月額サブスクリプションコストは23ドルですが、同じインスタンスをPay-As-You-Goで利用した場合、1時間あたりのコストは0.103ドルとなります。月間コストは、$0.103*24*30 = $74.16 USDになります。

3、インスタンスタイプ:要件に応じてインスタンスタイプを選択します。必要に応じて後からリソースを増やすことができます。
4、イメージ:ECS インスタンスにインストールしたいアプリケーションを Marketplace イメージで見つけることができますが、常にクリーンな公式イメージで自分でインストールすることをお勧めします。後でアプリケーションにエラーが発生した場合、どこを見ればいいかわかるようになります。
5、ストレージ:システムディスクは ECS インスタンスがリリースされると削除されます。インスタンスが誤って削除されてもディスクは保持されるので、可能な限りデータディスクを使用してください。
私が使用した設定は以下の通りです。

image.png

デフォルトで作成されるVPCを選択することができます。その中に4092個のインスタンスを追加することができます。私はECSインスタンスごとに異なるセキュリティグループを使用しているので、個別に設定することができ、未使用のポートが開かれていないことを確認します。

もう一つ重要なのは、パスワードを使うのではなく、キーベースの認証を使うことです。すでにキーペアを持っている場合は、公開キーをAlibaba Cloudに追加します。持っていない場合は、Alibaba Cloudを使って作成することができます。キーは非常に安全な場所に保管し、キー自体はパスフレーズで暗号化されていることを確認してください。

image.png

ECSインスタンスを作成する際の注意点は以上です。

ECSインスタンスの設定

インスタンスを作成してターミナルにログインしたら、ウェブサイトをセットアップする前に考慮すべきことがいくつかあります。

1、コマンドを実行するためにrootアカウントを使用するのではなく、最初の接続にsudoユーザーを設定し、常にコマンドを実行するためにsudoユーザーを使用します。また、あまりにもsudoユーザーのためにキーベースの認証を設定し、完全にrootログインを無効にすることができます。
2、ベースイメージは常に更新しておきましょう。例えば、Ubuntu Serverで以下のようなコマンドを実行します。
3、Alibabaのベースイメージは、必要のない余分なパッケージを持っていません。また、必要のないパッケージをインストールしないことに注意してください。
4、インストール中に調子が悪くなった場合は、システムディスクを変更することで、いつでもインスタンスをリセットすることができます。インスタンスを削除して再作成する必要はありません。

sudo ユーザーを作成し、その中で key based auth を設定しました。ベースイメージを更新し、無人システムのアップグレードを設定しました。Nginx web-serverをインストールするためのチュートリアルに沿って、Nginx web-serverをインストールし、さらに、PHP-FPMを使ってPHP 7.2をインストールしました。PHP 7.2は現時点で入手可能なPHPの最新バージョンです。最新のソフトを使うことで、バグもなく、処理速度も速くなり、安定した運用ができるようになります。最後に、SuiteCRMのアーカイブを公式サイトからダウンロードして、Nginxにデプロイしてみました。

アプリケーションのインストールは、Getting startedのチュートリアルTech Shareの作者が書いたチュートリアルを参考にしてください。

セキュリティグループルールの設定

ECS インスタンスのセキュリティグループでは、未使用のポートを開かないようにすることが非常に重要です。私がSuiteCRMインスタンスに使用したセキュリティグループのルールを見てください。

image.png

すべての ICMP パケットと一緒にポート 22, 80, 443 だけを許可していることがわかります。22番ポートはSSH接続に使われています。ポート80は安全ではないHTTPポートで、私の場合はHTTPSの443番ポートにリダイレクトしています。ICMPパケットは、ホストが生きているかどうかを確認するためのpingに使われます。ICMPパケットを削除しても問題ありませんが、インスタンスへのpingができなくなります。

RDSインスタンスの作成

RDSインスタンスを作成する前に頭に浮かぶ最初の疑問は、なぜそれが必要なのかということです。また、MySQL、MariaDB、PostgreSQL、MongoDBなどのオープンソースのデータベースサーバーをECSインスタンス自体にインストールすることもできます。

この質問に対する答えは、RDSインスタンス用のApsaraDBは速度とセキュリティのために最適化されているということです。デフォルトでは、作成したインスタンスはホワイトリストに登録されたインスタンスのみがアクセス可能です。

ECSインスタンスを作成する際の注意点を見てみましょう。

1、リージョン:ECSインスタンスが作成されるデータベースインスタンスを作成する際には、必ず同じリージョンを選択してください。また、両者が同じVPC内にあることを確認してください。これにより、同じネットワーク内のホスト間で無料のイントラネットデータ転送を活用することができます。もう一つの利点は、ECSインスタンスのプライベートIPアドレスのみをホワイトリスト化する必要があることです。これにより、データベースのセキュリティが大幅に向上します。
2、課金:繰り返しになりますが、毎月のサブスクリプションのコストは、Pay-As-You-Go方式よりも低くなっています。ニーズに応じて選択してください。
3、容量:あなたは、1コア、1 GBのインスタンス、および5 GBのストレージなどのローエンドの構成で開始することができます。後で要件には、リソースを増やすことができます。
4、アカウント:MySQL 5.6 インスタンスのマスターアカウントは、必要な場合を除き、絶対に作成しないでください。データベースごとにデータベースとデータベースユーザを作成します。
以下は私がSuiteCRMで使用したRDSの設定です。

image.png

MySQL RDSインスタンスが有効になったら、インスタンスのセキュリティタブからECSインスタンスをホワイトリストにしました。IPアドレスをホワイトリスト化するとすぐに、RDSインスタンスのホスト名とポート番号を取得しましたが、これはMySQLのデフォルトポート "3306 "でした。「suitecrm」という名前のデータベースと「suitecrm」という名前のデータベースユーザを作成し、そのユーザに読み書きのアクセス権を与えました。

HTTPSの使用

インターネットの発達に伴い、毎日のように多くのウェブサイトが追加されています。Let's Encrypt証明書局がSSL証明書を無料で提供するようになってからは、すべてのWebサイトでSSLを利用することがトレンドになりました。セキュリティを確保するためには、WebアプリケーションでSSLを利用することは非常に重要です。やりとりされているデータが暗号化されていないと、ネットワークの中を盗み聞きされて機密情報を抜き取られてしまう可能性があります。

Alibaba CloudもSSL証明書を提供していますが、私の感覚ではかなり高額です。しかし、高価なものには余分な保証がつきものです。Alibaba Cloudが提供するSSL証明書は、企業ユーザーに適しています。

SuiteCRMの導入では、SuiteCRMのWebアプリケーションのセキュリティを確保するために、Let's Encryptの無料SSLも利用しました。Let's Encrypt CAのクライアントアプリケーションであるCertbotで証明書を生成するには、ドメインをサーバーに向ける必要があります。

アリババクラウドでは、ドメイン名を無料のwhois保護付きで非常にリーズナブルな料金で提供しています。私の場合、クライアントは既に他所から購入したドメイン名を持っていました。私はサブドメインを作成し、そのドメインをECSインスタンスに向けました。Certbotをインストールして、簡単に証明書を生成することができました。証明書の有効期限が3ヶ月ごとに切れているので、証明書を自動的に更新するためのcronジョブを設定することを忘れないでください。

ダイレクトメールの設定

エンタープライズグレードのウェブサーバーを作成するには、専門知識と時間が必要であり、維持するためのコストが非常に高くなるため、自分でメールサーバーを設定することは避けるべきです。ちょっとした設定ミスでメールが直接スパムフォルダに入ってしまうこともあります。

アリババクラウドダイレクトメールサービスは、SMTPを使用してアプリケーションからメールを送信する安価な方法を提供しています。毎日最初の200通までは無料です。私の場合は1日200通で十分で、無料枠を超えたメールも激安です。ダイレクトメールに新しいメールサブドメインを追加してみました。ドメインを追加した際に、DNSの更新を求められました。指示通りにやってみましたが、DNSの検証に時間がかかりました。一度完了したら、送信者アドレスを追加して、SMTPサーバをアプリケーションで使用できるようにしました。

ウェブベースのインストール

ようやく、すべての準備が整いました。ECSでホストされているSuiteCRMアプリケーション。データベースサーバーはRDS用のApsaraDBでホストされています。ソフトウェアをインストールするためのウェブベースのインストールを簡単に行うことができました。

以下は、SuiteCRMのWebベースのインストール中にデータベースサーバーに提供した設定です。

image.png

同様に、必要な情報をすべて記入し、SMTPサーバの詳細を提供しました。

image.png

最終的に、私のアプリケーションはAlibaba Cloud上にデプロイされました。さらに、ApsaraDB for Memcacheを使用してSuiteCRMのセッションキャッシュを保存し、WAFを使用して侵入してくる脅威からアプリケーションを保護することをクライアントに提案しました。しかし、クライアントは、月々の請求額が増えることを理由に、これらの提案の実装を拒否しました。しかし、クライアントはアリババクラウドプラットフォーム上のアプリケーションのパフォーマンスを見て非常に満足していました。

私はAlibaba Cloud Tech Shareプラットフォームで技術ブログを頻繁に書いています。以下のリンクにアクセスして、私が書いたチュートリアルを見つけてください。また、私はAlibaba Cloud Tech Shareプラットフォーム上のUbuntu 16.04にSuiteCRMをインストールするために私が従っている実行するための手順とコマンドの詳細なガイドを書きます。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

アリババクラウドECS上のWP-CLIでWordPressのメディア管理、ユーザー管理、データベース運用

この3部構成のチュートリアルでは、WP-CLIでWordPressの高度な管理を設定する方法を説明します。パート2では、アリババクラウドECS上のWP-CLIでWordPressを管理することに焦点を当てています。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

Alibaba Cloud Tech Share執筆者のJeff Cleverley著。Tech Shareは、クラウドコミュニティ内で技術的な知識やベストプラクティスを共有することを奨励するAlibaba Cloudのインセンティブプログラムです。

今回は、WP-CLIが提供する高度なWordPress管理能力についての3部構成の深堀りの第2弾です。

前回のチュートリアルでは、ECS インスタンスに WP-CLI をインストールし、それが正しく動作するように正しいパーミッションとユーザーロールを準備し、WordPress コンテンツを管理するために WP-CLI を使用することを見てきました。

このチュートリアルでは、より高度な機能をより深く掘り下げていきます。メディア、ユーザーの管理、データベース操作を行います。デフォルトのパラメータをグローバルとローカルに設定し、それぞれのコマンドでサーバー上の多くのサイトを管理するためにどのように使用できるかをお見せします。

最後のチュートリアルでは、この機能をどのように活用して、ローカルマシンからすべてのインスタンスで何百ものサイトを管理することができるのか、機能性の高いプラグインやテーマの開発を高速化するためのコマンドのデモンストレーションを行い、最後に WP-CLI を拡張して、カスタムコマンドを使ってカスタム機能を提供する方法を紹介します。WP-CLI を効果的に使用することで、広く使われているプラグインを削除することで、WordPress サイトの肥大化を減らすことができることがわかります。

これまでと同様に、このチュートリアルでは、すでに Alibaba Cloud ECS インスタンスがプロビジョニングされており、その上で 1 つ(または複数)の WordPress サイトが稼働していることを前提としています。

このシリーズを通して、私は自分のスーパーユーザ「new_user」を使用し、sudoコマンドを使用して「root」コマンドを発行します。コマンドを実行する際には、私のユーザーを自分のユーザーに置き換えることを忘れないようにしてください。

また、コード例では私のテストドメイン 'an-example-domain.com' と 'other-example-domain.com' を使用していますが、コマンドを発行する際には、私のサイトドメインをあなた自身のものに置き換えることを忘れないでください。

1. WP-CLIでプラグインとテーマを管理

WP-CLIには、サイト上のプラグインやテーマを管理するための豊富なコマンドが含まれています。多くのサイトを管理している場合、これらはあなたの生産性を本当に高めることができます。

プラグインを管理するには「wp plugin」コマンドを、テーマを管理するには「wp theme」コマンドを使用します。

WordPress プラグインコマンド

いつものように 'list' サブコマンドがあります:

$ wp plugin list

image.png

[プラグインとその状態を一覧コマンドで表に表示]

また、'status'サブコマンドを使ってプラグインとそのステータスを表示することもできます。

$ wp plugin status

更新が必要なプラグインがあれば、WordPress.org のプラグインリポジトリに記載されているプラグインの「slug」を使って更新することもできます。

$ wp plugin update akismet

プラグインのリストアップと更新が完了すると、ターミナルは以下のようになります。

image.png

[プラグインの状態を確認して更新する]

プラグインを個別にアクティベートしたり、リスト化したりすることもできます。

$ wp plugin activate akismet nginx-helper

または、'--all' パラメータを使用します。

$ wp plugin activate —all

または、プラグインの無効化と削除を行います。

$ wp plugin deactivate akismet && wp plugin delete akismet

WordPress.org のリポジトリからプラグインを簡単に検索することができます。ブラウザを使うよりもはるかに高速ですが、多くの情報を返すわけではありません。何を探しているのか分かっていて、インストールのために「slug」を必要とするだけならば、もっと便利になります。

$ wp plugin search nginx

それらをインストールして、1つのコマンドで「--activate」します。

$ wp plugin install regenerate-thumbnails —activate

もちろんプラグインをアップデートすることもできますが、コアと同じように、問題があればプラグインを任意の前のバージョンに戻すこともできます。

これはWP-CLIを使うととても楽になる作業の一つです。WordPress の管理者を使用するには、プラグインを無効にしてから削除し、ブラウザで古いバージョンのプラグインを検索して、それをあなたのマシンにダウンロードして、それをインストールするためにあなたのサイトにアップロードする必要があります。

$ wp plugin update regenerate-thumbnails --version=3.0.0

すべてのプラグインを一度に更新します。

$ wp plugin update —all

WordPressのテーマコマンド

他のコマンドと同様に、'list' サブコマンドを使ってテーマをリストアップします。

$ wp theme list

image.png

[テーマとその状態を一覧コマンドで表に表示]

また、「status」サブコマンドを使ってテーマとそのステータスを表示することもできます。

$ wp theme status

どのテーマでも更新が必要な場合は、私の場合はTwentyseventeenがそうですが、私たちもそれを行うことができます。

$ wp theme update twentyseventeen

「wp theme」は「wp plugin」と同じサブコマンドをすべて持っているので、ほとんど同じように管理することができます。

$ wp theme search <word to search by>
$ wp theme install <theme>
$ wp theme activate <theme>
$ wp theme deactivate <theme>
$ wp theme delete <theme>
$ wp theme update --all

2. データベース操作を行う

前述したように、WP-CLI を使うと、WordPress データベースの操作をコマンドラインから行うことができます。

WordPress データベースのエクスポートやインポート、バックアップ、問題が発生した後のバックアップへの戻し、ステージングサイトの作成、サイトの移行などが、今では信じられないほど簡単に行えるようになりました。

$ wp db export backup.sql
$ wp db import backup.sql 

あなたのデータベースを最適化したり、修復したりすることができます。

$ wp db optimize
$ wp db repair

しかし、これらの 'wp db' コマンドを使って BASH スクリプトを作成し、それを cron ジョブで実行することで、別のプラグインの必要性を排除することができます。これは WP-CLI が WordPress サイトの肥大化を減らし、作業負荷を最適化するもう一つの例です。

このチュートリアルの最後に、WP-CLI のタスク自動化の例としてスクリプトを作成します。

検索と置換

別のチュートリアルでは、WP-CLI を使ってサイトをステージング環境とローカル開発環境に移行しましたが、これは WP-CLI の最も強力なコマンドの一つである 'search-replace' で簡単にできました。

私のテストサイトで検索と置換を実行してみましょう。

wp search-replace "WP-CLI" "WP-CLI Awesome Sauce”

image.png

[WordPressのデータベースで検索して置換]

image.png

[WP-CLIオーサムソース]

データベースのdump - 注意してください!

データベース全体を削除するための 'dump' コマンドもあります。

$ wp db dump

これは確認が必要ですが、指先でアクセスできるようにするには明らかに非常に危険なコマンドです。いずれにしても、WP-CLI グローバル設定ファイルでパラメータを設定してこのコマンドを無効にすることをお勧めします。これについては後のセクションで説明します。

データベースクエリ

queryサブコマンドを使用して、標準的な MySQL コマンドでデータベースに問い合わせを行うことができます。

すべての投稿IDのリストをデータベースにクエリしてみましょう。

$ wp db query "SELECT id FROM wp_posts;”

すべての投稿 ID を示すテーブルが得られます。

image.png

[WP-CLIでデータベースをクエリ]

3. ワードプレスのメディアを管理

WordPressの開発者がWordPressメディアマネージャーを愛用していることを公言しているのを聞いたことがないような気がします。一般的に、画像やメディアに関わる管理作業は最も効率が悪いとされており、メディアマネージャーは一般的に工夫が足りないとされています。

画像のインポート

WP-CLI は、特に画像のインポートに関しては、これを助けることができます。

私の例のサイトのメディアマネージャーを見てみましょう。

image.png

[サイトの空のメディアマネージャの例]

そして、先ほど作成した投稿がこちら:

image.png

[特集画像無しの投稿7]

特に「wp media import」という組み合わせのコマンドは強力です。このコマンドには設定可能なパラメータがあります。

まず、インポート元の場所を指定する必要があります。これはあなたのサーバー上でも構いませんし、リンクから直接画像を取得することもできます。タイトル '--title='、キャプション '--caption='、altテキスト '--alt='、投稿に添付する画像 '--post_id='、特徴的な画像 '--featured_image' を設定することができます。

$ media import https://a-website.com/some-image.png --title=“Image Title” --caption=“Image Caption” --alt=“Image alt text” --post_id=7 --featured_image 

インポートが成功したかどうかは、ターミナルに表示されます。

これで、カンフー猫の画像をメディアライブラリに取り込むことに成功したことがわかります。

image.png

[カンフー猫はメディアライブラリーにあります]

画像にはタイトル、キャプション、altテキストが設定されています。

image.png

[Kung Fu Cat画像の詳細 - タイトル、Alt、キャプションがすべて設定されています]

そして、ワンコマンドで特集画像として投稿に添付されています。

image.png

[カンフー猫注目画像 - WP Admin]

image.png

[カンフー猫注目画像 - Front End]

長い間WordPressで仕事をしていれば、これがどれだけ便利なのか理解できると思います。

画像の一括インポート

WordPress 開発者としての最大の悩みの一つは、クライアントからサイトで使用したい画像を大量に提供されたときです。これを行うために WP 管理者のメディアマネージャーを使用することは、言葉が悪いですが、拷問のようなものです。

ディレクトリにアップロードしてWordPressにスキャンさせることはできないので、メディアマネージャーのインターフェイスを使ってディレクトリにインポートする必要があります。原理的には、WP-CLI は実際にはこのプロセスを変更することはなく、単に合理化して信頼性を向上させるだけです。

というわけで、私のデスクトップにはカンフー猫の画像があります。

image.png

[カンフー猫コレクション]

SFTP プログラムを使って、サーバー上のユーザーのホームディレクトリにあるディレクトリに画像をアップロードすると、ディレクトリの内容が一覧表示されて、それらの画像を見ることができます。

あとは、'import' コマンドを使って、そのディレクトリから画像をインポートするだけです。

$ wp media import ~/kung_fu_cats/*

そして、ブラウザベースのツールの遅さや潜在的な障害もなく、画像のディレクトリ全体をMedia Managerにインポートすることに成功しました。

image.png

[カンフー猫がいっぱいのメディアライブラリー]

メディアの再生

ほとんどの開発者が自分のサイトで使用しているそれらのツールの別の1つは、'再生サムネイル'プラグインのいくつかのフォームです。通常は大規模なプラグインではありませんが、なぜ不必要なプラグインでウェブサイトを肥大化させるのでしょう?

私たちは簡単に使用することができます。

$ wp media regenerate

これをさらに改善していくことが出来ます...。

4. WP Evalを使ってWP-CLIとPHPを組み合わせる

WP-CLI の本当の力は、コマンドを組み合わせたときに発揮されます。その例として、'wp media regenerate' コマンドと 'wp eval' コマンドの組み合わせがあります。wp eval を使うと、WP-CLI コマンド内で任意の PHP コードを実行できるようになるので、WP-CLI コマンドと WordPress や他の PHP 関数を組み合わせることができます。

そこで、'media regenerate' コマンドを WordPress の PHP と組み合わせて、公開された記事の特集画像のみを再生成するようにしてみましょう。

$ wp media regenerate $(wp eval 'foreach( get_posts( array( "post_status" => "publish" ) ) as $id ) { echo get_post_thumbnail_id( $id ) . " "; }’ )

私たちが見ているように、私たちの投稿のうちの1つだけが公開されており、この投稿のための注目画像だけが再生されています。

image.png

[公開されている記事一覧]

このようにWP-CLIコマンドを組み合わせることで、WordPressのインストールを管理する際に、ほぼ無制限の柔軟性が得られます。

5. WordPress のユーザーとロールの管理

WordPressのユーザーを管理

「wp user」を使用すると、ユーザーとその情報を簡単に作成、削除、更新、管理することができます。

自分のサイトのユーザーをリストアップします。

$ wp user list

ご覧のように、ユーザーは一人しかいません。もう一人のユーザーを作成して、彼に 'author' の権限を与えてみましょう。

image.png

[自分のWordPressサイトのユーザーをリストアップ]

$ wp user create john john@an-example-domain.com --role=author --user_pass=johnsgreatpassword

ここでもう一度ユーザーをリストアップすると、Johnが追加されていることがわかります。

image.png

[新規ユーザーの追加]

image.png

[新規ユーザーJohnが追加されました]

WP-CLI を使うと、ロックアウトされてしまった時に簡単にサイトにアクセスできるようになります。(私は今までに起きたことはありません。)

6. ユーザーの役割と機能の管理

新しい Teacher ロールを作成してみませんか?これも非常に簡単ですが、まず、そのロールがすでに存在していないことを確認することをお勧めします。

これは以下のいずれかの方法で行うことができます。

$ wp role list --fields=role

或いは

$ wp role exists teacher

ご覧のように、「teacher」というロールはありません。

image.png

[ Teacherのロールは存在しない]

そこで、これを作成して、エディタロールと同じ機能を与えることができるようになりました。

$ wp role create teacher “Teacher” --clone=“editor”

その後、新しいロールの一覧表を見てみると

image.png

[Teacherロールが作成されました]

そして、新しいユーザーであるJohnにTeacherのロールを与えるのは簡単です。

$ wp user add-role john teacher

再びユーザーをリストアップしてみると、Johnのロールに「Teacher」が追加されていることがわかります。

image.png

image.png

[Johnは現在教師です]

7. WP-CLI設定ファイル

WP-CLIでは、グローバルレベルまたはプロジェクトディレクトリレベルでデフォルトの設定パラメータを設定することができます。

デフォルトでは、グローバル設定ファイルはユーザーのホームディレクトリ内の隠しディレクトリ '~/.wp-cli/config.yml' にあります。

その場所は、インスタンス上で 'wp --info' コマンドを実行すると確認できます。

$ wp —info

ただし、このファイルは自動的には作成されません。ホームディレクトリから以下のコマンドを発行してください。

$ touch ~/.wp-cli/config.yml

これで、「wp --info」コマンドを実行すると、設定ファイルが「wp-cli」に登録され、一覧表示されているはずです。これで確認してください。

image.png

[グローバル設定ファイルが一覧表示されるようになりました]

このファイルを開いて編集し、デフォルトの設定パラメータを追加してみましょう。

$ nano ~/.wp-cli/config.yml

このファイルでは、WP-CLI グローバルパラメータのデフォルト設定、サブコマンドのデフォルト設定、他の WordPress インストーラに接続するためのエイリアスの作成などを行うことができます。

とりあえず、設定ファイルに以下を入力します。

# Global Parameter Defaults
disabled_commands:
 - db drop

# Subcommand Defaults
config create:
    dbuser: new_user
    dbprefix: nu_

まず、「#グローバルパラメータのデフォルト」では、WordPressサイトのデータベースを削除する機能を無効にしていますが、これはWP-CLIがデータベース操作を非常に簡単にしている場合に特に重要です。

次に、「#サブコマンドのデフォルト」では、WP-CLIの「wp config create」コマンドを使って「wp-config.php」ファイルを作成する際に、デフォルトのテーブルのプレフィックスと一緒に、自分のユーザーをデフォルトのデータベースユーザーに設定しています。

config ファイルでできる他の素晴らしいことの一つは、リモートサーバーへの接続をより簡単にするためのエイリアスを作成することです。それについては次のセクションで見ていきます。

WP-CLI では任意の作業ディレクトリに設定ファイルを設定することもできますが、これは 'wp-cli.yml' または 'wp-cli.local.yml' という名前でなければなりません。

例えば、以下のようにします。

/var/www/an-example-domain.com/htdocs/wp-cli.yml

設定ファイルの引数は、高いものから低いものまで、以下の順番で解釈されます。

1.コマンドラインの引数
2.ローカルディレクトリ内のwp-cli.local.ymlファイル
3.ローカルディレクトリ内のwp-cli.ymlファイル
4.‾/.wp-cli.config.ymlファイル
5.WP-CLIのデフォルト

8. 便利なAliasの使い方

現時点では、インスタンス上のWordPressサイトを作業したい場合、サーバーにログインしてサイトのルートディレクトリに変更してWP-CLIコマンドを発行する必要があります。このプロセスは、WP-CLI を使用することで得られる効率を著しく低下させます。

この問題を解決するためにAliasを使用することで、サーバー内のどこからでもサイトを操作できるようになります。

WP-CLIの設定ファイルを開いて再度編集します。

$ nano ~/.wp-cli/config.yml

下部の「#Subcommand Defaults」の下に, 以下の行を追加します.

# Aliases
@example:
    path: /var/www/an-example-domain.com/htdocs/
@another:
    path: /var/www/another-example-domain.com/htdocs/

そして、ファイルを保存して閉じます。

config.yml' ファイルは以下のようになっているはずです。

image.png

[WP-CLI設定ファイル with Aliasの場合]

これで、サーバー上のどこからでも、それぞれのサイトにAliasを使って WP-CLI コマンドを発行することができるようになりました。

例えば、ディレクトリをホームディレクトリに変更して、以下のコマンドを発行します。

$ cd ~
$ wp @example plugin list
$ wp @another plugin list

そして、両方のサイトのプラグイン一覧を見ていきます。

image.png

[Aliasを使ってサーバー上のどこからでも簡単にWP-CLIコマンドを発行できます]

9. WP-CLI & Cron Jobsでタスクを自動化

WordPressのサイトを管理していると、定期的にスケジュールを組んで実行しなければならないタスクがよくあります。そのようなタスクの1つが、先に述べたWordPressデータベースの自動バックアップをスケジュール的に実行することです。

Cronジョブを使ってWP-CLIコマンドを実行するようにシステムをスケジュールすることができます。

WP-CLIのCronコマンドは、WordPressの「WP-Cron」を使用しているため、WP管理者からのフィードバックに頼って実行するため、信頼性が低く、真のCronジョブではないので、私は使用しません。

ほとんどの場合、'WP-Cron' を無効にして、Linux システムの Cron ジョブとして 'wp-cron.php' を実行することをお勧めします。

あなたのサイトで、'wp-config.php' ファイルを開き、次の行を追加します。

define('DISABLE_WP_CRON', true)

ここで、編集用にシステムのcrontabを開きます。

$ crontab -e

そして、独自のサイトドメインを使用して、その中に次の行を追加します。

0 1 * * * * wget https://an-example-domain.com/wp-cron.php

wget コマンドは、コマンドラインからウェブからコンテンツを取得するために使用されます。非インタラクティブなので、コマンドラインでの利用には最適です。

これで system cron ジョブは毎朝 1 時に WordPress の 'WP-Cron' タスクを実行するようになりました。

同様に、WP-CLI コマンドを使って、システム Cron を使ってスケジュールされたバックアップを実行してみましょう。システムCronのジョブは「crontab」を使って管理しています。

システムの「crontab」を開いて編集します。

crontab -e

WP-CLI のフルパスを追加する必要があります。このチュートリアルの冒頭では '/usr/local/bin' に移動しました。

Aliasを設定していない場合は、WP-CLI グローバルの '--path=' パラメータを使用する必要があります。

0 0 * * * /usr/local/bin/wp --path=/var/www/an-example-domain.com/htdocs db export

Aliasを設定している場合は、追加するだけです。

0 0 * * * * /usr/local/bin/wp @example db export

今、あなたのサイトは、真夜中に毎日のバックアップをスケジュールします - 0 0 *

あなたの「crontab」はこのようになっているはずです。

image.png

[Linux Cronで信頼性の高い仕事のスケジュールを組みます]

このシリーズのパート2は終了しました。最終回のチュートリアルでは、これまでに学んだコマンドや設定を使って、数百ものWordPressサイトをいくつものアリババインスタンスで管理できるようにしていきます。また、WordPress の開発とデプロイのワークフローで WP-CLI を使用して開発をスピードアップする方法を学び、最後にカスタムコマンドを使って拡張します。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

gitでコンフリクトが発生してしまったら

コンフリクトとは、作業用ブランチさんとmasterブランチさんとの間にどんどん距離ができてしまってる状態のこと。

自分が作業してる間に、masterは更新されていきます。

気づかないうちに、お互いがどんな作業をしているのか知らないまま進んでしまっている状態なんです。

【コンフリクトをなくす方法】

1 まずどのファイルがコンフリクトが発生してるのか確認する
git status

2 コンフリクトが発生してるファイルをmasterと比較して修正する

3 修正したファイルを追加してコミット
git add .
git commit -m “コンフリクト解消”

4 作業用ブランチをプッシュする
git push origin 作業用ブランチ名

5 git merge master

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

開発用ブランチにMasterブランチの最新コードを取り込む

【手順】
※ もし、作業途中のものでcommit出来るものがあればcommitしておく

1 masterブランチへ移動
git checkout master

2 git pullでmasterを最新に
git pull origin master

3 開発用ブランチへ移動
git checkout 開発用ブランチ

4 mergeコマンドでmaserの内容を取り込む
git merge master

5 取り込んだものをリモートにpush
git push origin 開発用ブランチ

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

php artisan migrate が実行できないとき

Laravelでマイグレーションを実行しようとしたが、実行できない…

エラー内容

SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost'

直訳すると、root@localhostのユーザーが定義されてませんという感じ。

どうやらmysqlにうまく接続できてないっぽいですね。

【原因】
同じテーブルを2つ作成してしまっていた。
migrationでのテーブル作成と、ターミナル上でcreate table文を使っても作成していた。

【解決方法】
ターミナル上で作成したテーブルを削除すればok。

Laravelを使い始めたばかりでマイグレーションに関してよくわかっていなかった。。。?

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

アリババクラウドECS上のWP-CLIで複数のWordPressサイトを管理する方法

この3部構成のチュートリアルでは、WP-CLIでWordPressの高度な管理を設定する方法を説明します。パート3では、アリババクラウドECS上のWP-CLIで複数のWordPressサイトを管理することに焦点を当てています。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

この記事はAlibaba Cloud Tech Share執筆者のJeff Cleverleyによるものです。Tech Shareは、クラウドコミュニティ内で技術的な知識やベストプラクティスを共有することを奨励するAlibaba Cloudのインセンティブプログラムです。

前回のチュートリアルでは、WP-CLI をインスタンスにインストールし、適切に動作するように正しいパーミッションとユーザーロールを準備し、WordPress コンテンツを管理するために使用しました。その後、インストール、プラグイン、テーマ、ユーザーの管理、データベース操作を行うためのより高度な機能を掘り下げていきました。また、デフォルトのパラメータを設定して、各コマンドでサーバー上の多くのサイトを管理できるようにしました。

最後のチュートリアルでは、WP-CLI をさらに活用して、ローカルマシンから多くの異なるサーバーインスタンスにまたがる何百ものサイトを管理する方法を実演します。最後に、WP-CLI を拡張してカスタムコマンドでカスタム機能を提供する前に、機能プラグインやテーマの開発をスピードアップするために使用できるコマンドを探っていきます。

これまでと同様に、このチュートリアルでは、すでにアリババのインスタンスがプロビジョニングされていて、その上で 1 つ(またはいくつか)の WordPress サイトが稼働していることを前提としています。

このシリーズを通して、私は自分のスーパーユーザー「new_user」を使用し、sudoコマンドを使用して「root」コマンドを発行します。コマンドを実行する際には、私のユーザーを自分のユーザーに置き換えることを忘れないようにしてください。

私はまた、コード例で私のテストドメイン'an-example-domain.com'と'another-example-domain.com'を使用していますが、コマンドを発行するときに私のサイトドメインをあなた自身のものに置き換えることを覚えておいてください。

1. WP-CLIで複数のWordPressサイトを管理する

CLIを使った方が、ブラウザでWordPressサイトを管理するよりもはるかに効率的で生産的であることが明らかになりました。物事はずっと速く動き、コマンドは慣れてしまえばとても簡単です。

適当に使えば使えそうなパワーを感じています。それでもダッシュボードからやるよりは、特に多くのWordPressインストールを同時に管理できるサービスがたくさんある中では、それほど効率が良いとは思えないかもしれません。

では、どうすればいいのでしょうか?よくあることですが、複数の方法があります。今日はそのうちの2つを見てみましょう。

AliasグループとBASHスクリプトについてです。

1a.Aliasグループを使って複数のサイトを管理

先ほどの例ではAliasesを使って、以下のコマンドで2つのサイトのプラグインをチェックしてみました。

$ cd ~
$ wp @example plugin list
$ wp @another plugin list

しかし、WP-CLI はこの非効率性も解決してくれます。Aliasをグループにまとめて、複数のサイトのアクションを一度のコマンドで実行することができます。

Aliasグループを作成してみましょう。編集用の設定ファイルをもう一度開きます。

$ nano ~/.wp-cli/config.yml

そして今回は、以前のAliasの下にもう一行追加します。

# Alias Groups
@all-examples:
 - @example
 - @another

完全な設定ファイルは以下のようになっているはずです。

image.png

<設定にAliasグループを追加する>

これで一つのコマンドを発行することができます。例えば、両方のサイトのプラグインをリストアップしてみましょう。

$ wp @all-examples plugin list

image.png

<多くのサイトを管理するためにAliasグループを利用>

実際には、WP-CLIには@all Aliasも組み込まれていて、定義されたすべてのAliasに対して任意のコマンドを実行します。

$ wp @all plugin list

しかし、それはこの一連のチュートリアルで必要とされる実証的な目的のために機能します。

1b.WP-CLIでBASHスクリプトを使用して複数のサイトを管理

以下の方法でbashスクリプトにwp-cliコマンドを追加することで、上記と同様の結果を得ることができます。

PATHにスクリプトファイルを作成します。

$ sudo touch /usr/local/bin/wp-plugin-list.sh
$ sudo nano /usr/local/bin/wp-plugin-list.sh

そして、このコードを追加します。

#!/bin/bash 
declare -a sites=(
    '/var/www/an-example-domain.com/htdocs' 
    '/var/www/another-example-domain.com/htdocs'
)
for site in "${sites[@]}"; 
do 
    echo “List plugins for:  
    echo $site
    wp --path=$site plugin list
    echo “ “ 
done

保存して終了し、スクリプトを実行可能な状態にします。

$ sudo chmod +x /usr/local/bin/wp-plugin-list.sh

これでスクリプトを実行する準備ができました。まず、スクリプトを実行してプラグインをチェックします。

$ wp-plugin-list.sh

image.png

[WP-CLIコマンドとスクリプトを使用したすべてのプラグインをリストアップ]

一般的には、BASH スクリプトを作成するよりもAliasを使用する方が最適と考えられますが、スクリプトの方が優れている場合もあります。スクリプトを使用すると、WP-CLI コマンドと WP-CLI 以外のコマンドをさまざまな組み合わせで組み合わせることができ、多くのサイトを管理するためにより効果的であることが証明されるかもしれません。例えば、データベースをバックアップして、サーバー上の別のディレクトリやサードパーティのストレージソリューションにコピーすることができます。

WP 管理者から各サイトにログインして、プラグインの更新をチェックする作業は、これらの方法よりもはるかに時間がかかります。

注意事項:
この時点から、この一連のチュートリアルで示されたコマンドは、より適切であればAliasを使用して発行することができます。独自のAliasを使用するか、Aliasを WordPress ディレクトリへのフルパスに置き換えてください。

2. WP-CLIでSSHを使ったリモート管理

ここまでは順調です。今はWP-CLIの力を使って、複数のWordPressサイトを同じサーバーで管理しています。何十、何百ものサイトがある場合、これは信じられないほどの貴重な時間を節約することができます。

しかし、私たちはまだワークフローを改善することができます。

1.ローカルサーバーからリモートサーバーにSSH接続。

2.WordPress サイトディレクトリ内で WP-CLI コマンドを実行するか、サーバー内のスクリプトを使用して、そのサーバー上の多くの WordPress サイトディレクトリにまたがってコマンドを実行。

3.他のサーバーに対しても同様の処理を繰り返す。

これらを組み合わせて、おそらく 1 つの Alibaba インスタンスだけでなく、多くのインスタンスにまたがって WP-CLI 管理コマンドを実行することができたらどうでしょうか?

そのためには、ローカルマシンとすべてのAlibabaインスタンスの両方でWP-CLIを実行する必要があります。

Windows PCにWP-CLIをインストールする手順

MacにWP-CLIをインストールする手順

このセクションを完了する前に、インストールを完了する必要があります。

ローカルマシンから WP-CLI コマンドを発行し、'--ssh=' パスコマンドを使用してリモートサーバー上で WP-CLI コマンドを実行させることができます。

リモートのプラグインをリストアップします。

$ wp --ssh=new_user@an-example-domain.com/var/www/an-example-domain.com/htdocs/ plugin list

または、私のリモートテーマをリストアップします。

$ wp --ssh=new_user@an-example-domain.com/var/www/an-example-domain.com/htdocs/ theme list

ご覧の通り、これは完璧に機能します。

image.png

[WP-CLIをローカルで使用してリモートでホストされているWPサイトを管理]

AliasとSSHの使用

SSH コマンドをフルパスで入力しなければならないのは非常に疲れますし、非効率的です。前のセクションでは、フルパスを必要とせずにインスタンス内のどこからでも WP-CLI コマンドを発行できるようにするためにAliasを使用しました。

グローバルなAliasの使用

ローカルマシンに.wp-cliディレクトリやグローバル設定ファイルがまだ作成されていないかもしれません。そうでない場合は、まずそれらを作成してから、設定ファイルを開いて編集してください。

(既に存在する場合は、編集用の設定ファイルを開いてください)

$ sudo mkdir ~/.wp-cli && sudo touch ~/.wp-cli/config.yml
$ sudo nano ~/.wp-cli/config.yml

インスタンスのグローバル設定ファイルに追加した方法と非常によく似た方法で、 Aliasを追加します。唯一の違いは、'path' の代わりに 'ssh' パラメータを使って設定することです。disabled_commands' も同時に設定すべきでしょう。

#Global Parameter Defaults
disabled_commands:
 - db drop

# Aliases
@example:
    ssh: new_user@an-example-domain.com/var/www/an-example-domain.com/htdocs/
@another:
    ssh: new_user@another-example-domain.com/var/www/another-example-domain.com/htdocs/

ローカルマシン上のグローバル WP-CLI 設定ファイルは以下のようになっているはずです。

image.png

[ローカルのグローバル設定ファイルにリモートエイリアスを追加]

これでローカルで WP-CLI を使うことができるようになり、'wp' コマンドとサブコマンドの間にAlias '@name' を追加するだけで、リモートの Alibaba インスタンスで WP-CLI の管理タスクを簡単に実行できるようになりました。

ローカルマシンからリモートインスタンスにコマンドを発行します。

$ wp @example plugin list

或いは

$ wp @another core check-update

Aliasグループを使ってSSHで複数のサイトを管理

サーバー上と同じように、リモートで複数のサイトを管理するために、ローカルでAliasグループを使用することができます。

違いは、異なるサーバー上の WordPress サイトへのAliasを含む Alias Groups を作成できるようになったことです。つまり、Aliasグループを使って、ローカルマシンからコマンドを発行して、多くの異なるアリババインスタンス上の多くの異なる WordPress サイトを管理できるようになりました。

ローカルのグローバル設定ファイルを開きます。

$ nano ~/.wp-cli/config.yml

既存のAliasesの下に以下の行を追加します。

@all-examples:
 - @example
 - @another 

設定ファイルは以下のようになっているはずです。

image.png

[ローカルのグローバル設定ファイルにAliasグループを追加]

プロジェクト内でのAliasの使用

モダンなWordPress開発とデプロイのワークフローの一部としてWP-CLIを使用していることがあります。他のチュートリアルでは、ステージング環境、ローカル環境、プロダクション環境を設定し、Git のバージョン管理ワークフローを実装しています。

WP-CLI はこのような状況で非常にうまく機能し、プロジェクトのローカル WP-CLI 設定ファイル内でプロジェクトごとに特定の Aliases を設定することができます。

本番サイトのローカル開発版がある場合は、'wp-cli.yml' ファイルを追加して、このワークフローのグローバルパラメータを設定します。

ローカル開発サイトのルートにディレクトリを変更し、その中に 'wp-cli.yml' ファイルを作成します。

$ cd /local/sites/anotherexampledomain/app/public
$ sudo nano wp-cli.yml

このファイルの中に、リモートサイトごとに異なるAliasを追加します。

@test:
    ssh: new_user@another-example-domain.com/var/www/test.another-example-domain.com/htdocs
@staging: 
    ssh: new_user@another-domain.com/var/www/staging.another-domain.com/htdocs/
@production:
    ssh: new_user@another-domain.com/var/www/another-domain.com/htdocs/

プロジェクトの 'wp-cli.yml' 設定ファイルは以下のようになっているはずです。

image.png

[ステージングサイトや制作サイトへのSSHパスの追加について]

さて、ローカルで作業しているときに、これらのエイリアスを使ってリモートサーバーにWP-CLIコマンドを発行することができるようになりました。

$ wp @production plugin list

そして、グローバルエイリアスと同じように、Aliasグループを作成することができます。

@test:
    ssh: new_user@another-example-domain.com/var/www/test.another-example-domain.com/htdocs
@staging: 
    ssh: new_user@another-domain.com/var/www/staging.another-domain.com/htdocs/
@production:
    ssh: new_user@another-domain.com/var/www/another-domain.com/htdocs/
@prerelease:
 - @test
 - @staging

これで、以下のようにプロジェクトのAliasグループにコマンドを発行することができました。

$ wp @prerelease plugin list

3. WP-CLIスキャフォールドを使って開発スピードを上げる

WP-CLI には 'wp scaffold' というコマンドがあり、標準的な WordPress 開発タスクの多くに必要なボイラプレートコードを生成するために使用されます。

これらの基本的なコードを生成するには、以下のようなものがあります。

  • Child Themes
  • Feature Plugins
  • Custom Post Types
  • Custom Taxonomies
  • PHPunit testing of plugins
  • PHPunit testing of themes PHPunitのテストコード生成のデモンストレーションは、それ自体がシリーズ化されている必要があるため、このチュートリアルの範囲を超えています。しかし、「wp scaffold」でできる他のクールなことを手短に説明しましょう。

基本的なディレクトリ構造と、テーマを有効化するために最低限必要なファイルを作成したい場合は、次のコマンドを実行します。

 wp @example scaffold child-theme twentyseventeen-child --parent_theme=twentyseventeen --theme_name="Twenty Seventeen Child" --author="New User" --author_uri="an-example-domain.com" --theme_uri="an-example-theme.com" —activate

エイリアスを使用して、ローカルマシンからAlibabaインスタンスに子テーマを作成する方法に注目してください。信じられないほど効率的です。

テーマが作成されたことがターミナルで確認できます。

image.png

そして、サイト上で活性化されています。

image.png

image.png

[WP管理者の新しい子テーマ]

プラグインはどうでしょう?それは至ってシンプルです。

wp @example scaffold plugin custom-plugin --plugin_name="Custom Plugin" --plugin_description="A custom plugin for custom functionality" --plugin_author="New User" --plugin_author_uri=https://an-example-domain.com --plugin_uri=https://custom-plugin.com —activate

そして今、私たちはプラグインをインストールして有効にしています。

image.png

[WP管理者の新しいプラグインについて]

WordPressで最も一般的な開発者のタスクの1つは、真のカスタムWordPress開発のためのカスタムポストタイプとタクソノミーを構築することです。

カスタムポストタイプのボイラープレートコードを作成し、プラグインまたはテーマのいずれかに追加し、必要なディレクトリ構造を以下のようにして追加します。

wp @example scaffold post-type tutorials --label="Tutorials" --plugin=custom-plugin --dashicon=welcome-learn-more

同様に、カスタムタクソノミーを使用します。

wp @example scaffold taxonomy wpcli --post_types=posts --label="wpcli" --plugin=custom-plugin

WP Adminで生成されたコードやディレクトリ構造を検査することができます。

image.png

[カスタムポストタイプのボイラープレートのコードとプラグインで生成されたファイルについて]

image.png

[カスタムタクソノミのボイラプレートのコードとプラグインで生成されたファイルについて]

4 WP-CLIの拡張

これらのチュートリアルを経て、ここまでたどり着いたのであれば、WP-CLIはWordPressのツールセットに欠かせないツールだと思っていただければ幸いです。

ここで終わったとしても、これは素晴らしいことです。

WP-CLI は拡張性があるので、私たちの旅はここで終わりません。

これは2つの方法で達成することができます。ファイルからPHPをロードしてWP-CLIで実行するか、カスタムコマンドを作成するプラグインを構築することができます。

両方の方法を実演するために、似たような出力例を使ってみます。

PHPファイルを実行

動作するかどうかテストしたい PHP スニペットがある場合、わざわざ WordPress プラグインや functions.php テーマファイルに含めてテストする必要はありません。

WP-CLI が提供する 'wp eval-file' コマンドを使えば、すぐにテストできます。これはWordPressを読み込んだ後にPHPファイルを実行します。

ホームディレクトリに 'random-post.php' ファイルを作成し、編集用に開きます。

$ nano random-post.php

ファイルの中に以下のコードを追加します。

<?php
global $wpdb;
$random_example_post = $wpdb->get_var(
    "SELECT post_title
    FROM $wpdb->posts
    WHERE post_type = 'post'
        AND post_status = 'publish'
    ORDER BY rand()
    LIMIT 1"
);
echo "A random example post: $random_example_post \n";

エディタでPHPスクリプトを作成します。

image.png

<実行するPHPスクリプトファイル>

これは、ランダムな投稿をデータベースに照会して表示するだけの基本的な機能です。

ここで、エイリアスを使用するか、WordPressディレクトリ内から以下のコマンドを実行してください。

$ wp @example eval-file random-post.php

あなたの端末には、ランダムな投稿タイトルが表示されるようになりました。

image.png

[WP-CLI eval-fileを使ってWordPressでPHPファイルを実行してみましょう]

プラグインでカスタムコマンドを作成する

また、独自の WordPress プラグインを作成することで、WP-CLI の内蔵機能を拡張することもできます。このようにして、WP-CLI の機能を拡張するには、WP-CLI コマンドを使って独自の機能を追加します。

まず、「wp scaffold」関数を使って、プラグインのボイラプレートを作成してみましょう。

wp @example scaffold plugin wp-cli-custom-commands —plugin_name=“WP-CLI Custom Commands" --plugin_description="A custom plugin for creating a WP-CLI custom command“ --plugin_author="New User" —activate

ここで、ディレクトリをプラグインのルートディレクトリに変更し、プラグインファイルを開きます。

$ sudo nano wp-cli-custom-commands.php

$ cd /var/www/an-example-domain.com/htdocs/wp-content/plugins/wp-cli-custom-commands

プラグインファイルには、各コマンドに対応するクラスと、各サブコマンドに対応するパブリックメソッドが必要です。

これを実演するために、先ほどのスクリプトと同じ機能を再現し、'random-post'コマンドで実行させます。

「Custom_Commands」という名前のクラスを作成します。このクラスには、サブコマンド用の単一のパブリックメソッド「random_post」が含まれており、上記のスクリプトと同じ単純なデータベースクエリを実行してランダムな投稿を返します。

次に、'add-command' を使って WP-CLI コマンドとしてクラスを登録します。この関数はコマンド名とクラス名を引数にとります。

wp-cli-custom-commands.php' に以下のコードを追加します。

if( defined( 'WP_CLI' ) && WP_CLI ) {

        class Custom_Commands {

                function random_post ( $args, $assoc_args ) {

                        global $wpdb;

                        $random_post = $wpdb->get_var(
                        "SELECT post_title FROM $wpdb->posts 
                        WHERE post_type = 'post' 
                        AND post_status = 'publish' 
                        ORDER BY rand() 
                        LIMIT 1");

                        WP_CLI::success( "Random post: $random_post" );
                }
        }

        WP_CLI::add_command( 'custom_commands', 'Custom_Commands' );
}

プラグインのメインファイルはエディタではこのようになっているはずです。

image.png

[カスタムコマンドプラグインファイル]

「wp scaffold」コマンドで定義していなかったので、プラグインのURIと著者のURIが抜けています。

あとは実行することで、コマンドが登録されていることを確認できます。

$ wp @example custom_commands

これは利用可能なサブコマンドの一覧を表示します。これで、そのコマンドを実行することができます。

$ wp @example custom_commands random_post

端末のWP-CLIでは、WordPressサイトからランダムに選択された投稿タイトルの後に「Success」というメッセージが表示されるはずです。

image.png

おめでとうございます - WP-CLIの拡張に成功しました。

これで、初めての WP-CLI カスタムコマンドを作成し、実行することができました。

これで、プラグインを使ってWordPressサイト上で様々な機能を実行するコマンドを作成し、思いつく限りのWordPress管理タスクを実行するための簡単な方法を作成することができるようになったはずです。

このシリーズのチュートリアルの最後に到達しました。WP-CLI の可能性を深く掘り下げてきましたが、これは可能性の表面を掻きむしっているに過ぎません。

もっと知りたい方は、公式リソースをチェックしてみてください。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

PHPでShift_JISのcsvファイルをパースするとcsvが壊れる

PHPでcsvファイルをパースしようと思ったのですが、文字列にカンマが混入しているところがうまくパースできませんでした。"文字列,文字列"となっている部分のカンマが項目の区切りのカンマと認識されてしまいます。

解決方法

"文字列,文字列"という部分は通常文字列リテラルの"で囲われているので文字列として認識されるはずなのですが、Shift_JISのファイルについてはうまくいきません。

一旦ファイルの中身をUTF-8に変換して、tmpファイルに保存した後に、csvファイルとして読み込むとうまく認識することができます。

$sJisFile = file_get_contents($file->path());
$utf8File = mb_convert_encoding($sJisFile, 'UTF-8', 'SJIS');
$fp = tmpfile();
$meta = stream_get_meta_data($fp);
fwrite($fp, $utf8File);
rewind($fp);

$file = new \SplFileObject($meta['uri']);
$file->setFlags(\SplFileObject::READ_CSV);

foreach($file as $line) {
    var_dump($line);
}

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

Laravelでクリーンアーキテクチャに近づけてみる

目次

  • はじめに
  • バージョン
  • もととなるシンプルなCRUD
  • クリーンにしてみる
    • 準備
    • packagesディレクトリの全体像
    • Entities
    • Gateways
    • Use Cases(とControllers)
  • テストを書いてみる

はじめに

Laravelを使ってクリーンアーキテクチャっぽいことをやってみました。
"クリーンアーキテクチャっぽくしつつLaravelの機能(主にEloquent)を使う"
ということを試してみました。
自分の理解がまだ浅いこともあって、"クリーン" かどうかと聞かれると正直微妙なところですが、MVCフレームワークの恩恵を受けつつクリーンアーキテクチャを採用してみる一つの妥協点としてはアリなんじゃないかと思います。

実際のコードはこちら
https://github.com/koyablue/laravel_clean_architecture

バージョン

PHP 7.4.4
Laravel 6.18.40

もととなるシンプルなCRUD

  • ユーザーがいて、そのユーザーがメモの作成/編集/削除ができる
  • 作成したメモの一覧と詳細が表示できる

というシンプルなCRUDで試してみます。
例として以下のようなものを想像してください。

controller
app/Http/Controllers/MemoController.php
<?php

namespace App\Http\Controllers;

use App\Models\Memo;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class MemoController extends Controller
{
    /**
     * 一覧
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function index(){
        $user = Auth::user();
        $memos = $user->memos;
        return view('index', compact('memos'));
    }

    /**
     * 詳細表示
     * @param $memoId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function show($memoId){
        $memo = Memo::find($memoId);
        return view('detail', compact('memo'));
    }

    /**
     * 新規作成
     * @param Request $request
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function create(Request $request){
        $user = Auth::user();
        $input = $request->get('content');
        $memo = new Memo();
        $user->Memos()->save(
            $memo->fill(['content' => $input]));
        return redirect(route('index'));
    }

    /**
     * 編集画面
     * @param $memoId
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function edit($memoId){
        $memo = Memo::find($memoId);
        return view('edit', compact('memo'));
    }

    /**
     * 更新
     * @param Request $request
     * @param $memoId
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function update(Request $request, $memoId){
        $memo = Memo::find($memoId);
        $memo->fill(['content' => $request->get('content')])->save();
        return redirect(route('index'));
    }

    /**
     * 削除
     * @param $memoId
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function delete($memoId){
        $memo = Memo::find($memoId);
        $memo->delete();
        return redirect(route('index'));
    }
}


クリーンにしてみる

  • Laravelの機能(Eloquent, FormRequestなど)をある程度使った上で、なるべくクリーンアーキテクチャに近づけてみる

ということを心がけながら、上記のCRUDをクリーンアーキテクチャっぽくしてゆきましょう。
ということで、あの図を貼っておきます。
CleanArchitecture.jpg

準備

スクリーンショット 2020-09-27 19.03.45.png
appとおなじ階層にpackagesというディレクトリを作成します。今回はこの中でいろいろ細かく区切って実装してゆくことにしました。
ディレクトリを追加したら、ちゃんとLaravelに読み込んでもらえるようcomposer.jsonも編集しておきます。

composer.json
"autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "classmap": [
            "database/seeds",
            "database/factories",
            "packages" <-これを追加
        ]
    }

事前準備はこれで終わりです。

packagesディレクトリの全体像

ちなみに完成後のpackagesディレクトリは、以下のようになります
スクリーンショット 2020-09-27 20.38.44.png
このディレクトリの置き方がいいのか悪いのかちょっと自信がありませんが、とりあえず今回は、こんな感じの配置なんだなーと思っていただければ。

Entities

Entityが何かを調べたり考えたりしていたら正直いつまでたっても先に進まない気がしたので、とりあえず
「アプリケーションの中で一番重要そうなモデル(達)」
というふうに捉えて実装しようと思います。(ちなみにこちらの記事がとてもわかりやすかったです。https://nrslib.com/clean-ddd-entity/)

packages/Domain/Domain
というディレクトリをEntities用に用意します。
今回のサンプルは

  • ユーザーがいて、そのユーザーがメモの作成/編集/削除ができる
  • 作成したメモの一覧と詳細が表示できる

というものでした。
"ユーザー" と "メモ"が重要っぽく見えますので、Entityとして必要なのはユーザーとメモと考えるのが妥当かなと思います。(本当はたぶんもっとしっかり整理/洗い出しを行わないといけないんですが...)

今回メモのCRUDに焦点を当てているので、ユーザーは一旦置いておいて、メモのドメインモデルのみ作成します。

packages/Domain/Domain/Memo/Memo.php
<?php

namespace packages\Domain\Domain\Memo;

class Memo
{
    private int $id;
    private int $userId;
    private string $content;
    private \DateTime $createdAt;

    /**
     * Memo constructor.
     * @param int $id
     * @param int $userId
     * @param string $content
     * @param \DateTime $createdAt
     */
    public function __construct(int $id, int $userId, string $content, \DateTime $createdAt)
    {
        $this->id = $id;
        $this->userId = $userId;
        $this->content = $content;
        $this->createdAt = $createdAt;
    }

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return int
     */
    public function getUserId(): int
    {
        return $this->userId;
    }

    /**
     * @return string
     */
    public function getContent(): string
    {
        return $this->content;
    }

    /**
     * @return \DateTime
     */
    public function getCreatedAt(): \DateTime
    {
        return $this->createdAt;
    }
}

Gateways

DBとやりとりする部分です。

  • packages/Domain/Domain/Memo/MemoRepositoryInterface
  • packages/Infrastructure/Memo/MemoRepository
  • packages/UseCase/Memo/QueryService/MemoQueryServiceInterface
  • packages/Infrastructure/Memo/MemoQueryService

の4つをGatewaysとして作成しました。
永続化系(Repository)と参照系(QueryService)の処理を分けて、それぞれにinterfaceと実装クラスがあるというような感じです。

Eloquentをどこで使うかを考えた結果、この層で使うことにしました。
ただし、あくまでも処理の途中で使用するだけで、返す値はドメインモデルか、xxDtoと名付けた専用モデルにするよう心がけました。

MemoRepositoryInterface
packages/Domain/Domain/Memo/MemoRepositoryInterface.php
<?php
namespace packages\Domain\Domain\Memo;

interface MemoRepositoryInterface
{
    public function save(Memo $memo): Memo;

    public function update(int $memoId, string $content): Memo;

    public function delete(int $memoId);
}

MemoRepository
packages/Infrastructure/Memo/MemoRepository.php
<?php

namespace packages\Infrastructure\Memo;

use packages\Domain\Domain\Memo\Memo;
use packages\Domain\Domain\Memo\MemoRepositoryInterface;
use App\Models\Memo as EloqMemo;

class MemoRepository implements MemoRepositoryInterface
{
    /**
     * @param Memo $memo
     * @return Memo
     */
    public function save(Memo $memo): Memo
    {
        $eloqMemo = new EloqMemo();

        $eloqMemo->fill(
            [
                'user_id' => $memo->getUserId(),
                'content' => $memo->getContent()
            ])->save();

        return new Memo($eloqMemo->id, $eloqMemo->user_id, $eloqMemo->content, $eloqMemo->created_at);
    }

    /**
     * @param int $memoId
     * @param string $content
     * @return Memo
     */
    public function update(int $memoId, string $content): Memo
    {
        $eloqMemo = EloqMemo::find($memoId);
        $eloqMemo->fill(['content' => $content])->save();
        return new Memo($eloqMemo->id, $eloqMemo->user_id, $eloqMemo->content, $eloqMemo->created_at);
    }

    /**
     * @param int $memoId
     */
    public function delete(int $memoId)
    {
        $eloqMemo = EloqMemo::find($memoId);
        $eloqMemo->delete();
    }
}

MemoQueryServiceInterface
packages/UseCase/Memo/QueryService/MemoQueryServiceInterface.php
<?php

namespace packages\UseCase\Memo\QueryService;

use packages\Domain\Domain\Memo\Memo;
use packages\UseCase\Memo\Dto\MemoDetailDto;
use packages\UseCase\Memo\Dto\MemoEditDto;

interface MemoQueryServiceInterface
{
    public function fetchUsersMemo(int $userId): array;

    public function getMemoDetail(int $memoId): MemoDetailDto;

    public function getEditTarget(int $memoId): MemoEditDto;

    public function findById(int $memoId): Memo;
}
MemoQueryService
packages/Infrastructure/Memo/MemoQueryService.php
<?php

namespace packages\Infrastructure\Memo;

use packages\Domain\Domain\Memo\Memo;
use packages\UseCase\Memo\Dto\MemoDetailDto;
use packages\UseCase\Memo\Dto\MemoEditDto;
use packages\UseCase\Memo\Dto\UsersMemoDto;
use packages\UseCase\Memo\QueryService\MemoQueryServiceInterface;
use App\Models\Memo as EloqMemo;

class MemoQueryService implements MemoQueryServiceInterface
{
    /**
     * @param int $userId
     * @return array
     */
    public function fetchUsersMemo($userId): array
    {
        $eloqMemoList = EloqMemo::where('user_id', $userId)->get()->all();
        $usersMemoDtoList = array_map(function ($eloqMemo){
            return new UsersMemoDto($eloqMemo->id, $eloqMemo->content, $eloqMemo->created_at);
        }, $eloqMemoList);

        return $usersMemoDtoList;
    }

    /**
     * @param int $memoId
     * @return MemoDetailDto
     */
    public function getMemoDetail(int $memoId): MemoDetailDto
    {
        $eloqMemoModel = EloqMemo::find($memoId);
        return new MemoDetailDto($eloqMemoModel->id, $eloqMemoModel->content, $eloqMemoModel->created_at);
    }

    /**
     * @param int $memoId
     * @return MemoEditDto
     */
    public function getEditTarget(int $memoId): MemoEditDto
    {
        $eloqMemoModel = EloqMemo::find($memoId);
        return new MemoEditDto($eloqMemoModel->id, $eloqMemoModel->content);
    }

    /**
     * @param int $memoId
     * @return Memo
     */
    public function findById(int $memoId): Memo
    {
        $eloqMemoModel = EloqMemo::find($memoId);
        $memo = new Memo($eloqMemoModel->id, $eloqMemoModel->user_id, $eloqMemoModel->content,
            $eloqMemoModel->created_at);

        return $memo;
    }
}


Use Cases(とControllers)

この層はControllerからの呼び出しと合わせて説明します。

Use Casesは、アプリケーションができることを表す部分です。
アプリケーションができることというのは、今回でいうと

  • メモの作成
  • メモの更新
  • メモの削除
  • メモの一覧表示
    etc...
    みたいなことを意味します。
    対象となる部分が多いので、永続化処理と参照処理をそれぞれ一つずつ紹介します。他の部分を確認されたい場合は、上に貼ったGitHubのリンクからコードをご覧になってください。

create

メモの新規作成処理です。
引数として渡ってきたパラメーターの値からMemoモデルを作成し、MemoRepositoryのsaveメソッドに渡します。
以下がinterfaceと実装クラスです。

packages/UseCase/Memo/Create/MemoCreateUseCaseInterface.php
<?php

namespace packages\UseCase\Memo\Create;

use packages\Domain\Domain\Memo\Memo;

interface MemoCreateUseCaseInterface
{
    public function create(MemoCreateRequest $request): Memo;
}

packages/Domain/Application/Memo/MemoCreateInteractor.php
<?php

namespace packages\Domain\Application\Memo;

use Carbon\Carbon;
use packages\Domain\Domain\Memo\Memo;
use packages\Domain\Domain\Memo\MemoRepositoryInterface;
use packages\UseCase\Memo\Create\MemoCreateRequest;
use packages\UseCase\Memo\Create\MemoCreateUseCaseInterface;

class MemoCreateInteractor implements MemoCreateUseCaseInterface
{
    private MemoRepositoryInterface $memoRepository;

    /**
     * MemoCreateInteractor constructor.
     * @param MemoRepositoryInterface $memoRepository
     */
    public function __construct(MemoRepositoryInterface $memoRepository)
    {
        $this->memoRepository = $memoRepository;
    }

    /**
     * @param MemoCreateRequest $request
     * @return Memo
     */
    public function create(MemoCreateRequest $request): Memo
    {
        $memo = new Memo(mt_rand(), $request->getUserId(), $request->getContent(), Carbon::now());
        return $this->memoRepository->save($memo);
    }
}

MemoRepositoryのsaveメソッドでは、以下のようにEloquentのモデルを新規作成しDBに保存->Memoモデルを返すという処理を行っています。

MemoRepository
    /**
     * @param Memo $memo
     * @return Memo
     */
    public function save(Memo $memo): Memo
    {
        $eloqMemo = new EloqMemo();

        $eloqMemo->fill(
            [
                'user_id' => $memo->getUserId(),
                'content' => $memo->getContent()
            ])->save();

        return new Memo($eloqMemo->id, $eloqMemo->user_id, $eloqMemo->content, $eloqMemo->created_at);
    }

MemoControllerからは以下のように呼び出しています
バリデーションはFormRequestを使いました

MemoController
    /**
     * 新規作成
     * @param MemoCreateFormRequest $request
     * @param MemoCreateUseCaseInterface $interactor
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    public function create(MemoCreateFormRequest $request, MemoCreateUseCaseInterface $interactor)
    {
        $userId = Auth::user()->id;
        $content = $request->get('content');
        $memoCreateRequest = new MemoCreateRequest($userId, $content);
        $interactor->create($memoCreateRequest);
        return redirect(route('index'));
    }

updateとdeleteも基本的には同じ処理です。渡されたパラメーターからMemoモデルを作成し、Repositoryで永続化処理を行います。
ただ、deleteについては戻り値がありません。何を返すのがベストか正直わかりませんでした。(どうやら戻り値がないのがクリーンとされているらしいです。図らずもクリーンになってしまったのかもしれません)


index

作成したメモの一覧表示です。
パラメーターの値をMemoQueryServiceのfetchUsersMemoに渡して、DBからデータを取得する処理を依頼します。

packages/UseCase/Memo/MemoIndexUseCaseInterface.php
<?php

namespace packages\UseCase\Memo\Index;

interface MemoIndexUseCaseInterface
{
    public function getMemoList(MemoIndexRequest $request): array;
}

MemoQueryServiceのfetchUsersMemoメソッドでは、検索結果の値からUsersMemoDtoというモデルを作成し、その配列を返します。

MemoQueryService
    /**
     * @param int $userId
     * @return array
     */
    public function fetchUsersMemo($userId): array
    {
        $eloqMemoList = EloqMemo::where('user_id', $userId)->get()->all();
        $usersMemoDtoList = array_map(function ($eloqMemo){
            return new UsersMemoDto($eloqMemo->id, $eloqMemo->content, $eloqMemo->created_at);
        }, $eloqMemoList);

        return $usersMemoDtoList;
    }
packages/Domain/Application/Memo/MemoIndexInteractor.php
<?php

namespace packages\Domain\Application\Memo;

use packages\UseCase\Memo\Index\MemoIndexRequest;
use packages\UseCase\Memo\Index\MemoIndexUseCaseInterface;
use packages\UseCase\Memo\QueryService\MemoQueryServiceInterface;

class MemoIndexInteractor implements MemoIndexUseCaseInterface
{
    private MemoQueryServiceInterface $memoQueryService;

    /**
     * MemoIndexInteractor constructor.
     * @param MemoQueryServiceInterface $memoQueryService
     */
    public function __construct(MemoQueryServiceInterface $memoQueryService)
    {
        $this->memoQueryService = $memoQueryService;
    }

    /**
     * @param MemoIndexRequest $request
     * @return array
     */
    public function getMemoList(MemoIndexRequest $request): array
    {
        return $this->memoQueryService->fetchUsersMemo($request->getUserId());
    }
}

UsersMemoDtoは以下のようになっています

UsersMemoDto
<?php

namespace packages\UseCase\Memo\Dto;

class UsersMemoDto
{
    private int $memoId;
    private string $content;
    private \DateTime $createdAt;

    /**
     * UsersMemoDto constructor.
     * @param int $memoId
     * @param string $content
     * @param \DateTime $createdAt
     */
    public function __construct(int $memoId, string $content, \DateTime $createdAt)
    {
        $this->memoId = $memoId;
        $this->content = $content;
        $this->createdAt = $createdAt;
    }

    /**
     * @return int
     */
    public function getMemoId()
    {
        return $this->memoId;
    }

    /**
     * @return string
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }
}

MemoControllerのindexは以下の通りです。

MemoController
    /**
     * 一覧
     * @param MemoIndexUseCaseInterface $interactor
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function index(MemoIndexUseCaseInterface $interactor)
    {
        $userId = Auth::user()->id;

        $memoIndexRequest = new MemoIndexRequest($userId);
        $usersMemoDtoList = $interactor->getMemoList($memoIndexRequest);

        $memoViewModels = array_map(function ($usersMemo){
            return new MemoViewModel($usersMemo->getMemoId(), $usersMemo->getContent());
        }, $usersMemoDtoList);
        $MemoIndexViewModel = new MemoIndexViewModel($memoViewModels);

        return view('index', compact('MemoIndexViewModel'));
    }

返ってきたUsersMemoDtoの配列からMemoViewModelというモデルの配列を作成し、その配列を引数に持たせてMemoIndexViewModelというモデルを生成します。
イメージとしては

//このMemoはEloquentのMemo
Memo::where('user_id', $userId)->get();

で返ってくるCollection => MemoIndexViewModel
そのCollectionの中身のMemoモデル => MemoViewModel
みたいな感じです。

MemoViewModelとMemoIndexViewModelの内容はこのようになっています。

app/Models/MemoViewModel.php
<?php

namespace App\Models;

class MemoViewModel
{
    public int $id;
    public string $content;
    public $createdAt;

    /**
     * MemoViewModel constructor.
     * @param int $id
     * @param string $content
     * @param \DateTime|null $createdAt
     */
    public function __construct(int $id, string $content, \DateTime $createdAt = null)
    {
        $this->id = $id;
        $this->content = $content;
        $this->createdAt = $createdAt;
    }
}

app/Models/Memo/MemoIndexViewModel.php
<?php

namespace App\Models;

class MemoIndexViewModel
{
    public array $memos;

    /**
     * MemoIndexViewModel constructor.
     * @param MemoViewModel[] $memos
     */
    public function __construct(array $memos)
    {
        $this->memos = $memos;
    }

}

viewではEloquentのモデルと同じように展開できます

resources/views/index.blade.php
<table class="table table-hover">
    <tbody>
        @if(!empty($MemoIndexViewModel->memos))
            @foreach($MemoIndexViewModel->memos as $memo)
                <tr>
                    <td>{{$memo->content}}</td>
                    <td>
                        <button type="button" class="btn btn-outline-primary">
                            <a href="{{route('show', ['memoId' => $memo->id])}}">show</a>
                        </button>
                    </td>
                    <td>
                        <button type="button" class="btn btn-outline-primary">
                            <a href="{{route('edit', ['memoId' => $memo->id])}}">edit</a>
                        </button>
                    </td>
                    <td>
                        <form method="POST" action="{{route('delete', ['memoId' => $memo->id])}}">
                            @csrf
                            <button type="submit" class="btn btn-outline-danger">delete</button>
                        </form>
                    </td>
                </tr>
            @endforeach
        @endif
    </tbody>
</table>

テストを書いてみる

メモ作成機能のテスト

//TestBaseでユーザーを作成しています

<?php

namespace Tests\Unit;

use packages\Domain\Application\Memo\MemoCreateInteractor;
use packages\Infrastructure\Memo\MemoRepository;
use packages\UseCase\Memo\Create\MemoCreateRequest;
use Tests\Base\TestBase;

class MemoCreateInteractorTest extends TestBase
{
    public function testMemoCreate()
    {
        $userId = $this->user->id;
        $str = null;
        for ($i = 0; $i < 10; $i++){
            $str .= chr(mt_rand(97, 122));
        }
        $content = $str;
        $repository = new MemoRepository();
        $memoCreateRequest = new MemoCreateRequest($userId, $content);
        $interactor = new MemoCreateInteractor($repository);
        $createdMemo = $interactor->create($memoCreateRequest);

        var_dump($createdMemo);

        $this->assertNotNull($createdMemo);
        $this->assertNotNull($createdMemo->getId());
        $this->assertEquals($userId, $createdMemo->getUserId());
        $this->assertEquals($str, $createdMemo->getContent());
        $this->assertNotNull($createdMemo->getCreatedAt());

    }
}

やってみた所感

今回サンプルを作成してみて思ったこと

  • QueryServiceはそのまま呼び出してもいいのでは
  • 参照系の処理の結果を、専用のDTOに詰めるのか、ドメインモデルに詰めるのか、場合によってどうするのが適切なのかをちゃんと考えないとかなり不便になりそう。
  • ↑も含めて、機械的に同じような実装をしていると詰みそう。しっかり意識しながら実装する必要があるので、そういうクリーンさは体感できたかも(と言いつつ依存の方向とかは正直あまり意識できていない...)
  • viewで使うためのview modelが無限に増殖しそう
  • 共通化できそうな処理をある程度共通化してしまっていいのか悩む。なんとなく、あんまり共通化しすぎない方が変更に強そう

参考にした記事一覧

多いので別記事にまとめました。
https://qiita.com/koyablue/items/e0e8d66803bef789b6bc

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

Laravelでクリーンアーキテクチャを実装するときに参考にしたいリンク集

  • DDDとかクリーンアーキテクチャについて調べるにあたって参考にした記事一覧です
  • Laravelで実装することを目的として調べたので、ほぼPHP/Laravel系の記事です
  • いいのがあれば随時追加予定

"Laravelでクリーンアーキテクチャ" の実装サンプルが見たい場合

実際のコードを見たい人向け

Entityとは何かわからなくなった場合

正直わからないです

Eloquentをクリーンアーキテクチャで使いたくなった場合

「フレームワークに依存しない」の正義に疑問を抱いた人向け

参照系の実装でつらみを感じた場合

一覧表示画面などの実装中に不穏な空気を感じ取った人向け

うっかり "is not instantiable" エラーに出くわした場合

bindし忘れた人向け

(おまけ) シリコンバレーではどうなのか知りたい場合

井の中の蛙は嫌な人向け

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

Laravelでneo4jを使ってみる。プッシュ通知メモ

push通知のライブラリ入れる細かい設定は他サイト参照

composer require laravel-notification-channels/webpush

※以下neo4jで注意する点

デフォルトのモデルはNeoEloquentを使っていないので変更

config/webpush.php
'model' => \App\Models\PushSubscription::class,

モデル

app/Models/PushSubscription.php
<?php

namespace App\Models;


use Vinelab\NeoEloquent\Eloquent\Model as NeoEloquent;

/**
 * @property string $endpoint
 * @property string|null $public_key
 * @property string|null $auth_token
 * @property string|null $content_encoding
 * @property \Illuminate\Database\Eloquent\Model $subscribable
 */
class PushSubscription extends NeoEloquent
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
     protected $label = 'PushSubscriptions'; 
    protected $fillable = [
        'endpoint',
        'public_key',
        'auth_token',
        'content_encoding',
    ];

    /**
     * Create a new model instance.
     *
     * @param  array $attributes
     * @return void
     */
    public function __construct(array $attributes = [])
    {
        if (! isset($this->connection)) {
            $this->setConnection(config('webpush.database_connection'));
        }

        if (! isset($this->table)) {
            $this->setTable(config('webpush.table_name'));
        }

        parent::__construct($attributes);
    }

    /**
     * Get the model related to the subscription.
     *
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function subscribable()
    {
        return $this->morphTo();
    }

    /**
     * Find a subscription by the given endpint.
     *
     * @param  string $endpoint
     * @return static|null
     */
    public static function findByEndpoint($endpoint)
    {
        return static::where('endpoint', $endpoint)->first();
    }
}

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

※学習用メモ デザインパターン:Builder編[PHP]

Builderパターンとはどういうものか

まず最初に、builderは建築業者を意味する言葉です。
今回のパターンは同じ作成過程異なった結果を得るためのものとなります。

考え方として、建物を建てる際に、完成までに必要な要素として「構築の過程」と「素材」があると考えます。
「過程」では、どの順で、どこに何を配置するか、「素材」では、柱や壁、屋根は何で作るか、という形です。

「過程」「素材」それぞれ様々な組み合わせが考えられます。
そこで、「過程」「素材」のそれぞれを個別で用意しておくことで、色々な用途に柔軟に対応できるようになります。

Builderパターンは、このような「過程」を決定するDirectorと呼ばれるものと「素材(表現形式)」を決定するBuilderと呼ばれるものを組み合わせることで、オブジェクトの生成をより柔軟にし、そのオブジェクトの「過程」をもコントロールすることができるようにするためのパターンです。
[参考サイト]https://www.techscore.com/tech/DesignPattern/Builder.html/

Template Methodパターンを用いるメリット/デメリット

  • メリット

    • 複雑なオブジェクトの構築方法をカプセル化できる
    • 複数の素材を用いることで多彩なオブジェクトを生成できる
  • デメリット

    • シンプルな構造のインスタンス生成に適応させた場合、意味もなく工数が増えてしまう
    • どのような情報を渡す必要があるのかクライアント側が認識している必要がある

[参考サイト]http://doriven.hatenablog.com/entry/2014/11/08/040739

サンプルコード

仕様

ミックスドリンクの情報を持つインスタンスを作成する。(今回もいいものが思いつかなかった)
「過程」は「何を作るか」、「素材」は「どれだけの量を作るか、どれだけの濃度とするかの数値」とした(ややわかりづらいかも...)
同じ過程で異なる濃度のハイボールを作成する」を目標に設定

以下を用意します。

  • DrinkBuilderインターフェース

    • 各ドリンクbuilderの動作保証をします。
  • MixDrinkInfoクラス

    • 返すインスタンスの形式を指定します。
  • HighballBuilderクラス

    • ハイボールインスタンスの作成の役割
  • DrinkDirectorクラス

    • 渡されたbuilder(過程)を用いて、指定された溶質や全体量(素材)で作成を行う

では実際に書いてみる

DrinkBuilder.php
interface DrinkBuilder
{
    public function getMixDrink($amount, $concentration);
}

上記インターフェースでインスタンス作成の動作保証を行う。

MixDrinkInfo.class.php
class MixDrinkInfo
{
    private $name;
    private $solute;
    private $amount;
    private $concentration;
    private $soluteAmount;

    public function __construct($name, $solute, $amount, $concentration)
    {
        $this->name = $name;
        $this->solute = $solute;
        $this->amount = $amount;
        $this->concentration = $concentration;
        $this->soluteAmount = $this->calculateSoluteAmount($amount, $concentration);
    }

    // ドリンクの名前を返します。
    public function getName()
    {
        return $this->name;
    }

    // 溶質の名前を返します。
    public function getSolute()
    {
        return $this->solute;
    }

    // ドリンクの量を返します。
    public function getAmount()
    {
        return $this->amount;
    }

    // ドリンクの溶質の濃度を返します。
    public function getConcentration()
    {
        return $this->concentration;
    }

    // ドリンクの溶質の量を返します。
    public function getSoluteAmount()
    {
        return $this->soluteAmount;
    }

    // 溶質の量を計算します。
    private function calculateSoluteAmount($amount, $concentration)
    {
        return $amount * $concentration / 100;
    }


}

と各情報を取得できるものとなるようにする

以下builder

HighballBuilder.class.php
class HighballBuilder implements DrinkBuilder
{
    const DRINK_NAME = 'ハイボール';
    const SOLUTE = 'ウイスキー';

    public function getMixDrink($amount, $concentration)
    {
        // 量および濃度が無い場合はnull
        if (!$amount || !$concentration) {
            return null;
        }

        return new MixDrinkInfo(self::DRINK_NAME, self::SOLUTE, $amount, $concentration);
    }
}

ドリンク名や溶質名(溶媒名も入れるべきですがソーダ割限定という事で。。。())を持たせ、
保持情報はMixDrinkInfoに一任する形とした。
ここまで書いてなぜミックスドリンクでハイボールを例にしているのかと少し後悔

以下director

DrinkDirector.class.php
class DrinkDirector
{
    private $builder;
    private $amount;
    private $concentration;

    public function __construct($builder, $amount, $concentration)
    {
        $this->builder = $builder;
        $this->amount = $amount;
        $this->concentration = $concentration;
    }

    public function getMixDrinkObject()
    {
        $drinkObj = $this->builder->getMixDrink($this->amount, $this->concentration);

        return $drinkObj;
    }
}

Directorクラスで指定されたbuilder(過程)、指定された素材でのオブジェクトの生成を指示し、結果物を返す。

呼び出してみる

$builder = new HighballBuilder();
$amount = 1000;
$concentration = 30;

$director = new DrinkDirector($builder, $amount, $concentration);

$highballObj = $director->getMixDrinkObject();

echo 'ドリンク名:' . $highballObj->getName() . PHP_EOL;
echo '溶質名:' . $highballObj->getSolute() . PHP_EOL;
echo '量:' . $highballObj->getAmount() . 'ml' . PHP_EOL;
echo '濃度:' . $highballObj->getConcentration() . '%' . PHP_EOL;
echo '溶質の量:' . $highballObj->getSoluteAmount() . 'ml' . PHP_EOL;

$concentration = 50;

$director = new DrinkDirector($builder, $amount, $concentration);

$highballObj = $director->getMixDrinkObject();

echo 'ドリンク名:' . $highballObj->getName() . PHP_EOL;
echo '溶質名:' . $highballObj->getSolute() . PHP_EOL;
echo '量:' . $highballObj->getAmount() . 'ml' . PHP_EOL;
echo '濃度:' . $highballObj->getConcentration() . '%' . PHP_EOL;
echo '溶質の量:' . $highballObj->getSoluteAmount() . 'ml' . PHP_EOL;

/*
ドリンク名:ハイボール
溶質名:ウイスキー
量:1000ml
濃度:30%
溶質の量:300ml
ドリンク名:ハイボール
溶質名:ウイスキー
量:1000ml
濃度:50%
溶質の量:500ml
*/

上記のように期待した結果が得られた。

まとめ

  • Builder(過程)、素材の組み合わせによって多彩な形式のオブジェクトを生成できるという柔軟さがある
  • 「過程」が共通のものがあり、なおかつ「素材」もいくつか共通な部分があるような状況で大きな効果を発揮しそうであると感じた。

もう少し多彩な組み合わせがある良い例を思いつくことができればよかった

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

Mac HomebrewでPHPをインストールする

目的

  • MacにHomebrewを使ってPHPを導入する方法をまとめる

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
Homebrew バージョン 7.4.8 Homwbrewを用いて導入

前提条件

  • Homebrewが導入され$ brewコマンドを実行できること。

前提情報

  • 本記事ではPHP 7.4をインストールするがインストール時のバージョン指定で他のバージョンもインストール可能である。

詳細

  1. 下記コマンドを実行してHomebrewを最新状態にする。

    $ brew upgrade
    
  2. 下記コマンドを実行してPHP 7.4をインストールする。

    $ brew install php@7.4
    
  3. 下記コマンドを実行してインストールされたPHPのバージョンを確認する。

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