20191126のPHPに関する記事は18件です。

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}'");

これで正常にできるはず

かんたん

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

Seleniumで業務のテストを自動化しようと試みた話

この記事はチームラボエンジニアリングアドベントカレンダー4日目の記事です!

はじめに

初めまして、チームラボエンジニアリングの鶴本です。
入社して4ヶ月ほど経ち、少しずついろんなことができるようになってきました。
そんな僕が生意気にも業務のテストの自動化をしようと試みたので記事にしました。

何を自動化する?

僕が在籍しているチームでは、弊社が管理しているサイトの保守やバグ改修を主な業務としているのですが、そのサイトのテスト項目にmetaタグが仕様書通りに設定されているかという項目があります。
そのテストは現状、サイトをブラウザで開いてデベロッパーツールを用いて目視で確認するという、やや非効率的なものであるわけです。
そのテストを自動で行うようにします。

どうやって自動化する?

Seleniumの自動化コードをPHPコマンドで実行し、仕様通りにmetaタグが設定されているかどうかをログで出力し確認します。
また今回は、PHPからSeleniumを使用できるphp-webdriverというライブラリを使用します。

環境構築

php-webdriverを動作させるために環境構築の手順を書きます。

作業ディレクトリ作成と移動

mkdir php-selenium
cd php-selenium

Composerバイナリをダウンロード(インストール済みの場合は結構です)

curl -sS https://getcomposer.org/installer | php

php-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を使用してどんどん自動化したいですね!

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

【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.sh

Homestead.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/

疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!

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

PHPWeekly 191114 簡単サマリー

PHPWeeklyの内容を簡単に翻訳し、興味のある内容に対してコメントを残します。

知っている知識の中で、ブログを書こうと思っていますが、間違ってる内容が含まれている可能性もあります。

間違っているところに対する指摘はいつも歓迎ですが、非難・悪口は求めていません。

始まる前に

  1. PHP 7.4が発表され、SymfonyとLaravelにも7.4が適用されました。

  2. ブログを作成しながら知らなかったことを調べたり、補足したりするのにかなり時間がかかりました。
    やっぱり勉強し続けないとプログラマーとして衰えることも改めて思いました。

記事

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;   // binary

11. 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をしようしていない為、チェックだけしました。

アナウンスメント

  1. PHP 7.4.0RC6 リリース

  2. Symfony 5.0.0-RC1 リリース

  3. Symfony 4.4.0-RC1 リリース

  4. Laravel 6.5.1 リリース

  5. ワードプレス 5.2.4 アップデート

おもしろいプロジェクト、ツール、ライブラリー

PHPWeeklyへ紹介されたプロジェクトリストです。

おもしろいと思ったら参加してもよいかと思います。

  1. scarlets: ウェブフレームワーク

  2. arrayy: 配列操作ライブラリー

  3. izend: コンテンツマネージングの為のMVCエンジン

  4. simple-cache: キャッシュサーバーと簡単にインターれくと可能なキャッシュレイヤー

  5. php-iban: 銀行アカウント情報の生成・パーシング・検証・エラーチェックのためのライブラリー

  6. gravity-pdf: メール、pdf ダウンロードフォームを作ってくれるワードプレスプラグイン

  7. pastebin-php: PHPで実装されたpastebin。何かよくわからない。

  8. generators: カスタムLaravelファイルジェネレーター

  9. sncredisbundle: SymfonyのためのRedisバンドル

  10. enhavo: Symfonyで作られたオープンソースCMS

  11. munus: PHPで関数型プログラミングをするためのライブラリー。

  12. slim4-skeleton: Slim4フレームワークのSkeleton。名前通り!

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

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 OPTIONS

PUTは許可されていませんでした。

解決

PUTではなくPOSTを使えばいいということがわかったので、Apacheの設定を変更せずともエラーを解消することができました。

  • ルーティングのメソッド変更
Route::put('/xx/{xxx}/xx/{xxx}', 'SomethingApiController@somethingUpdate')

Route::post('/xx/{xxx}/xx/{xxx}', 'SomethingApiController@somethingUpdate')

  • Ajax時のメソッド変更
type: "PUT",

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

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_ERROR

fixする対象は作業した(差分のある)ファイルです。
phpのsyntaxなどでエラーが出た場合は別途メッセージが表示されます。

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

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>';
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

