- 投稿日:2019-11-26T23:36:53+09:00
shell やlinuxでファイル名の特殊文字をエスケープしたい!
タイトルそのままlinuxコマンドで
問題
wc -l /var/tmp/20191126-(2,135,397個)テストファイル.csvのように「20191126-(2,135,397個)ファイル.csv」ファイルの行数を取得しようとしたらエラー
-bash: syntax error near unexpected token `('シンタックスエラーだと。。。
解決策
どうやら「(」が特殊文字でエスケープする必要があるとのこと
ファイル名をシングルクォートで囲めばOK!
wc -l '/var/tmp/20191126-(2,135,397個)テストファイル.csv'PHPであれば、、
$filePath = '/var/tmp/20191126-(2,135,397個)テストファイル.csv' $totalRows = exec("wc -l '${filePath}'");これで正常にできるはず
かんたん
- 投稿日:2019-11-26T23:11:50+09:00
Seleniumで業務のテストを自動化しようと試みた話
この記事はチームラボエンジニアリングアドベントカレンダー4日目の記事です!
はじめに
初めまして、チームラボエンジニアリングの鶴本です。
入社して4ヶ月ほど経ち、少しずついろんなことができるようになってきました。
そんな僕が生意気にも業務のテストの自動化をしようと試みたので記事にしました。何を自動化する?
僕が在籍しているチームでは、弊社が管理しているサイトの保守やバグ改修を主な業務としているのですが、そのサイトのテスト項目に
metaタグが仕様書通りに設定されているか
という項目があります。
そのテストは現状、サイトをブラウザで開いてデベロッパーツールを用いて目視で確認するという、やや非効率的なものであるわけです。
そのテストを自動で行うようにします。どうやって自動化する?
Seleniumの自動化コードをPHPコマンドで実行し、仕様通りにmetaタグが設定されているかどうかをログで出力し確認します。
また今回は、PHPからSeleniumを使用できるphp-webdriver
というライブラリを使用します。環境構築
php-webdriver
を動作させるために環境構築の手順を書きます。作業ディレクトリ作成と移動
mkdir php-selenium cd php-seleniumComposerバイナリをダウンロード(インストール済みの場合は結構です)
curl -sS https://getcomposer.org/installer | phpphp-seleniumインストール
php composer.phar require facebook/webdriver(結構時間かかります。。。)
ChromeのWebDriver設置
ここからダウンロードし、パスの通ったディレクトリに移動します。
※ChromeのWebDriverのバージョンは、インストールされているChromeのバージョンと合わせてください。// パスの通ったディレクトリに移動する mv ~/Downloads/chromedriver /usr/local/bin/selenium-server-standaloneのダウンロード
ここからダウンロードして、作業ディレクトリに移動してください。
これで環境構築は以上です!metaタグのテストの例
2019年11月26日時点で弊社サイトのmeta:titleとmeta:descriptionが下記のようになっているのでそれを仕様とします。
Seleniumで立ち上げたブラウザで下記のようなmetaタグになっているかをテストします。
meta:title meta:title チームラボ / teamLab 最新のテクノロジーを活用したシステムやデジタルコンテンツの開発を行うチームラボは、アーティスト、プログラマ、エンジニア、CGアニメーター、数学者、建築家など、デジタル社会の様々な分野のスペシャリストから構成されているウルトラテクノロジスト集団。 自動化テストのコード
下記のコード
meta_test.php
を作業ディレクトリ配下に移動します。meta_test.php<?php require_once './vendor/autoload.php'; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\Remote\DesiredCapabilities; use Facebook\WebDriver\WebDriverBy; /** * meta:titleとmeta:descriptionのテストを行う */ function start_meta_test() { // チェックするmetaタグを定義する $check_list = array( 'meta_title' => 'チームラボ / teamLab', 'meta_description' => '最新のテクノロジーを活用したシステムやデジタルコンテンツの開発を行うチームラボは、アーティスト、プログラマ、エンジニア、CGアニメーター、数学者、建築家など、デジタル社会の様々な分野のスペシャリストから構成されているウルトラテクノロジスト集団。', ); // server-standalone.jarを実行しているホストのURL $host = 'http://localhost:4444/wd/hub'; // chromeドライバーを起動する $driver = RemoteWebDriver::create($host, DesiredCapabilities::chrome()); // 弊社サイトへアクセスする $driver->get('https://www.team-lab.com/'); // meta:titleが仕様通りか判定する $match_meta_title = $driver->getTitle() === $check_list['meta_title']; echo('meta:title: ' . ($match_meta_title ? 'OK' : 'NG')) . "\n"; try { // xpathから要素取得のWebDriverByクラスのインスタンスを取得する $description_driver_by = WebDriverBy::xpath('/html/head/meta[4]'); // meta:descriptionを取得する $meta_description = $driver->findElement($description_driver_by)->getAttribute('content'); // meta:descriptionが仕様通りか判定する $match_meta_description = $meta_description === $check_list['meta_description']; echo ('meta:decription: ' .($match_meta_description ? 'OK' : 'NG')) . "\n"; } catch (Exception $e) { echo "meta:descriptionを取得できませんでした\n" . $e->getTraceAsString(); } finally { $driver->close(); } } // テストを開始する start_meta_test();テストを実行する
selenium-server-standaloneを立ち上げる
Seleniumを実行するにはselenium-server-standaloneを立ち上げる必要があります。
下記コマンドで立ち上げてください。java -jar selenium-server-standalone-4.0.0-alpha-2.jar※selenium-server-standalone-4.0.0-alpha-2.jarはダウンロードしたバージョンに合わせてください。
下記のようなログが出たら立ち上げ成功です。
java -jar selenium-server-standalone-4.0.0-alpha- 2.jar 22:34:07.185 INFO [GridLauncherV3.parse] - Selenium server version: 4.0.0-alpha-2, revision: f148142cf8 22:34:07.251 INFO [GridLauncherV3.lambda$buildLaunchers$3] - Launching a standalone Selenium Server on po rt 4444 22:34:07.453 INFO [WebDriverServlet.<init>] - Initialising WebDriverServlet 22:34:07.543 INFO [SeleniumServer.boot] - Selenium Server is up and running on port 4444テスト実行
いよいよテスト実行です。PHPコマンドで実行しましょう。
php meta_test.php実行するとテスト結果が表示されました。
コマンド一発でサイトのmetaタグが仕様とあっているかをテストすることができました。meta:title: OK meta:decription: OKまとめ
今回はSeleniumを使用してmetaタグのテストを行いましたが、他にもスクショを撮ったり、フォームの入力をしたりなどいろいろなテストが行えます。
簡単なUIテストであればSeleniumを使用してどんどん自動化したいですね!
- 投稿日:2019-11-26T22:59:08+09:00
【Laravel】VirtualBox+Vagrantで環境構築
こんにちは!!
PHPの始めることになりました!
使用するフレームワークがLaravelです
VirtualBox+Vagrantを使って環境構築したので
今回はVirtualBox+Vagrantを使ってLaravelの環境構築のやり方についてまとめました!!やること
ゼロからlaravel-projectという名前のアプリケーションを作ることにします!
内容は、、
まず、VirtualBox+Vagrantを使ってLaravelの環境構築をします。
その後、laravel-projectという名前のアプリケーションを作ります!
それでは始めましょう!!前提
前提条件が以下になります
ホストOS
macOS(Mojave)
使用するツール
virtual box
仮想環境を作成するツール
vagrant
仮想環境を操作するツール仮想環境を構築しましょう
それでは仮想環境を構築していきましょう
VirtualBox、Vagrantのインストール
下記の公式サイトからVirtualBox、Vagrantダウンロードしてください
ダウンロード後は指示に従いインストールしてください
VirtualBox 公式サイト
https://www.virtualbox.org/wiki/Downloads
Vagrant 公式サイト
https://www.vagrantup.com/
インストール終了後、PCを再起動してくださいHomesteadを追加します
Vagrant boxのlaravel/homesteadを追加します
仮想マシンのOSの種類を追加するイメージですちなみに、laravel/homesteadの他にubuntuやCentOsなどがあります
公式サイトから追加可能なboxを見ることができます
興味ある方は他のboxも追加してみてください!ターミナル$ vagrant box add laravel/homestead追加されたboxは
vagrant box list
で確認できますターミナル 実行結果(例)$ vagrant box list laravel/homestead (virtualbox, 8.2.1) ←これが表示されればOKです! ubuntu/bionic64 (virtualbox, 20190705.0.0)Homesteadの設定
Homesteadの取得
Homesteadの設定に必要なファイルをgitで取ってきます
ついでにvagrantで使用する共有ファイル(laravel
好きな名前でOK)も作成します
共有ファイルについては後ほど説明しますターミナル$ mkdir laravel $ cd laravel $ git clone https://github.com/laravel/homestead.git Homestead $ cd Homestead $ bash init.shHomestead.yamlの編集
Homestead.yamlの一部を下記のように編集します
Homestead.yaml(変更前)folders: - map: ~/Code to: /home/vagrant/Code変更前の意味
ホストOS上の~/Code
ディレクトリを
Vagrant上で/home/vagrant/Code
ディレクトリとして扱うHomestead.yaml(変更後)folders: - map: ~/Desktop/laravel to: /home/vagrant/Code変更後の意味
ホストOS上の~/Desktop/laravel
ディレクトリを
Vagrant上で/home/vagrant/Code
ディレクトリとして扱う仮想環境の立ち上げ
vagrantを立ち上げ、接続しましょう
ターミナル$ cd ~/Desktop/laravel/Homestead $ vagrant up $ vagrant ssh接続後、下記が表示されれば完了です
_ _ _ | | | | | | | |__ ___ _ __ ___ ___ ___| |_ ___ __ _ __| | | '_ \ / _ \| '_ ` _ \ / _ \/ __| __/ _ \/ _` |/ _` | | | | | (_) | | | | | | __/\__ \ || __/ (_| | (_| | |_| |_|\___/|_| |_| |_|\___||___/\__\___|\__,_|\__,_|vagrant上でLaravelのアプリケーションの作成
Laravelのアプリケーションを作っていきましょう
composerをダウンロードする
ターミナル$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" $ php -r "if (hash_file('sha384', 'composer-setup.php') === 'a5c698ffe4b8e849a443b120cd5ba38043260d5c4023dbf93e1558871f1f07f58274fc6f4c93bcfd858c6bd0775cd8d1') { 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.phar
ができれば完了ですcomposerが導入できているか確認しましょう
ターミナル 実行結果$ composer -v ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 1.9.1 2019-11-01 17:20:17プロジェクトを作成しよう
下記がプロジェクトの作成コマンドです
laravel-project
の部分を好きなアプリ名に変更してくださいターミナルcomposer create-project laravel/laravel laravel-project --prefer-distサーバーを立ち上げよう
下記がphpサーバーを立ち上げるコマンドです
ターミナルphp artisan serveサーバーが立ち上がっているか確認しましょう
http://192.168.10.10をクリックして確認してください以上で、ゼロからlaravel-projectという名前のアプリケーションを作成完了です!!
参考記事
Laravel公式サイト
https://readouble.com/laravel/mac+VirtualBox+Vagrant with HomesteadでLaravel
https://vamola.info/programming/mac-laravel/疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!
- 投稿日:2019-11-26T20:42:41+09:00
PHPWeekly 191114 簡単サマリー
PHPWeeklyの内容を簡単に翻訳し、興味のある内容に対してコメントを残します。
知っている知識の中で、ブログを書こうと思っていますが、間違ってる内容が含まれている可能性もあります。
間違っているところに対する指摘はいつも歓迎ですが、
非難・悪口
は求めていません。始まる前に
PHP 7.4が発表され、SymfonyとLaravelにも7.4が適用されました。
ブログを作成しながら知らなかったことを調べたり、補足したりするのにかなり時間がかかりました。
やっぱり勉強し続けないとプログラマーとして衰えることも改めて思いました。記事
PHP 7.4 ガイド
新しく追加された機能を簡単に記録します。
1. タイプ指定が可能になりました。
class User { private int $id; public string $name = "Nico"; public iterable $iterables; protected ClassName $classType; public ?string $void = null; public float $x, $y; var bool $flag; }2. スプレッド演算子
Argument Unpackingとも呼ばれるスプレッド演算子は実は 5.6 バージョンから使用可能でしたが、関数の引数だけ使用可能でした。
PHP 7.4 バージョンからは配列を定義する時から使用可能になりました。
$mammals = ['bear', 'ape']; $animals = ['owl', 'crocodile', ...$mammals, 'frog']; $domain = [...$animals, …$fungus, …$plant];3. アローファンクション
JavaScriptでアローファンクションが楽っていうことをわかりました。
やっとPHPでも使えるようになりました。
$domain = ['bear', 'ape', 'pine', 'lettuce','porcino', 'portobello']; $plurals = array_map(fn($domains) => count($domains), $domains);4. データが NULLの場合の対応演算子
$this->request->data['theAnimalSpecieThatIsTheKingOfTheSavanna'] ??= 'none'; // 위의 문법은 정확히 아래와 같다. $this->request->data['theAnimalSpecieThatIsTheKingOfTheSavanna'] = $this->request->data['theAnimalSpecieThatIsTheKingOfTheSavanna'] ?? 'none';5. Covariant リターン値と Contravariant 引数
説明がまとまらないため、二つに分けて説明します。
5-1. Covariant リターン値
以前には関数をOverrideするとき、リターン値は親クラスのリターンタイプから帰ることができませんでした。
でも7.4バージョンではリターンタイプをサブタイプに変更することができます。
interface Factory { function make(): object; } class UserFactory implements Factory { function make(): User { // implements ... } }5-2. Contravariant 引数
子クラスで関数をOverrideする場合、引数をスーパータイプに変更することができます。
interface Concatable { function concat(Iterator $input) } class Collection implements Concatable { function concat(iterable $input) { // implements ... } }6. Weak References
C#, Java, Pythonには既に実装されているWeak ReferencesがPHPにも実装されました。
Weak Referencesは参照されたオブジェクトが破壊されると、参照したオブジェクトも破壊される参照の形態を意味します。
$obj = new stdClass; $weakref = WeakReference::create($obj); var_dump($weakref->get()); unset($obj); var_dump($weakref->get());7. プリローディング(Preloading)
サービスの速度を改善するために
OPCache
を使用してリクエストされたファイルをOpcodes
に変換した経験があるかと思います。その言葉の中で、
リクエストされた時に変換される
というのはOpcodes
に変換されるのに時間がかかるとのことです。7.4 バージョンではプリローディングを利用し、サーバーが起動されるときに特定の PHP ファイルセットをメモリーにロードすることで返還にかかる時間をなくすことができます。
問題は、一回プリローディングされたファイル変更の追跡ができないことです。
プリローディングされたファイルに変更がある場合、サーバーを再起動する必要があります。
8. 新しいカスタムオブジェクト直列化(Serialization)方法
オブジェクトを直列化する方法として
__sleep()
マジックファンクション、__wakeup()
マジックファンクション、
Serializable
インタフェースを使用する3つの方法があったが、そこには問題がありました。7.4バージョンではこれを解決するため、
__serialize()
と__unserialize()
マジックファンクションを追加しました。class A { private $prop_a; public function __serialize(): array { return ["prop_a" => $this->prop_a]; } public function __unserialize(array $data) { $this->prop_a = $data["prop_a"]; } } class B extends A { private $prop_b; public function __serialize(): array { return [ "prop_b" => $this->prop_b, "parent_data" => parent::__serialize(), ]; } public function __unserialize(array $data) { parent::__unserialize($data["parent_data"]); $this->prop_b = $data["prop_b"]; } }9. Foreign Function Interface: FFI
PHPで C 関数を呼出する時・C データ構造にアクセスするライブラリーがロードできるようになりました。
速度改善の側面ではよいところがあると思いますが、個人的に使うことがあるかと思ったら、あまりないかも...との感想です。
$ffi = FFI::cdef( "int printf(const char *format, ...);", "libc.so.6"); $ffi->printf("Hello %s!\n", "world");10. 数字リテラルセパレーター(Numeric Literal Separator)
PHP 7.4では数字を分けるためにUnderscoreを使用することが可能になりました。
6.674_083e-11; // float 299_792_458; // decimal 0xCAFE_F00D; // hexadecimal 0b0101_1111; // binary11. Reflection for references
なんと翻訳すればいいかわかりません。
翻訳するとしたら参照値を反映するためのもの
くらいですか?以前には2つの変数が同じ内容を参照しているかを確認する作業が結構重い作業でしたが、
PHP 7.4に導入されたReflection for references
を使用すればコストを削減することが可能になりました。12.
mb_str_split()
함수 추가
str_split()
関数は文字列を配列に変換します。
mb_str_split()
は文字をchunks(大きいかたまり)
でパーシングすることができます。print_r(mb_str_split("победа", 2)); --EXPECT-- Array ( [0] => по [1] => бе [2] => да )13. パスワードハッシングレジストリ
password_algos()
関数が追加され、登録済みのパスワードハッシングアルゴリズムのリストを確認することが可能になりました。2019年度 Impact Awards 受賞者発表
ベストツール、ベストフレームワークおよびアプリケーション、ベスト開発者経験、PHPコントリビューター、PHP 関連の会社に対するアワードがありました。
それぞれComposer、Laravel/Symfoy、GitHub、Sara Golemon、Automatticが受賞しました。
2019年ワードプレスで人気のあった5つのポートグラフィーテーマー
WP関連内容はパスします。
ソーシャル戦略をアップグレードすることができる10個のワードプレスのFacebookプラグイン
WP関連内容はパスします。
チュートリアルおよびスキール
ワードプレスカレンダープラグインでコースのスケジュール・タイムテーブルを見せる方法
WP関連内容はパスします。
APIの速度改善のためLaravelでHTTPセッションを無効化する方法
API サーバーではセッションを利用して得られる利点がないし、セッション関連のファイルを探索するためにかなりのコストが必要です。
Laravelでセッションを無効かすることで簡単にAPIサーバーの性能を改善することが可能です。
app/Http/Kernel.php
ファイルのprotected $middlewareGroups
から下記のミドルウェアを削除したら終わりです。\Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class,Laminasへのマイグレーションテスト
Zend FrameworkがLaminasという名に代わりました。
変更点が結構あり、マイグレーションが簡単ではないそうです。
このブログを参考し、マイグレーションをテストしてみるのがおすすめです。
動的分析を使って0個のドキュメントタイプからフルドキュメントタイプ宣言まで
PHPで引数とリターン値のタイプを宣言できるようになったのは多くの方々が知っていると思います。
ドキュメントを作成する為にいろんな方法を使ってきましたが、レガシーコードに対して手をつけることはやっぱり難しいですよね。
このブログでは
TypeCollector::collect($value, __METHOD__, 0)
という関数を利用し、1〜4週間のデータ収集を通じ、引数とリターン値にタイプを追加することを提案しています。タイプ追加をしないといけない状況で不具合を起こさずタイプ追加ができる一つのいい方法ではないかと思います。
あなたのReactをビルドしてみましょう
Reactを使ってない為、パスします。
マルウェアをもっと深く分析する:パート2
マルウェアがどんな方式で動作しているのか分析したブログです。
内容を理解した訳ではないですが、動作原理を少しわかった後気づいたことは
だからネットからいろんなファイルをダウンロードするのが危ないんだ
ということでした。PHPのコードスタイルをコントロールしよう
コード品質を維持する為に使える3つのツールを紹介しました。
ECS、PHPStan、Psalmです。
実は現在僕は一人で作業しているので、今までコード品質・CIなどを全く考えてなかったです。
最近勉強会などを参加しながらコード品質に対して気づき、いろんなことを適用して見ようと思っているので、少しずつプロジェクトに導入することを考えています。
Unitテストから頑張ります。
※ 勉強しなきゃいけないところがが本当に多いですね。
ElasticsearchおよびKibanaを利用して開発環境のSymfonyログをビジュアル化する方法
Symfonyフレームワークを使っている方々にはいいブログかと思います。
Laravelアプリの新しいユーザーにウェルカムノーティフィケーション送信
laravel-welcome-notification ライブラリーを利用して新しいユーザーにウェルカムメールとパスワード再設定を送信する方法に対して書いています。
PHPでjQueryを利用し、複数のテキストボックスを自動完成させる
ユーザー経験を向上させる為のAjaxをどうやって利用するかに関するブログ。
最近はjQueryをしようしていない為、チェックだけしました。
アナウンスメント
おもしろいプロジェクト、ツール、ライブラリー
PHPWeeklyへ紹介されたプロジェクトリストです。
おもしろいと思ったら参加してもよいかと思います。
scarlets: ウェブフレームワーク
arrayy: 配列操作ライブラリー
izend: コンテンツマネージングの為のMVCエンジン
simple-cache: キャッシュサーバーと簡単にインターれくと可能なキャッシュレイヤー
php-iban: 銀行アカウント情報の生成・パーシング・検証・エラーチェックのためのライブラリー
gravity-pdf: メール、pdf ダウンロードフォームを作ってくれるワードプレスプラグイン
pastebin-php: PHPで実装されたpastebin。何かよくわからない。
generators: カスタムLaravelファイルジェネレーター
sncredisbundle: SymfonyのためのRedisバンドル
enhavo: Symfonyで作られたオープンソースCMS
munus: PHPで関数型プログラミングをするためのライブラリー。
slim4-skeleton: Slim4フレームワークのSkeleton。名前通り!
- 投稿日:2019-11-26T20:02:45+09:00
Ajax通信で403エラーになる
Ajax通信が403エラーで返ってきていた原因の調査にとても時間を取られたので、備忘録として残します。
環境
- php: 7.1.30
- Laravel: 5.4.36
- Webサーバー: Apache 2.4
返ってきていたエラー
Ajaxでデータを更新するフォームで、送信したのにうまく更新されていない
->Laravelのログを見てみる
->ログには何も出ていない
->Chromeのコンソールで確認した結果、以下のエラーが返ってきていました。PUT https://xxx/yyy/zzz 403 (Forbidden)Ajax通信が403エラーでうまく動作していません。なぜかアクセス禁止の扱いを受けてしまっています。
調査
- Apacheのエラーログの確認
Laravelのログに何も出ていない->そこまで処理が来ていないということなので、Apacheのエラーログを確認することにしました。(ここまでに結構な時間がかかってしまいました)
Apacheのエラーログを確認すると、以下のエラーが出ていました。
[Thu Nov 21 19:42:57.735794 2019] [authz_core:error] [pid 22270] [client xx.yyy.zzz.nn:12345] AH01630: client denied by server configuration: /home/xxx/public_html/public/xxxxxx, referer: https://xxx/yyy/zzzとりあえず
AH01630: client denied by server configurationのエラー文言で検索してみるも、"アクセス制限の記述方法がApache2.2と2.4で変わっている"系の記事ばかりで、解決に繋がるようなめぼしい情報は見つかりませんでした。
とりあえずApacheの設定をどこか変更するのだろうと、闇雲に設定ファイルの中を覗き回っているうちに、1日が終わってしまいました。
ところで、最初にChromeのコンソールで確認したエラー文言は
- PUTで送信して403のエラー
というものでした。
もしかしてPUTメソッドが禁止されているのでは??
と自分で思いついたわけではなくヒントをいただいたので、該当箇所を確認することに。/etc/httpd/conf.d/userdir.conf <Directory "/home/*/public_html"> AllowOverride FileInfo AuthConfig Limit Indexes Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec Require method GET POST OPTIONS </Directory>↓これ
Require method GET POST OPTIONSPUTは許可されていませんでした。
解決
PUTではなくPOSTを使えばいいということがわかったので、Apacheの設定を変更せずともエラーを解消することができました。
- ルーティングのメソッド変更
Route::put('/xx/{xxx}/xx/{xxx}', 'SomethingApiController@somethingUpdate')↓
Route::post('/xx/{xxx}/xx/{xxx}', 'SomethingApiController@somethingUpdate')
- Ajax時のメソッド変更
type: "PUT",↓
type: "POST",
- 投稿日:2019-11-26T19:47:33+09:00
pre-commit時にphp-cs-fixierを走らせる
作業者それぞれ書き方にブレがあるとウガーってなりますよね。
コミット時にphp-cs-fixierを走らせて強制的にfixするようにします。pre-commitのファイルを作成します
リポジトリの.gitディレクトリの中にあるpre-commit.sampleファイルをリネームコピーします。
cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
pre-commitファイル
pre-commitファイルに下記を記述します。
#!/bin/sh # php-cs-fixerを適用してからコミットします if git rev-parse --verify HEAD >/dev/null 2>&1 then against=HEAD else # Initial commit: diff against an empty tree object against=$(git hash-object -t tree /dev/null) fi # If you want to allow non-ASCII filenames set this variable to true. allownonascii=$(git config --bool hooks.allownonascii) # Redirect output to stderr. exec 1>&2 # 終了コード IS_ERROR=0 # 区切り文字を改行のみにする IFS=$'\n' for FILE in `git diff --cached --name-only --diff-filter=d | grep .php`; do if php -l $FILE; then php-cs-fixer fix $FILE git add $FILE else IS_ERROR=1 fi done if [ $IS_ERROR -eq 1 ] ; then echo syntax error was detected. please correct. fi exit $IS_ERRORfixする対象は作業した(差分のある)ファイルです。
phpのsyntaxなどでエラーが出た場合は別途メッセージが表示されます。
- 投稿日:2019-11-26T19:41:52+09:00
phpに関する備忘録
php操作
phpに関する備忘録です
フォルダー作成
アプリケーション > XAMPP > htdocs
とたどり、任意のフォルダー作成。
ここで作ったフォルダーでphpファイルを管理する。ここでファイルを作成する理由は、
自分のパソコン内ににサーバーを構築していて、
サーバーと自分のパソコンを区別するため。作成したフォルダーをブラウザに表示
フォルダーをテキストエディターで開き、phpファイルを作成。
その中にコードを書いていく。ブラウザに表示する際は、
XAMPPのmanager-osxアプリで Apach web server を起動し、
その後ブラウザで「localhost/任意にファイル名」と入力すると表示される。phpを書く準備
<?php //ここにコードを書いていく ?>phpファイル内にphpのコードしか書かないのであれば、
後半部分の?>は省略する決まりとなっている。
予期せぬエラーが起きる可能性があるため、確実に省略する!!基本構文
ブラウザに表示させる
ブラウザに「Hello world」と表示させる
echo 'Hello world';文字列連結
文字列どうしを連結したい時は、「.」をつけて連結する。
echo 'Hello' . 'world';改行
echo '<br>';演算
和差積商、あまりの計算は他の言語と同じ
変数の宣言
$name = 'yamada';変数の呼び出し
呼び出しには注意が必要で、
シングルコーテーションで括ると文字列として認識されてしまうため、
ダブルコーテーションで括る。echo "$name";この場合、ブラウザにyamadaと表示される。
代入演算子
$name = "yamada"; $name .= "taro";上のように書くと、文字が連結されて、
echo "$name";と呼び出すと、yamadataroと表示される。
配列作成
$fruits = ['banana','peach','grape',];配列に要素を追加させる
//最後に追加する $fruits[] = 'orange'; //100番目に追加 $fruits[100] = 'orange';配列の表示
//全部を表示 var_dump($fruits); //一つだけ表示 var_dump($fruits[0]);連想配列作成
$yamada = ['name' => 'taro', 'age' => 20,'sex' => 'female'];連想配列の呼び出し
//名前を呼び出す var_dump($kuninaka['name']); //年齢を呼び出す var_dump($kuninaka['age']);連想配列の追加
$yamada['hoby'] = 'travel';条件分岐の構文
phpの条件分岐は2つのみ
if else文//elseifはくっつけて書く if(条件式){ //処理内容 } elseif{ //処理内容 }else{ //処理内容 }foreach
foreachは配列や連想配列の表示などに使われる
配列
$isonofamily = ['sazae','katuo','tarao','wakame','masuo','namihei','fune'];呼び出し
構文はforeach(配列名 as 任意の変数名) { echo 任意の変数名; };使用例
foreach($isonfamily as $people) { echo $people.'<br>'; };結果は改行された形で上から順番に表示される。
連想配列
$team = ['okinawa' => 'FC琉球','chiba' => '柏ソレイユ','ibaraki' => '鹿島アントラーズ'];呼び出し構文
foreach($連想配列名 as $任意の変数名1 => $任意の変数名2){ echo $変数名1 . $変数名2; };呼び出し例
foreach($team as $pref => $name){ echo $pref . 'のチームは' . $name . '<br>'; };
- 投稿日:2019-11-26T16:30:24+09:00
phpQueryを使ってWEBスクレイピングを試してみた
PHPでwebスクレイピングを試してみた。
phpQueryというライブラリを使うとjQueryのように要素を指定して簡単にスクレイピングできるみたいなので、サンプルを試してみました。
jQueryのようにセレクタを指定する事ができるので、すごく直観的にスクレイピングしたい要素が取得できます。
※スクレイピングについては自己責任で行ってください。WEBスクレイピングとは
ウェブスクレイピングとは、ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。ウェブ・クローラーあるいはウェブ・スパイダーとも呼ばれる。
出典:WikipediaphpQueryのダウンロード
phpQueryのダウンロードページから最新バージョンをダウンロードします。phpQuery-onefile.phpというファイルがダウンロードされるのでそれを読み込ませます。
実装サンプル
PHP<?php require_once("./phpQuery-onefile.php"); $html = file_get_contents("https://ja.wikipedia.org/wiki/%E3%82%A6%E3%82%A7%E3%83%96%E3%82%B9%E3%82%AF%E3%83%AC%E3%82%A4%E3%83%94%E3%83%B3%E3%82%B0"); //HTMLを全文取得 $dom = phpQuery::newDocument($html); /* * Wikipediaのタイトル・H1タグの中身と、 * 画像の一覧を取得している。 */ //H1タグの取得 $h1 = $dom->find("h1")->text(); echo $h1 . '<br>'; //titleタグの取得 $title = $dom->find("title")->text(); echo $title . '<br>'; //imgタグの一覧を取得 foreach ($dom->find('img') as $img){ $img = $img->getAttribute('src'); echo '<img src=' . $img . '><br>'; } //aタグの一覧を取得 foreach ($dom->find('a') as $a){ $a = $a->getAttribute('href'); echo '<a href=' . $a . '>' . $a . '</a><br>'; }スクレイピングする際の注意点
スクレイピングで検索すると検索候補に「違法」とか「著作権」などの単語が出てきます。
スクレイピング自体には違法性はないですが、収集した情報の使い方を誤ると、著作権法違反となってしまう可能性がるので注意が必要です。
また、実際スクレイピングにより対象のサイトに負荷をかけて偽計業務妨害容疑で逮捕された例もあるようです。スクレイピングは使い方を誤ってしまうと意図せず自分が加害者になってしまう可能性があるので使用には注意が必要です。
参考:岡崎市立中央図書館事件まとめ
ちょっとしたスクレイピングのプログラムであればこのライブラリを使用することで簡単に実装する事ができそうでした。
スクレイピングした結果をCSVに出力するようにしたらもっと便利になりそうです。
- 投稿日:2019-11-26T15:43:17+09:00
PHPでxlsxファイルをPDFに変換するならLibreOfficeしかないな
こちらはJoolen Advent Calendar 2019 2日目の記事です。
前日の記事は@motuoさんのEC-CUBE4系(Symfony)のtwigをAjax( jQuery)を使ってValidationしてみた。でした。
カレンダーのURLはこちら
Joolen Advent Calendar 2019まえがき
最近PHPを使用して、xlsxファイルをPDFに変換する業務がありました。
色々試した結果LibreOfficeに落ち着きました。
その理由を実際に変換した結果とともに書きます。環境
- macOS Mojave
- PHP7.2
- Composer 1.9.1
PDFへの変換方法の選択肢
そもそもPHPでxlsxをPDFに変換できるのか?
そんな疑問から色々ネットの海を泳いだ結果、以下の情報を得ました。
- PhpSpreadsheetとDompdfなどを利用して変換する
- LibreOfficeをインストールして、PHPからコマンドを呼び出す
とりあえずやってみよう!
1. PhpSpreadsheetとDompdfなどを利用して変換する
まずはPHPでExcelをファイルを弄れることで有名なPhpSpreadsheetを使用してみます。
PhpSpreadsheetはDompdfの他にmPDFとTCPDFが対応しているので、全て試します。必要なライブラリをインストール
作業ディレクトリはこんな感じです。
. ├── composer.json ├── composer.lock ├── src └── storage ├── excel └── pdfcomposerで各種ライブラリインストール
$ composer require phpoffice/phpspreadsheet $ composer require dompdf/dompdf $ composer require mpdf/mpdf $ composer require tecnickcom/tcpdfcomposer.jsonを確認
composer.json{ (中略) "require": { "phpoffice/phpspreadsheet": "^1.9", "dompdf/dompdf": "^0.8.3", "mpdf/mpdf": "^8.0" "tecnickcom/tcpdf": "^6.3", } }OK!!
サンプルエクセルファイル作成
私の手元のPCにはエクセルが入ってないので、Google スプレッドシートで以下の納品書のようなものを作成
このファイルを
storage/excel
の中に保存します。. ├── composer.json ├── composer.lock ├── src └── storage ├── excel │ └── delivery-note.xlsx <- 作成したエクセルファイル └── pdfコードを書く
src/xlsx-to-pdf.php<?php require __DIR__ . '/../vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\IOFactory; $orderNote = __DIR__ . '/../storage/excel/order-note.xlsx'; echo "load: $orderNote", PHP_EOL; $reader = IOFactory::createReader('Xlsx'); $spreadsheet = $reader->load($orderNote); // Dompdfを使用してエクスポート outputPdf($spreadsheet, 'Dompdf'); // mPDFを使用してエクスポート outputPdf($spreadsheet, 'Mpdf'); // TCPDFを使用してエクスポート outputPdf($spreadsheet, 'Tcpdf'); function outputPdf($spreadsheet, $writerType) { $writer = IOFactory::createWriter($spreadsheet, ucfirst(strtolower($writerType))); $writer->save(__DIR__ . '/../storage/pdf/' . strtolower($writerType) . '.pdf'); }実行した結果PDFファイルが追加されました。
中身を見てみましょう。Dompdf
mPDF
TCPDF
TCPDFの驚きの白さ。
DompdfとmPDFは文字化けと印刷設定でなんとかなるかもしれないですね。罫線が一部消えてるのが気になりますが。。。
この後文字化け対策でフォント入れようとしたのですが、上手く出来ませんでした。
知っている方いれば教えてください。。。2. LibreOfficeをインストールして、PHPからコマンドを呼び出す
LibreOfficeは無料で使えるオフィスソフトです。
コマンドラインからも色々な操作ができるので、それを利用してxlsxをPDFに変換します。LibreOfficeのインストール
下記のURLからインストールします。
https://ja.libreoffice.org/download/download/Linuxの場合はyumやapt-getにあると思います。
コードを書く
先ほど書いたコードにLibreOfficeを使用して変換する処理を追加します.
src/xlsx-to-pdf.php<?php require __DIR__ . '/../vendor/autoload.php'; use PhpOffice\PhpSpreadsheet\IOFactory; $orderNote = __DIR__ . '/../storage/excel/order-note.xlsx'; echo "load: $orderNote", PHP_EOL; (中略) // LibreOfficeを使用してエクスポート $soffice = '/Applications/LibreOffice.app/Contents/MacOS/soffice'; $command = "$soffice --headless --convert-to pdf --outdir " . __DIR__ . "/../storage/pdf $orderNote"; echo $command, PHP_EOL; exec($command);悪くないですが、高さの自動調整が効いてないですね。
LibreOfficeはシステムフォントを利用するようなので、Linuxでも日本語化は苦ではない。PhpSpreadsheetとLibreOfficeを使用してPDF変換した結果は以上です。
まとめ
- LibreOfficeは高さの自動調整だけ気をつければ大丈夫そう
- 日本語化できるならPhpSpreadsheetも使えるかも
- そもそもPHPでxlsxをPDFに変換するのは向いてない
- どうしてもやるならLibreOfficeが一番楽で綺麗
以上です。
明日は@hwatryさんのTypeScript 雑感(個人的なハマりポイント等)です。
- 投稿日:2019-11-26T15:25:26+09:00
Twig + Bluma でページネーションをやってみる
目的
PHPのテンプレートフレームワークであるTwigとCSSのフレームワークであるBulmaでページネーションをやってみる
コード
PHPでテンプレートを使用する際に下記のデータを指定してください。
- currentPage: 現在のページ番号
- maxPage : 最大ページ番号
- pageRange : ここで指定したページ数分、最初のページ, 最終ページ, 現在ページの前後ページへのリンクを省略せずに表示する。
PHP側
return $this->view->render( $response, 'commitlog.twig', [ 'BASE_PATH' => $this->config['BASE_PATH'], 'commitlogs' => $commitlogs, 'pageLimit' => $limit, 'currentPage' => $page, 'maxPage' => $maxPage, 'pageRange' => 2 ] );ページネーション用のテンプレート
pagination.php<nav class="pagination is-centered" role="navigation" aria-label="pagination"> {% if currentPage != 1 %} <a class="pagination-previous" href="?page={{currentPage - 1}}">Previous</a> {% endif %} {% if currentPage != maxPage %} <a class="pagination-next" href="?page={{currentPage + 1}}">Next page</a> {% endif %} <ul class="pagination-list"> {% set preItemHasEllipsis = false %} {% for i in range(1,maxPage) %} {% if i == currentPage %} <li><a class="pagination-link is-current" aria-lasbel="Goto page {{i}}">{{i}}</a></li> {% set preItemHasEllipsis = false %} {% elseif (i <= 1 + pageRange) or (i >= maxPage - pageRange) or ((currentPage - pageRange <= i) and (i <= currentPage + pageRange)) %} <li><a class="pagination-link" aria-lasbel="Goto page {{i}}" href="?page={{i}}">{{i}}</a></li> {% set preItemHasEllipsis = false %} {% elseif preItemHasEllipsis == false and ( (i == 1 + 1 + pageRange) or (i == maxPage - pageRange - 1) or (i == currentPage - pageRange - 1) or (i == currentPage + pageRange + 1) ) %} <li><span class="pagination-ellipsis">…</span></li> {% set preItemHasEllipsis = true %} {% endif %} {% endfor %} </ul> </nav>読み出し側のテンプレート
// 略 <h1 class="title">コミットログ</h1> <div class="content"> <div class="table-container"> // 略 </div> {{ include("component/pagination.twig")}} // 略メモ
ループは効率が悪そうなので、速度が重要なら分岐でうまいこと作った方がいいと思う。
- 投稿日:2019-11-26T15:25:26+09:00
Twig + bulma でページネーションをやってみる
目的
PHPのテンプレートフレームワークであるTwigとCSSのフレームワークであるBulmaでページネーションをやってみる
コード
PHPでテンプレートを使用する際に下記のデータを指定してください。
- currentPage: 現在のページ番号
- maxPage : 最大ページ番号
- pageRange : ここで指定したページ数分、最初のページ, 最終ページ, 現在ページの前後ページへのリンクを省略せずに表示する。
PHP側
return $this->view->render( $response, 'commitlog.twig', [ 'BASE_PATH' => $this->config['BASE_PATH'], 'commitlogs' => $commitlogs, 'pageLimit' => $limit, 'currentPage' => $page, 'maxPage' => $maxPage, 'pageRange' => 2 ] );ページネーション用のテンプレート
pagination.php<nav class="pagination is-centered" role="navigation" aria-label="pagination"> {% if currentPage != 1 %} <a class="pagination-previous" href="?page={{currentPage - 1}}">Previous</a> {% endif %} {% if currentPage != maxPage %} <a class="pagination-next" href="?page={{currentPage + 1}}">Next page</a> {% endif %} <ul class="pagination-list"> {% set preItemHasEllipsis = false %} {% for i in range(1,maxPage) %} {% if i == currentPage %} <li><a class="pagination-link is-current" aria-lasbel="Goto page {{i}}">{{i}}</a></li> {% set preItemHasEllipsis = false %} {% elseif (i <= 1 + pageRange) or (i >= maxPage - pageRange) or ((currentPage - pageRange <= i) and (i <= currentPage + pageRange)) %} <li><a class="pagination-link" aria-lasbel="Goto page {{i}}" href="?page={{i}}">{{i}}</a></li> {% set preItemHasEllipsis = false %} {% elseif preItemHasEllipsis == false and ( (i == 1 + 1 + pageRange) or (i == maxPage - pageRange - 1) or (i == currentPage - pageRange - 1) or (i == currentPage + pageRange + 1) ) %} <li><span class="pagination-ellipsis">…</span></li> {% set preItemHasEllipsis = true %} {% endif %} {% endfor %} </ul> </nav>読み出し側のテンプレート
// 略 <h1 class="title">コミットログ</h1> <div class="content"> <div class="table-container"> // 略 </div> {{ include("component/pagination.twig")}} // 略メモ
ループは効率が悪そうなので、速度が重要なら分岐でうまいこと作った方がいいと思う。
- 投稿日:2019-11-26T14:12:01+09:00
APOD APIを用いて天文画像をダウンロードする
Q. 天文学・宇宙科学関連の画像がほしい
A. NASAがAPODってサイト持ってますよ → http://apod.nasa.gov/お・わ・り (ゝω・)v
? ... ? ... ? ...
これだけだと寂しいので、画像取得の自動化を目指します。
要旨
この投稿では、NASA APIsおよび、その簡単な利用について書きます。
APODとは
天文画像が毎日掲載されるサイトです。
アーカイブに残っているもっとも古い記事は1995/06/15のもの。老舗です。
https://apod.nasa.gov/apod/lib/about_apod.html
Astronomy Picture of the Day (APOD) is originated, written, coordinated, and edited since 1995 by Robert Nemiroff and Jerry Bonnell. The APOD archive contains the largest collection of annotated astronomical images on the internet.
やりたいこと
- APODが最後に投稿した画像を、クライアントに保存する
- 保存タイミングは、毎日のある時刻、または端末起動時とする
- 過去画像まではこだわらない
用意するもの
APOD API
NASAよりAPIが提供されています。
https://github.com/nasa/apod-api#docs-
Example
requesthttps://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&start_date=2017-07-08&end_date=2017-07-10return[ { "copyright": "T. Rector", "date": "2017-07-08", "explanation": "Similar in size to large, bright spiral galaxies in our neighborhood, IC 342 is a mere 10 million light-years distant in the long-necked, northern constellation Camelopardalis. A sprawling island universe, IC 342 would otherwise be a prominent galaxy in our night sky, but it is hidden from clear view and only glimpsed through the veil of stars, gas and dust clouds along the plane of our own Milky Way galaxy. Even though IC 342's light is dimmed by intervening cosmic clouds, this sharp telescopic image traces the galaxy's own obscuring dust, blue star clusters, and glowing pink star forming regions along spiral arms that wind far from the galaxy's core. IC 342 may have undergone a recent burst of star formation activity and is close enough to have gravitationally influenced the evolution of the local group of galaxies and the Milky Way.", "hdurl": "https://apod.nasa.gov/apod/image/1707/ic342_rector2048.jpg", "media_type": "image", "service_version": "v1", "title": "Hidden Galaxy IC 342", "url": "https://apod.nasa.gov/apod/image/1707/ic342_rector1024s.jpg" }, /* 省略 */ ]
api_key=DEMO_KEY
にてすぐにAPIを使えるようですが、下記のとおり回数制限があるようです。DEMO_KEY Rate Limits
- Hourly Limit: 30 requests per IP address per hour
- Daily Limit: 50 requests per IP address per day
必要であれば、同ページのフォームにて、API Key を生成してくださいませ。
cURLするスクリプト
APIの戻りJSONをもとに、画像をクライアントに保存するスクリプトを書きます。
apod.php<?php $url_format = 'https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&start_date=%s'; $saving_directory = './'; $timezone_area = 'America/New_York'; date_default_timezone_set($timezone_area); $url = sprintf($url_format, date('Y-m-d')); $resource = curl_init(); curl_setopt($resource, CURLOPT_HEADER, false); curl_setopt($resource, CURLOPT_RETURNTRANSFER, true); curl_setopt($resource, CURLOPT_BINARYTRANSFER, true); curl_setopt($resource, CURLOPT_URL, $url); curl_setopt($resource, CURLOPT_SSLVERSION,1); $json = curl_exec($resource); // 成功例 // $json = '[{"date":"2019-11-24","explanation":"Is this image art? 50 years ago, Apollo 12 astronaut-photographer Charles \"Pete\" Conrad recorded this masterpiece while documenting colleague Alan Bean\'s lunar soil collection activities on Oceanus Procellarum. The featured image is dramatic and stark. The harsh environment of the Moon\'s Ocean of Storms is echoed in Bean\'s helmet, a perfectly composed reflection of Conrad and the lunar horizon. Works of photojournalists originally intent on recording the human condition on planet Earth, such as Lewis W. Hine\'s images from New York City in the early 20th century, or Margaret Bourke-White\'s magazine photography are widely regarded as art. Similarly many documentary astronomy and space images might also be appreciated for their artistic and esthetic appeal.","hdurl":"https://apod.nasa.gov/apod/image/1911/BeanConrad_Apollo12_950.jpg","media_type":"image","service_version":"v1","title":"Apollo 12: Self-Portrait","url":"https://apod.nasa.gov/apod/image/1911/BeanConrad_Apollo12_960.jpg"}]'; // 失敗例 // $json = '{"code":400,"msg":"Date must be between Jun 16, 1995 and Nov 24, 2019.","service_version":"v1"}'; if ($json === false) { exit(1); } $apod_pages = json_decode($json, true); if (isset($apod_pages['code'])) { exit(1); } $apod_page = current($apod_pages); $image_url = $apod_page['hdurl']; $image_url_array = explode('/', $image_url); $image_name = end($image_url_array); $saving_file = $saving_directory . $image_name; clearstatcache(true, $saving_file); if (file_exists($saving_file)) { exit(0); } $saving_file_resource = fopen($saving_file, 'wb'); curl_setopt($resource, CURLOPT_URL, $image_url); curl_setopt($resource, CURLOPT_FILE, $saving_file_resource); curl_exec($resource); curl_close($resource); fclose($saving_file_resource);起動設定
たとえばWindows機であれば、
- タスクスケジューラ
- スタートアップ
を設定し、適当なタイミングでスクリプトを動作させて、画像をダウンロードするようにします。
実施結果
基本的に毎日、指定したフォルダに新しい画像が増えるようになります。
楽しい!
おわりに
この投稿では、NASA APIsを使ってAPODから画像をダウンロードしました。
デスクトップの背景やスクリーンセーバが華やかになりますので、個人利用で楽しんでみてはいかがでしょうか。
それではごきげんよう。
- 投稿日:2019-11-26T11:25:58+09:00
#wordpressを知らないPHPエンジニアあるある
ツイッターで #wordpressを知らないPHPエンジニアあるある を書いているのですが、ツイッターだと流れてしまうので纏めています。
#WordPressを知らないPHPエンジニアあるある
— Fumito Mizuno@ WordPressユーザーのためのPHP入門 はじめから、ていねいに (@ounziw) October 14, 2019
javascriptなどをheader.phpで呼び出している
→ wp_enqueue_script を使いましょう。#wordpress #wordpressjp #php #javascript#WordPressを知らないPHPエンジニアあるある#CSRF 対策に自分でnonceを実装している
— Fumito Mizuno@ WordPressユーザーのためのPHP入門 はじめから、ていねいに (@ounziw) October 22, 2019
→settings_apiを使いましょう#WordPressjp #セキュリティ#WordPressを知らないPHPエンジニアあるある
— Fumito Mizuno@ WordPressユーザーのためのPHP入門 はじめから、ていねいに (@ounziw) November 2, 2019
sql文を書いて、データベースに直接アクセスしている
→ WP_Queryを使いましょう#wordpress #wordpressjp #php #mysql#WordPressを知らないPHPエンジニアあるある
— Fumito Mizuno@ WordPressユーザーのためのPHP入門 はじめから、ていねいに (@ounziw) November 17, 2019
キャッシュ機能を独自実装している
→ transient APIを使いましょう#wordpressを知らないPHPエンジニアあるある
— Fumito Mizuno@ WordPressユーザーのためのPHP入門 はじめから、ていねいに (@ounziw) November 25, 2019
1時間を3600、1日を86400と書いている
→ HOUR_IN_SECONDS や DAY_IN_SECONDS を使いましょう。#wordpressjp #php
- 投稿日:2019-11-26T10:51:06+09:00
wordpress 構築のちょっとした話
案件で、初めてwordpressの構築を任せていただきました。勉強になることばかりで楽しんでいます。
自分の整理がてら、学んだことをまとめてみます。関数化
例えば、カスタム投稿でACFのデータを取得する際は、なるべく細かく関数化して分けておく。そして、作った関数たちを組み合わせる。
関数化しておくと、再利用ができるし、同じフィールドの値に関して統一性ができてスッキリするので、便利とのこと。例えば、カスタム投稿ページにて投稿記事一つを出力する場合。
記事のタイトル
の他にテキスト1
とテキスト2
の値があったとする。custom-post.php<?php // テキスト1を取得する function get_post_text1 () { if ($text1 = get_field('text_field1')) { return '<p>' . $text1 . '</p>'; } } // テキスト2を取得する function get_post_text2 () { if ($text2 = get_field('text_field2')) { return '<p>' . $text2 . '</p>'; } } function get_new_post () { echo '<p>このページでは記事を紹介します。</p>' the_title(); // タイトルの出力 echo get_post_text1(); // text_field1の値を出力 echo get_post_text2(); // text_field2の値を出力 }引数
また、
the_title();
は引数に、値の前後に出力したい値を指定することでマークアップごとに出力できる。<?php echo the_title('タイトルの前に表示したいテキスト', 'タイトルの後に表示したいテキスト', 'boolean:出力するかどうか');これを利用することで、同じ値でもマークアップを変更して出力することができる。
例えば、アーカイブページと記事詳細ページで、同じ値だけどクラス名が異なっているときに使用する。function get_post_text1 ($before_html, $after_html) { if ($text1 = get_field('text_field1')) { return $before_html . $text1 . $after_html; } } // archive.php echo get_post_text1('<p class="archive__description>"', '</p>'); // single.php echo get_post_text1('<p class="single__description -strong>"', '</p>');あとは、引数に
get_the_ID();
を指定することで、関数内とか、ループ内でも自由にデータとってくることができるのかな〜。
この辺りが、まだわかっていないのでまた理解したらまとめよ。
- 投稿日:2019-11-26T10:39:13+09:00
【Laravel開発ノート】ドメインを設定するポイント
1.目的
ローカル環境で「http://192.168.10.10/」→「http://weibo.test」と表示したい。
2.設定のポイント
2.1 「.env」ファイル
APP_DEBUG=true APP_URL=http://weibo.test2.2 「homestead.yaml」ファイル
sites: - map: weibo.test # <--- ここ! to: /home/vagrant/code/Laravel/public2.3 「/etc/hosts」ファイル
$ vim /etc/hosts最後の一行に以下の内容を追加
192.168.10.10 weibo.test
- 投稿日:2019-11-26T09:51:56+09:00
AndroidでPHPにjsonを送受信する
Androidでjsonを送受信する
この記事は調べながら作ったサンプルを自分用にまとめたものです。
AndroidでPHPにjsonを送受信するアプリを作ります。
例として、Androidで入力した文字列二つをPHPに送り、PHPで受け取った文字列を連結した後Androidに送り返します。
完成したアプリは以下のようになります。Androidのコード
layoutのidは
- 二つのEditTextはeditText1とeditText2
- ボタンはsubmit
- 結果の表示TextはtextResult
としたときのMainActivityは以下のようになります。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) submit.setOnClickListener { HitAPITask().execute("http://10.0.2.2/test/apistring.php") } } inner class HitAPITask : AsyncTask<String, String, String>() { override fun doInBackground(vararg params: String?): String? { var connection: HttpURLConnection? = null var reader: BufferedReader? = null var buffer: StringBuffer try { val url = URL(params[0]) connection = url.openConnection() as HttpURLConnection //送信するための設定 connection.requestMethod = "POST" connection.setInstanceFollowRedirects(false) connection.setDoOutput(true) connection.setDoInput(true) //送信する文字列の取得 val text1 = findViewById<EditText>(R.id.editText1).text.toString() val text2 = findViewById<EditText>(R.id.editText2).text.toString() //送信するjsonの作成 val json = "{\"text1\":\"" + text1 + "\",\"text2\":\"" + text2 + "\"}" //再び送信するための設定 connection.setRequestProperty("Accept-Language", "jp") connection.setRequestProperty("Content-Type", "application/json; charset=utf-8") //ostreamへjsonを書き込む val os = connection.getOutputStream() val ps = PrintStream(os) ps.print(json) ps.close() //Apiから返ってきたjsonの処理 val stream = connection.getInputStream() reader = BufferedReader(InputStreamReader(stream, "UTF-8")) buffer = StringBuffer() var line: String? while (true) { line = reader.readLine() if (line == null) { break } buffer.append(line) Log.d("CHECK", buffer.toString()) } //jsonから結果を取り出す val jsonText = buffer.toString() val jsonObject = JSONObject(jsonText) val result = jsonObject.getString("result") return result //例外が起こった時の処理 } catch (e: MalformedURLException) { e.printStackTrace() } catch (e: IOException) { e.printStackTrace() } catch (e: JSONException) { e.printStackTrace() } finally { connection?.disconnect() try { reader?.close() } catch (e: IOException) { e.printStackTrace() } } //例外が起こった時の返り値 return null } override fun onPostExecute(rtn: String?) { super.onPostExecute(rtn) if (rtn == null) rtn textResult.text = rtn } } }ポイント
- HTTP通信にはAsyncTaskで非同期通信する。
- jsonを送信するにはsetRequestPropertyを使う。phpで$_GETを使って受け取るにはapplication/jsonの部分を変える必要があるはず。最初はこれをやりたかったがうまくいかなかった。
connection.setRequestProperty("Accept-Language", "jp") connection.setRequestProperty("Content-Type", "application/json; charset=utf-8")
- 送信にはgetOutputStreamでostreamにjsonを書き込む
val os = connection.getOutputStream() val ps = PrintStream(os) ps.print(json) ps.close()
- 通信するためにはpermissionで許可をしなければならない。
AndroidManifest.xml<uses-permission android:name="android.permission.INTERNET"/>+エミュレータでApacheのサーバのPHPと通信する。その場合URLはlocalhostではなくhttp://10.0.2.2となる。
さらに、10.0.2.2との通信に許可が必要なので、newtwork_security_config.xmlを用意する。
AndroidManifest.xmlにSecurityConfigでnewtwork_security_config.xmlを使うことを教えておく。AndroidManifest.xml<application android:networkSecurityConfig="@xml/network_security_config"/>newtwork_security_config.xml<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="false">10.0.2.2</domain> </domain-config> </network-security-config>PHPのコード
apistring.php<?php $json = file_get_contents('php://input'); $array = json_decode($json); $a = $array->text1; $b = $array->text2; $sum = $a . $b; //$sumsum = 3; $ary = array('result'=>$sum); echo json_encode($ary); ?>ポイント
- POSTではなくfile_get_contents('php://input')でjsonを受け取る。
最後に
理解せずに書いている部分も多いので少しずつ勉強していこう。
参考文献
Android Httpでデータ取得
AndroidでのHTTP通信
AndroidからAPIを叩いてJSON取って中身を表示させるまで
JSON形式でPOSTされたデータの受信方法
【PHP】JSONデータのPOST受け取りで application/x-www-form-urlencoded とapplication/json の両方に対応
- 投稿日:2019-11-26T00:41:52+09:00
【Laravel】メール送信失敗時のログを残す
Laravel で複数のメールアドレスへの送信処理を書いた時、メール送信が失敗したアドレスの一覧を取得したかったが、ドキュメントには載っていなかったのでメモとして残しておく。
環境
Laravel 6.X
やり方
Mail::failures()
で送信失敗メールアドレスが配列で取得できる。(参考)
例えば、Mailable クラスの設定後、送信処理をコントローラに書く場合、下記のような感じ。MailSendController.phpnamespace App\Http\Controllers; use App\Mail\SampleMailableClass; use Illuminate\Support\Facades\Mail; use Illuminate\Http\Request; use Log; class MailSendController extends Controller { public function send() { $emails = ['hoge@example.com', 'huga@example.com'] Mail::to($emails)->send(new SampleMailableClass()); if (count(Mail::failures()) > 0) { Log::channel('slack')->warning(Mail::failures()); }; } }Slack へのログ投稿の方法は こちらの記事 に詳しく書かれていました。
参考
- 投稿日:2019-11-26T00:23:57+09:00
Laravel6でRedisとSSE(Server-Sent Events)を使った簡易チャットを作成してみた
私はLaravel初心者です。基本的にググりながら書いています。よろしくお願いします。
Laravelで簡単にチャットを作ってみようと思ったのですが、1秒ごとにDBにアクセスするようなのは避けたかったので、Redisでやってみるといいんじゃないかと思い、作成してみたので書かせていただきます。m(_ _)m
Redisを使ってチャットをするというやり方はこちらが参考です。
RedisとServer Sent EventでJavaScriptでチャットを作ってみた:電脳ヒッチハイクガイド:電脳空間カウボーイズZZ(電脳空間カウボーイズ)
このような簡単なチャットを作成します
自動生成ファイルもありますが、自分が最終的に変更したのは↓です
https://github.com/okumurakengo/laravel-sse-chat/pull/1/files
1. Homesteadで環境構築しました
参考:Laravel Homestead - Laravel - The PHP Framework For Web Artisans
vagrant@homestead:~$ pwd /home/vagrant vagrant@homestead:~$ composer create-project --prefer-dist laravel/laravel code #laravelのプロジェクト作成 /* 省略 */ vagrant@homestead:~$ cd code #laravelのルートに移動
/home/vagrant/code/public
の内容が読み込まれて、Laravelの最初のページを表示できました。
Laravelのバージョンは6.2です。2. コントローラー、ビュー作成
2-1. ルートを定義する
web.php
に1行追加routes/web.phpRoute::get('/chat', 'ChatController@index');2-2. コントローラー作成
$ php artisan make:controller ChatController #app/Http/Controllers/ChatController.php が作成されるコントローラーで画面が表示されるように変更します
app/Http/Controllers/ChatController.php<?php namespace App\Http\Controllers; use Illuminate\View\View; use Illuminate\Http\Request; class ChatController extends Controller { /** * チャット画面表示 * * @return View */ public function index(): View { return view('chat'); } }2-3. bladeファイル作成
chat.blade.php
を作成resources/views/chat.blade.php<!DOCTYPE html> <meta charset=UTF-8> <title>Document</title> <h1>Simple Chat</h1>
これで
/chat
にアクセスしてhtmlを表示することができますここまでの変更分はこのようになりました
https://github.com/okumurakengo/laravel-sse-chat/commit/b4792d091f8279d00a403ea7303b2ede9ddd8f17
3. vuejsを使えるようにする
参考:JavaScript & CSS Scaffolding - Laravel - The PHP Framework For Web Artisans
3-1. vueの足場(雛形)を作成
$ composer require laravel/ui --dev #vueの足場(雛形)を作成するために、laravel/uiをインストールする $ php artisan ui vue #vueの足場(雛形)を作成
php artisan ui vue
実行後は↓のような差分になりましたhttps://github.com/okumurakengo/laravel-sse-chat/commit/5c46d127f3fed00b252e32e75c4c8523ed0677ec
3-2. vueのコンポーネントを画面に表示する
自動生成された
ExampleComponent.vue
を表示してみます
app.js
とchat.blade.php
を変更resources/js/app.jsconst app = new Vue({ el: "#app", + template: '<example-component />', });
resources/views/chat.blade.php<!DOCTYPE html> <meta charset=UTF-8> <title>Document</title> + <script src={{ mix('js/app.js') }} defer></script> <h1>Simple Chat</h1> + <div id=app></div>$ yarn #ライブラリなどをインストール $ yarn dev #js、cssをビルドしてpublic/配下に出力。yarn watchとしたら変更を監視してくれるvueのコンポーネントを画面に表示することができました。
yarn dev
のビルドした出力結果も含まれていますが、ここで変更したコミットは以下です。
※今回は特に気にしませんでしたが、app.js
などの自動生成ファイルは.gitignore
に設定しましょうhttps://github.com/okumurakengo/laravel-sse-chat/commit/5ee0df386ceb298331d382ba2dafe3bc03afd7a9
4. チャットの画面を作成
app.js
でApp.vue
を読み込むように変更resources/js/app.jsVue.component('example-component', require('./components/ExampleComponent.vue').default); + Vue.component('App', require('./components/App.vue').default); const app = new Vue({ el: "#app", - template: '<example-component />', + template: '<App />', });
resources/js/components/App.vue
を作成resources/js/components/App.vue<template> <div id="app" class="container"> <div class="flex"> <div class="users"> <select v-model="selectUser"> <option v-for="user in users"> {{ user }} </option> </select> </div> <div class="chat"> </div> </div> <form> <input> <input type="submit" value="送信"> </form> </div> </template> <script> const users = ['Bob', 'Alice', 'Carol']; export default { data() { return { users, selectUser: users[0], } }, } </script> <style lang="scss" scoped> #app { font-family: Verdana; &.container { width: 500px; padding: 10px; background: #eee; .users { width: 30%; height: 300px; border-right: 1px solid gray; } .chat { width: 70%; height: 300px; padding: 10px; overflow: scroll; p { margin: 0; } } } .flex { display: flex; } } </style>ここでの変更、※
public/js/app.js
はyarn dev
かyarn watch
で生成されたファイルhttps://github.com/okumurakengo/laravel-sse-chat/commit/85f2216270110b2075b26a248c45cf2794812747
5. テーブル、モデル作成
チャットの内容をMySQLにも保存します
5-1. MySQL接続の設定
参考
【Laravel Homestead - Laravel - The PHP Framework For Web Artisans】
https://laravel.com/docs/6.x/homestead#connecting-to-databasesuser:homestead
password:secret
でMySQLに接続できるので、.env
を変更します.env- DB_CONNECTION=mysql - DB_HOST=127.0.0.1 - DB_PORT=3306 - DB_DATABASE=laravel - DB_USERNAME=root - DB_PASSWORD= + DB_CONNECTION=mysql + DB_HOST=127.0.0.1 + DB_PORT=3306 + DB_DATABASE=homestead + DB_USERNAME=homestead + DB_PASSWORD=secret5-2. テーブル作成
migrationファイル作成
$ php artisan make:migration create_chat_table --table chat Created Migration: 2019_12_11_145549_create_chat_table以下のように変更
database/migrations/2019_12_11_145549_create_chat_table.php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateChatTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('chats', function (Blueprint $table) { $table->increments('id'); $table->string('user'); $table->string('post'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('chats'); } }
php artisan migrate
を実行する。※チャットのテーブル以外にもデフォルトであるマイグレーションファイルも実行される$ php artisan migrate Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (0.08 seconds) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (0.12 seconds) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (0.18 seconds) Migrating: 2019_12_11_145549_create_chat_table Migrated: 2019_12_11_145549_create_chat_table (0.2 seconds)
chats
テーブルができていればOK5-3. モデル作成
$ php artisan make:model Chat
生成された
app/Chat.php
を変更app/Chat.php<?php namespace App; use Illuminate\Database\Eloquent\Model; class Chat extends Model { protected $fillable = ['user', 'post']; }ここでの変更のコミットです
https://github.com/okumurakengo/laravel-sse-chat/commit/f8d4b20e68d10b3a7372165df761fef991466b73
6. チャットの登録をする
チャットの入力フォームからサブミットすると、ajaxでapiにデータを送るように変更します
resources/js/components/App.vue<template> <div id="app" class="container"> <div class="flex"> <div class="users"> <select v-model="selectUser"> <option v-for="user in users"> {{ user }} </option> </select> </div> <div class="chat"> </div> </div> - <form> - <input> - <input type="submit" value="送信"> - </form> + <form @submit.prevent="addPost"> + <input v-model="textValue"> + <input type="submit" value="送信"> + </form> </div> </template> <script> const users = ['Bob', 'Alice', 'Carol']; export default { data() { return { users, selectUser: users[0], + textValue: '', } }, + methods: { + async addPost() { + if (!this.textValue.trim()) { + return + } + await axios.post('/api/chat/add', { user: this.selectUser, post: this.textValue }) + this.textValue = '' + }, + }, } </script> // ...
api.php
に追加routes/api.php// ... Route::post('/chat/add', 'Api\\ChatController@add');Controllersフォルダに新たに
Api
フォルダを作成し、そこに新しくChatController.php
を作成します
add
メソッドで、MySQLとRedisにデータを保存します。app/Http/Controllers/Api/ChatController.php<?php namespace App\Http\Controllers\Api; use App\Chat; use Illuminate\View\View; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Redis; class ChatController extends Controller { /** * チャット保存 * * @param Request $request * @return JsonResponse */ public function add(Request $request): JsonResponse { $chat = Chat::make([ 'user' => $request->get('user'), 'post' => $request->get('post'), ]); $chat->save(); Redis::set('latest_created_at', $chat->created_at->toDateTimeString()); return response()->json(['result' => 'ok']); } }この状態でチャットで送信してみると
データを保存することができました
ここでのコミットです
https://github.com/okumurakengo/laravel-sse-chat/commit/3002bf6db1c1f0fc23b6c4ef1f154e1d8d2a4b27
7. チャットの一覧取得
チャットで別の人が打ち込んだ内容を取得するためにServer Sent Eventsを使って取得します。
Server Sent Events(SSE)とはサーバーからプッシュ通知を受信するためのHTTP接続をしてくれるAPIです。
よくwebsocketと比較されます、websocketはブラウザとサーバーで双方向通信できるが、SSEはサーバーからブラウザへの一方向の通信を行います。参考 : Server Sent Events using Laravel and Vue
created()
でServer Sent Eventsを使ってHTTP接続を開き、接続しっぱなしにしてくれます。resources/js/components/App.vue<template> <div id="app" class="container"> <div class="flex"> <div class="users"> <select v-model="selectUser"> <option v-for="user in users"> {{ user }} </option> </select> </div> - <div class="chat"> - </div> + <div class="chat" ref="chat"> + <div v-for="({ user, post, created_at }) in posts"> + <p><strong>{{ user }}</strong> <small>{{ created_at }}</small></p> + <p>{{ post }}</p> + <hr> + </div> + </div> </div> <form @submit.prevent="addPost"> <input v-model="textValue"> <input type="submit" value="送信"> </form> </div> </template> <script> const users = ['Bob', 'Alice', 'Carol']; export default { data() { return { users, selectUser: users[0], textValue: '', + posts: [], } }, + created() { + const es = new EventSource('/api/chat/event'); + es.addEventListener('message', e => { + const { posts } = JSON.parse(e.data) + if (posts.length) { + this.renderList(posts) + } + }); + }, methods: { async addPost() { if (!this.textValue.trim()) { return } await axios.post('/api/chat/add', { user: this.selectUser, post: this.textValue }) this.textValue = '' }, + renderList(posts) { + this.posts = [...this.posts, ...posts] + // 下に追加したのでスクロールする + this.$nextTick(() => this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight) + }, }, } </script> // ...
api.php
に追加routes/api.php// ... Route::post('/chat/add', 'Api\\ChatController@add'); + Route::get('/chat/event', 'Api\\ChatController@event');
Server Sent EventsでJSONを返すようにします
app/Http/Controllers/Api/ChatController.phpuse App\Chat; use Carbon\Carbon; use Illuminate\View\View; use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; use Illuminate\Support\Facades\Redis; use Symfony\Component\HttpFoundation\StreamedResponse; // ... /** * チャット一覧取得 * * @return StreamedResponse */ public function event(): StreamedResponse { // 最近のチャット5件と、一番最近のcreated_atを取得 $chats = Chat::orderBy('created_at', 'desc')->limit(5)->get()->sortBy('created_at')->values(); $tmpLatestCreatedAt = optional($chats->last())->created_at ?? Carbon::minValue(); $response = new StreamedResponse(function() use ($chats, $tmpLatestCreatedAt) { printf("data: %s\n\n", json_encode(['posts' => $chats])); ob_flush(); flush(); while(true) { $latestCreatedAt = is_null(Redis::get('latest_created_at')) ? Carbon::minValue() : Carbon::parse(Redis::get('latest_created_at')); if ($latestCreatedAt->gt($tmpLatestCreatedAt)) { // チャットに更新があった場合はテーブルから取得 $latestChats = Chat::where('created_at', '>', $tmpLatestCreatedAt)->orderBy('created_at', 'asc')->get(); $tmpLatestCreatedAt = $latestCreatedAt; } echo 'data: ' . json_encode(['posts' => $latestChats ?? []]) . "\n\n"; ob_flush(); flush(); $latestChats = null; sleep(1); } }); $response->headers->set('Content-Type', 'text/event-stream'); $response->headers->set('X-Accel-Buffering', 'no'); $response->headers->set('Cach-Control', 'no-cache'); return $response; } // ...チャットの内容を更新に合わせてリアルタイムに取得できました。
ここでの変更です
https://github.com/okumurakengo/laravel-sse-chat/commit/7515b4cd22c67d4a074166b51655688673b66668
8. 「
Bobが入力中です
」と表示させるresources/js/components/App.vue<template> <div id="app" class="container"> <div class="flex"> <div class="users"> <select v-model="selectUser"> <option v-for="user in users"> {{ user }} </option> </select> </div> <div class="chat" ref="chat"> <div v-for="({ user, post, created_at }) in posts"> <p><strong>{{ user }}</strong> <small>{{ created_at }}</small></p> <p>{{ post }}</p> <hr> </div> </div> </div> <form @submit.prevent="addPost"> - <input v-model="textValue"> + <input v-model="textValue" @keyup="typing"> <input type="submit" value="送信"> + {{ typingMessage }} </form> </div> </template> <script> const users = ['Bob', 'Alice', 'Carol']; export default { data() { return { users, selectUser: users[0], textValue: '', posts: [], + typingUsers: [], } }, created() { const es = new EventSource('/api/chat/event'); es.addEventListener('message', e => { - const { posts } = JSON.parse(e.data) + const { posts, typing_users: typingUsers = [] } = JSON.parse(e.data) if (posts.length) { this.renderList(posts) } + if (!_.isEqual(this.typingUsers, typingUsers)) { + this.typingUsers = typingUsers + } }); }, methods: { async addPost() { if (!this.textValue.trim()) { return } await axios.post('/api/chat/add', { user: this.selectUser, post: this.textValue }) this.textValue = '' }, renderList(posts) { this.posts = [...this.posts, ...posts] // 下に追加したのでスクロールする this.$nextTick(() => this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight) }, + typing: _.throttle(async function () { + await axios.post('/api/chat/typing', { user: this.selectUser }) + }, 700), }, + computed: { + typingMessage() { + const typingOtherUsers = this.typingUsers.filter(user => user !== this.selectUser) + if (typingOtherUsers.length === 0) { + return '' + } + if (typingOtherUsers.length === 1) { + return `${typingOtherUsers[0]}が入力しています` + } + if (typingOtherUsers.length > 1) { + return '複数人が入力しています' + } + } + }, } </script> // ...
api.php
に追加routes/api.php// ... Route::post('/chat/add', 'Api\\ChatController@add'); + Route::post('/chat/typing', 'Api\\ChatController@typing'); Route::get('/chat/event', 'Api\\ChatController@event');
入力中のユーザー情報を保存して、配列で返すように変更
app/Http/Controllers/Api/ChatController.php+ /** + * 入力中の人の情報を保存 + * + * @param Request $request + * @return JsonResponse + */ + public function typing(Request $request): JsonResponse + { + Redis::sadd('typing_users', $request->get('user')); + return response()->json(['result' => 'ok']); + } /** * チャット一覧取得 * * @return StreamedResponse */ public function event(): StreamedResponse { // 最近のチャット5件と、一番最近のcreated_atを取得 $chats = Chat::orderBy('created_at', 'desc')->limit(5)->get()->sortBy('created_at')->values(); $tmpLatestCreatedAt = optional($chats->last())->created_at ?? Carbon::minValue(); $response = new StreamedResponse(function() use ($chats, $tmpLatestCreatedAt) { echo 'data: ' . json_encode(['posts' => $chats]) . "\n\n"; ob_flush(); flush(); while(true) { $latestCreatedAt = is_null(Redis::get('latest_created_at')) ? Carbon::minValue() : Carbon::parse(Redis::get('latest_created_at')); + $typingUsers = Redis::smembers('typing_users'); if ($latestCreatedAt->gt($tmpLatestCreatedAt)) { // チャットに更新があった場合はテーブルから取得 $latestChats = Chat::where('created_at', '>', $tmpLatestCreatedAt)->orderBy('created_at', 'asc')->get(); $tmpLatestCreatedAt = $latestCreatedAt; } - echo 'data: ' . json_encode(['posts' => $latestChats ?? []]) . "\n\n"; + echo 'data: ' . json_encode(['posts' => $latestChats ?? [], 'typing_users' => $typingUsers]) . "\n\n"; ob_flush(); flush(); $latestChats = null; + Redis::del('typing_users'); sleep(1); } }); $response->headers->set('Content-Type', 'text/event-stream'); $response->headers->set('X-Accel-Buffering', 'no'); $response->headers->set('Cach-Control', 'no-cache'); return $response; }入力中の人の情報を出せることができました
ここでの変更です
https://github.com/okumurakengo/laravel-sse-chat/commit/4ff8bfeb455b19d22276c50b4689d7ee6db28c25
最後まで見ていただいてありがとうございましたm(_ _)m