phpQueryを使ってWEBスクレイピングを試してみた

PHPでwebスクレイピングを試してみた。

phpQueryというライブラリを使うとjQueryのように要素を指定して簡単にスクレイピングできるみたいなので、サンプルを試してみました。
jQueryのようにセレクタを指定する事ができるので、すごく直観的にスクレイピングしたい要素が取得できます。
※スクレイピングについては自己責任で行ってください。

WEBスクレイピングとは

ウェブスクレイピングとは、ウェブサイトから情報を抽出するコンピュータソフトウェア技術のこと。ウェブ・クローラーあるいはウェブ・スパイダーとも呼ばれる。
出典:Wikipedia

phpQueryのダウンロード

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に出力するようにしたらもっと便利になりそうです。

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

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に変換できるのか?
そんな疑問から色々ネットの海を泳いだ結果、以下の情報を得ました。

  1. PhpSpreadsheetとDompdfなどを利用して変換する
  2. LibreOfficeをインストールして、PHPからコマンドを呼び出す

とりあえずやってみよう!

1. PhpSpreadsheetとDompdfなどを利用して変換する

まずはPHPでExcelをファイルを弄れることで有名なPhpSpreadsheetを使用してみます。
PhpSpreadsheetはDompdfの他にmPDFとTCPDFが対応しているので、全て試します。

必要なライブラリをインストール

作業ディレクトリはこんな感じです。

.
├── composer.json
├── composer.lock
├── src
└── storage
    ├── excel
    └── pdf

composerで各種ライブラリインストール

$ composer require phpoffice/phpspreadsheet
$ composer require dompdf/dompdf
$ composer require mpdf/mpdf
$ composer require tecnickcom/tcpdf

composer.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

コードを書く:computer:

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

:thinking: :thinking: :thinking:

TCPDFの驚きの白さ。
DompdfとmPDFは文字化けと印刷設定でなんとかなるかもしれないですね。罫線が一部消えてるのが気になりますが。。。
この後文字化け対策でフォント入れようとしたのですが、上手く出来ませんでした。
知っている方いれば教えてください。。。

2. LibreOfficeをインストールして、PHPからコマンドを呼び出す

LibreOfficeは無料で使えるオフィスソフトです。
コマンドラインからも色々な操作ができるので、それを利用してxlsxをPDFに変換します。

LibreOfficeのインストール

下記のURLからインストールします。
https://ja.libreoffice.org/download/download/

Linuxの場合はyumやapt-getにあると思います。

コードを書く:computer:

先ほど書いたコードに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 雑感(個人的なハマりポイント等)です。

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

Twig + Bluma でページネーションをやってみる

目的

PHPのテンプレートフレームワークであるTwigとCSSのフレームワークであるBulmaでページネーションをやってみる
image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

コード

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">&hellip;</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")}}
// 略

メモ

ループは効率が悪そうなので、速度が重要なら分岐でうまいこと作った方がいいと思う。

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

Twig + bulma でページネーションをやってみる

目的

PHPのテンプレートフレームワークであるTwigとCSSのフレームワークであるBulmaでページネーションをやってみる
image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

コード

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">&hellip;</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")}}
// 略

メモ

ループは効率が悪そうなので、速度が重要なら分岐でうまいこと作った方がいいと思う。

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

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

request
https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY&start_date=2017-07-08&end_date=2017-07-10
return
[
  {
    "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を使えるようですが、下記のとおり回数制限があるようです。

https://api.nasa.gov/

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から画像をダウンロードしました。

デスクトップの背景やスクリーンセーバが華やかになりますので、個人利用で楽しんでみてはいかがでしょうか。

それではごきげんよう。

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

#wordpressを知らないPHPエンジニアあるある

ツイッターで #wordpressを知らないPHPエンジニアあるある を書いているのですが、ツイッターだと流れてしまうので纏めています。

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

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();を指定することで、関数内とか、ループ内でも自由にデータとってくることができるのかな〜。
この辺りが、まだわかっていないのでまた理解したらまとめよ。

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

【Laravel開発ノート】ドメインを設定するポイント

1.目的

ローカル環境で「http://192.168.10.10/」→「http://weibo.test」と表示したい。

2.設定のポイント

2.1 「.env」ファイル

APP_DEBUG=true
APP_URL=http://weibo.test

2.2 「homestead.yaml」ファイル

sites:
    - map: weibo.test # <--- ここ!
      to: /home/vagrant/code/Laravel/public 

2.3 「/etc/hosts」ファイル

$ vim /etc/hosts

最後の一行に以下の内容を追加

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

AndroidでPHPにjsonを送受信する

Androidでjsonを送受信する

この記事は調べながら作ったサンプルを自分用にまとめたものです。

AndroidでPHPにjsonを送受信するアプリを作ります。
例として、Androidで入力した文字列二つをPHPに送り、PHPで受け取った文字列を連結した後Androidに送り返します。
完成したアプリは以下のようになります。

Screenshot_1574675979.png

Androidのコード

layoutのidは

  • 二つのEditTextはeditText1とeditText2
  • ボタンはsubmit
  • 結果の表示TextはtextResult

としたときのMainActivityは以下のようになります。

MainActivity.kt
class 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 の両方に対応

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

【Laravel】メール送信失敗時のログを残す

Laravel で複数のメールアドレスへの送信処理を書いた時、メール送信が失敗したアドレスの一覧を取得したかったが、ドキュメントには載っていなかったのでメモとして残しておく。

環境

Laravel 6.X

やり方

Mail::failures() で送信失敗メールアドレスが配列で取得できる。(参考
例えば、Mailable クラスの設定後、送信処理をコントローラに書く場合、下記のような感じ。

MailSendController.php
namespace 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 へのログ投稿の方法は こちらの記事 に詳しく書かれていました。

参考

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

Laravel6でRedisとSSE(Server-Sent Events)を使った簡易チャットを作成してみた

私はLaravel初心者です。基本的にググりながら書いています。よろしくお願いします。:bow:

Laravelで簡単にチャットを作ってみようと思ったのですが、1秒ごとにDBにアクセスするようなのは避けたかったので、Redisでやってみるといいんじゃないかと思い、作成してみたので書かせていただきます。m(_ _)m

Redisを使ってチャットをするというやり方はこちらが参考です。
RedisとServer Sent EventでJavaScriptでチャットを作ってみた:電脳ヒッチハイクガイド:電脳空間カウボーイズZZ(電脳空間カウボーイズ)


このような簡単なチャットを作成します

FQ44bRqI04.gif


自動生成ファイルもありますが、自分が最終的に変更したのは↓です

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です。

Screen Shot 2019-12-08 at 22.13.25.png

2. コントローラー、ビュー作成

2-1. ルートを定義する

web.php に1行追加

routes/web.php
Route::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を表示することができます

Screen Shot 2019-12-09 at 23.50.43.png

ここまでの変更分はこのようになりました

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.jschat.blade.php を変更

resources/js/app.js
  const 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のコンポーネントを画面に表示することができました。

Screen Shot 2019-12-10 at 0.34.33.png

yarn devのビルドした出力結果も含まれていますが、ここで変更したコミットは以下です。
※今回は特に気にしませんでしたが、app.jsなどの自動生成ファイルは.gitignoreに設定しましょう

https://github.com/okumurakengo/laravel-sse-chat/commit/5ee0df386ceb298331d382ba2dafe3bc03afd7a9

4. チャットの画面を作成

app.jsApp.vueを読み込むように変更

resources/js/app.js
  Vue.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>

Screen Shot 2019-12-10 at 23.50.42.png

ここでの変更、※public/js/app.jsyarn devyarn 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-databases

user: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=secret

5-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テーブルができていればOK

Screen Shot 2019-12-12 at 0.24.38.png

5-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']);
    }
}

この状態でチャットで送信してみると

Screen Shot 2019-12-13 at 0.41.36.png

データを保存することができました

Screen Shot 2019-12-13 at 0.42.15.png

ここでのコミットです

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>&nbsp;<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.php
use 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;
    }

// ...

チャットの内容を更新に合わせてリアルタイムに取得できました。

A6kh3wvL29.gif

ここでの変更です

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>&nbsp;<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;
    }

入力中の人の情報を出せることができました

FQ44bRqI04.gif

ここでの変更です

https://github.com/okumurakengo/laravel-sse-chat/commit/4ff8bfeb455b19d22276c50b4689d7ee6db28c25


最後まで見ていただいてありがとうございましたm(_ _)m

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