20200214のPHPに関する記事は15件です。

簡単ですぐ使えるarray_multisortの複数並び替えの記述方法

この記事は「データベース結果の連想配列をPHP側でソートしたい」ひと向けの内容になっています。
結論だけ知りたい方は「実際の記述例」を見てください。

読者対象

  • PHPv5.6以上
  • 連想配列内をソートしたい
  • 複数の指定がしたい(指定数は可変でやりたい)

まずはコード例から理解する

PHP array_mutli_sort

上記公式にある「データベースの結果をソートする」の例をベースにします。
(リンク先にとばなくても問題ありません)

データ構造

id code name
1 E6B722 きいろ
2 74367D むらさきいろ
3 FF0000 あかいろ
4 DF3C94 ももいろ

データを可視化(デバッグ時の表示構造)

array (
  0 => array(
    'id' => 1,
    'code' => 'E6B722',
    'name' => 'きいろ',
  ),
  1 => array(
    'id' => 2,
    'code' => '74367D',
    'name' => 'むらさきいろ',
  ),
  2 => array(
    'id' => 3,
    'code' => 'FF0000',
    'name' => 'あかいろ',
  ),
  3 => array(
    'id' => 4,
    'code' => 'DF3C94',
    'name' => 'ももいろ',
  )
);

また、$sortListという配列にはあらかじめソート順を指定したデータを入れておきます。

$sortList[] = array('key' => 'id', 'sort' => SORT_ASC);
$sortList[] = array('key' => 'code', 'sort' => SORT_DESC);

今回は2つのソートを指定しました。

SQLに例えると、下記のようなイメージです。

ORDER BY id ASC, code DESC;

実際の記述例

// DBのデータを$dataに代入しているというイメージで読んでください
$data = $model->get()->toArray();
$result = $this->getSortArray($data, $sortList);

private function getSortArray($data, $sortList)
{
  $argument = [];

  if (empty($sortList)) {
    return $data;
  }

  foreach ((array)$sortList as $list) {
    $argument[] = array_column($data, $list['key']);
    $argument[] = $list['sort'];
  }

  $argment[] = $data;
  array_multisort(...$argment);

  return $data;
}

カラムがもっと多く、更に順序も2つではなく4つ指定したい!という人は$sortListに条件を追加してあげれば解決します。

結果だけ知りたい、という方は上記の処理内容で終わりになります。

以降は処理についての簡単な解説をしていきます。

array_multisortについて

まずは冗長になりますが、array_multisortの理解からです。

array_multisortは複数の配列や多次元配列をソートしてくれます。

そして公式サイトを見たことがある方は、引数の多さに混乱したかもしれません。

なのでまずは整理をしてみました。

array_multisort(
  引数1 並び替えしたい配列を指定  | 必須
  引数2 SORT_ASCかSORT_DESCを指定 | 省略可
  引数3 ソート時のデータ形式を指定  | 省略可
  ...
  以下上記と同じ指定方法
)

このように、実は意味合いが違う3つの引数指定があるだけです。

4つ目の引数からは、また追加の配列を指定し、ソート順を指定して…という風になるだけです。大切なのは3つです。

つまり、シンプルに考えるとただの配列もソートができるわけですね。

そして究極を言えば、下記でも動きます。

$array = ['2', '1', '3'];
array_multisort($array);
// array(0 => '1', 1 => '2', 2 => '3')

引数2を省略した場合は昇順になると公式に書いています。

なのでシンプルに昇順に並び替えたいなら、上記のような指定でおわりです。

多次元のソートは引数4以降の仕様を理解する

さて、ここまでの理解が得られたなら、次は引数4で指定ができる追加配列についての仕様を理解していきます。

追加配列は、それ以前のソートを引き継ぐことができます。

$array = ['2', '1', '3'];
$array2 = ['1', '2', '3'];
array_multisort($array, $array2);
// array(0 => '1', 1 => '2', 2 => '3')
// array2(0 => '2', 1 => '1', 2 => '3')

上記は引数2と引数3を省略し「ソートしたい配列」を2つ指定しています。

結果を見てもらうと分かる通り、$array2$arrayと同じ順番でソートされています。つまりソートを引き継いでいます。

この特性を応用すると、連想配列の複雑なソートを実現することができます。

上記処理の解説

ではようやく今回のプログラムの解説に移ります。

$data = $model->get()->toArray();
$result = $this->getSortArray($data, $sortList);

private function getSortArray($data, $sortList)
{
  // ※1
  $argument = [];

  if (empty($sortList)) {
    return $data;
  }

  // ※2
  foreach ((array)$sortList as $list) {
    $argument[] = array_column($data, $list['key']);
    $argument[] = $list['sort'];
  }

  // ※3
  $argment[] = $data;
  // ※4
  array_multisort(...$argment);

  return $data;
}

※1について

$argument = [];

可変の引数をこの配列に格納するため宣言しています。

※2について

$sortListの定義をおさらいします。

$sortList[] = array('key' => 'id', 'sort' => SORT_ASC);
$sortList[] = array('key' => 'code', 'sort' => SORT_DESC);

keyは、「連想配列内のどのプロパティ名でソートするのか」を意味します。

またsortはarray_multisortの引数2に該当します。

foreach ((array)$sortList as $list) {
  // 引数1にソートしたい配列を指定
  $argument[] = array_column($data, $list['key']);
  // 引数2に並び順を指定( 昇順or降順 )
  $argument[] = $list['sort'];
  // 引数3は省略
}

引数1と引数2のループが上記でされます。

引数2や引数3は配列ではないので、そこに配列が指定されると自動的に引数1である「ソートしたい配列」として認識してくれます。

ちなみに、

$argument[] = array_column($data, $list['key']);

はPHPv5.5以降で使える構文で、

foreach ((array)$data as $row) {
  // ex) $row: ['id' => 1, 'code' => 'E6B722', 'name' => 'きいろ']
  $argument[] = $row[$list['key']];
}

上記のforeachと同じ結果を返してくれます。2重ループの見た目にならなくてスッキリします。

※3について

$argment[] = $data;

本当にソートしたい配列を1番最後に指定します。

この配列は、それ以前の引数で指定された配列の順序をすべて引き継いだソートが適用されます。

※4について

array_multisort(...$argment);

PHPのv5.6から使うことができる可変長引数を使用しています。

PHP 関数の引数

あまり用途がない為、見慣れないかもしれません。ですがこの書き方がarray_multisortと相性が良く、スマートな処理になりました。

アンチパターン

ちなみに、やらかしたパターンを紹介します…。

$data = $model->get()->toArray();
$result = $this->getSortArray($data, $sortList);

private function getSortArray($data, $sortList)
{

  foreach ((array)$sortList as $list) {
    $array[] = array_column($data, $list['key']);
    // 1回目にidでソート、2回目にcodeでソート
    array_multisort($array, $list['sort'], $data);
  }

  return $data;
}

上記の場合は、idでソートした結果の$dataに、更にcodeでソートをしています。ですが上記は意図したとおりに動きません。

SQLで例えると、

ORDER BY id ASC, code DESC

このような結果を期待しているのですが、

ORDER BY code DESC

最後のソートだけが適用されます。

もし良さげでしたら、ぜひ可変長引数を使ってみてください。

最後に

ざっと調べた感じ、array_columnarray_multisortも速度的に問題なさそうですね。

array_multisortは今までなんとなくで使えていたので、理解できてよかった(・×・)ノ

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

php exec

php exec

備忘録として記載します。

exec("コマンド 引数")

ネットで調べると

exec("コマンド, 引数")

カンマが必要なように記載があるが、なくてもよかった

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

PHPでFIDO2(WebAuthn)認証を実装する

PHPでFIDO2(WebAuthn)認証を実装してみた

Yahoo Japan!のログインなどにも実装されだいぶ、目にする機会が増えたのでこの機会に実装してみました。

FIDO2認証とは

FIDO2認証とは、FIDOアライアンスが提供するパスワードレス認証の仕組みで、より安全な多要素認証を実現できます。

どんなものかは、Android7.0以上の端末で、Google Chromeを最新にして、Yahoo Japan!でログインするとわかると思います。アクセスすると、「指紋、顔認証などでログイン」と表示され、一度登録すると、次回以降は、パスワードの代わりに指紋や顔認証でログインすることができるようになります。

簡単に言うと、WEBサイトでも指紋認証 ができるようになりました。

さらに、良いのは指紋や顔の情報をWEB管理者が管理する必要はありません。その辺の詳しい仕組みは下記を見てください。
https://fidoalliance.org/fido%e3%81%ae%e4%bb%95%e7%b5%84%e3%81%bf/?lang=ja

準備

使用するライブラリ

今回はweb-auth/webauthn-lib を使いました。composerを使って、下記からインストールできます。

composer require web-auth/webauthn-lib

ドキュメントはこちら
https://webauthn-doc.spomky-labs.com/

ドキュメントを読み込むのに苦労したので、備忘録を兼ねて記事を残します。

準備するもの

ライブラリを使う上で準備するもの

  • ユーザアカウント 既存のログインが実装されている場合それを利用します
  • PHP ドキュメントはPHP7.2以上と記載されますが、7.3.14を使いました。(gmp_intval関数をライブラリが使っておりGMP関数をインストールするために、PHPバージョンをあげました)
  • 公開鍵を保存するもの 今回はDB(MySQL)に保存しました。
  • セッション機能もしくは、前のアクセス時に発行した情報を保存しておける機能
  • (必要あれば)ブラウザ判定機能 対応ブラウザは、まだ限定的なので、要件によっては判定が必要だと思います。この記事では触れてません。

実装

大きく2つの機能を提供することになります。

  1. 公開鍵を登録する機能 マイページなどで、一度認証デバイスで認証して、作成した公開鍵をRPサーバに登録しておきます。
  2. ログイン機能 認証デバイスで認証して、署名したchallengeを公開鍵で検証して認証する機能です。

この2つの機能を実装するために、いかの流れで実装をしていきます。

  1. 公開鍵Repositoryの作成
  2. ユーザEntityの作成
  3. RpServerの作成
  4. 公開鍵を登録するために、認証デバイスで認証するときのオプション(json)を作成
  5. 認証デバイスからレスポンスされる公開鍵(json)を保管する
  6. ログインするために認証デバイスにオプション(json)を作成
  7. 認証デバイスからレスポンスされる署名されたchallengeを含むjsonを検証する

結構、実装すること多いです。だいたい動かすだけなら、1〜2人日ぐらいでできましたが、既存のログインやらマイページに組み込むのに、+2〜3人日ぐらいかかりました。

以下にソースコードを載せてますが、記事用に実際の実装から多少変えました。特にJavascript。そのままでは動かない可能性があります。

公開鍵Repositoryの作成

準備するものでも触れましたが、認証デバイスが発行する公開鍵を保存しておく必要があります。今回は、DB(MySQL)に保管しました。ドキュメントでは、ファイルに保存する方法も記載されてます。

テーブル定義

CREATE TABLE `webauthn_credentials` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `public_key_credential_source_id` varchar(256) NOT NULL COMMENT '認証ID',
  `use_flag` tinyint(3) unsigned NOT NULL DEFAULT '1' COMMENT '使用フラグ[0:無効 1:有効]',
  `user_handle` varchar(256) NOT NULL COMMENT 'ユーザハンドル(ログインID)',
  `credential` text COMMENT '認証情報(json)'
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_public_key_credential_source_id` (`public_key_credential_source_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Webauthn認証';

公開鍵Repository

ライブラリでは、具体的な実装が提供されていません。各システムの制約があると思うので自由に実装しろとのことですが、ドキュメントを見るとサンプルソースがあります。それを参考に実装しました。
唯一の制約は、Webauthn\PublicKeyCredentialSourceRepository をimplementsすること。

php
namespace Webauthn\Repository;

use Webauthn\PublicKeyCredentialSource;
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyCredentialSourceRepositoryInterface;
use Webauthn\PublicKeyCredentialUserEntity;
use Project\Model\WebauthnCredentialModel;
use Project\Entity\WebauthnCredential;

class PublicKeyCredentialSourceRepository extend WebauthnCredentialModel implements PublicKeyCredentialSourceRepositoryInterface
{

    /**
     * 公開鍵を保管します saveCredentialSourceは、登録時と検証後の更新時に呼ばれます。
     */
    public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, bool $flush = true): void
    {
        $data['publicKeyCredentialSourceId'] = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId());
        $data['userHandle'] = $publicKeyCredentialSource->getUserHandle());
        $data['credential'] = json_encode($publicKeyCredentialSource);

        $WebauthnCredential = $this->repository->findOneBy(['publicKeyCredentialSourceId' => $data['publicKeyCredentialSourceId']]);

        if (!$WebauthnCredential) {
            $WebauthnCredential = new WebauthnCredential();
            $WebauthnCredential->fromArray($data);
            $this->entityManager->presist($WebauthnCredential);
        } else {
            $WebauthnCredential->fromArray($data);
            $this->entityManager->merge($WebauthnCredential);
        }
        if ($flush) {
            $this->entityManager->flush();
        }
    }

    /**
     * ユーザEntityが保有する公開鍵を全て取得します
     */
    public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
    {
        $WebauthnCredentials = $this->repository->findBy(['userHandle' => $publicKeyCredentialUserEntity->getId()]);
        return array_map(function ($WebauthnCredential) {
            $array = json_decode($WebauthnCredential->getCredential(), true);
            return PublicKeyCredentialSource::createFromArray($array);
        }, $WebauthnCredentials);
    }

    /**
     * CredentialIdで公開鍵を取得します
     */
    public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
    {
        if ($WebauthnCredential = $this->repository->findOneBy(['publicKeyCredentialSourceId' => base64_encode($publicKeyCredentialId)])){
            $array = json_decode($WebauthnCredential->getCredential(), true);
            return PublicKeyCredentialSource::createFromArray($array);
        }
        return null;
    }
}

上記の実装からもわかる通り、検索に使う情報は、publicKeyCredentialSourceIduserHandle のみです。他の情報は、特に意識する必要はなさそうなので、json形式にして一つのカラムに入れることにしました。そうすることで、今後ライブラリ側で情報が増えた場合にも、特に何もすることなくバージョンアップで対応可能だから(場合によるが)です。

saveCredentialSource()は、ライブラリ内部でも呼ばれています。その時は更新を期待されているので、注意が必要です。

ユーザEntityの作成

おそらくFIDO2認証を実装する場合、既存システムがあり、ユーザテーブルがあると思われます。なので、ライブラリ側でも実装されておらず、自由に実装しろとのことです。
実際、今回実装するときも既存ユーザEntityがあった上で実装しました。

制約は、
- IDがユニークかつstringであること
- Usernameがユニークであること

ドキュメントによると下記のようにして作成します。

php
use Webauthn\PublicKeyCredentialUserEntity;

$userEntity = new PublicKeyCredentialUserEntity(
    'john.doe',                             // Username
    'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c', // ID
    'John Doe'                              // Display name
);

それぞれ既存のユーザテーブルのなにと紐付けるかは、迷いどころです。
今回は、
- ID -> 既存システムのユーザテーブルの主キー(Stringちゃうけど)
- Username -> ログインIDとしました。

実装はこんな感じ

php
use Webauthn\PublicKeyCredentialUserEntity;
use Project\Model\CustomerModel;

final class PublicKeyCredentialUserEntityRepository extends CustomerModel
{
    /**
     * usernameでユーザEntityを取得します
     */
    public function findWebauthnUserByUsername(string $username): ?PublicKeyCredentialUserEntity
    {
        $user = $this->repository->findOneBy(['login' => $username]);
        if (null === $user) {
            return null;
        }

        return $this->createUserEntity($user);
    }

    /**
     * userhandleでユーザEntityを取得します
     */
    public function findWebauthnUserByUserHandle(string $userHandle): ?PublicKeyCredentialUserEntity
    {
        $user = $this->findOneBy(['id' => $userHandle]);
        if (null === $user) {
            return null;
        }

        return $this->createUserEntity($user);
    }

    /**
     * 既存のユーザテーブルの情報からユーザEntityを取得します
     */
    public function createUserEntity($user): PublicKeyCredentialUserEntity
    {
        return new PublicKeyCredentialUserEntity(
            $user->login,
            (string)$user->id,
            $user->name
        );
    }
}

RpServerの作成

RpServerは、この後の工程で使うオブジェクトです。いろんなところで使うので、共通で実装しましょう。

php
use Webauthn\Server;
use Webauthn\PublicKeyCredentialRpEntity;
use Project\PublicKeyCredentialSourceRepository;

public function getServer()
{
    $rpEntity = new PublicKeyCredentialRpEntity(
        'Webauthn Server',  //  アプリケーションの名前
        'example.com'       //  アプリケーションID = domain
    );
    $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();

    $this->server = new Server(
        $rpEntity,
        $publicKeyCredentialSourceRepository
    );
}

アプリケーションID

アプリケーションIDは、オプションです。指定しない場合は、現在のドメインが使われるようです。
オプションではありますが、ライブラリのドキュメントでは、強く推奨されてます。しょっちゅうドメインが変わるテスト環境などでなければ、設定ファイルなどから取得して設定してもいいかもしれません。

なお、アプリケーションIDは、ポートやパスなどをのぞいたドメインである必要があります。

Allowed: www.sub.domain.com, sub.domain.com, domain.com
Not Allowed: www.sub.domain.com:1337, https://domain.com:443, sub.domain.com/index

公開鍵を登録するために、認証デバイスで認証するときのオプション(json)を作成

これまでで、下準備が終わりました。ここからは、実際に画面を実装していくことになります。

登録の流れは下図の通りです。
regist.png

まずは、オプション(json)を作成します。

php
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\PublicKeyCredentialCreationOptions;
use Project\PublicKeyCredentialSourceRepository;

$userEntity = new PublicKeyCredentialUserEntity(
    'john.doe',
    'ea4e7b55-d8d0-4c7e-bbfa-78ca96ec574c',
    'John Doe'
);

$credentialSourceRepository = new PublicKeyCredentialSourceRepository();
$credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);

$excludeCredentials = array_map(function ($credential) {
    return $credential->getPublicKeyCredentialDescriptor();
}, $credentialSources);

$publicKeyCredentialCreationOptions = $this->getServer()->generatePublicKeyCredentialCreationOptions(
    $userEntity,                                                                // The user entity
    PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, // We will see this option later
    $excludeCredentials                                                         // Excluded authenticators
);

$json = json_encode($publicKeyCredentialCreationOptions);

上の例では、userEntityはサンプルを書きましたが、おそらくこの機能を使うときは、認証済みのはずなので、認証済みユーザ情報を取得してセットするといいと思います。
その際には、PublicKeyCredentialUserEntityRepositoryfindWebauthnUserByUserHandle() メソッドを使うといいと思います。

このとき、$publicKeyCredentialCreationOptionsの内容を保存しておき、公開鍵がリクエストされてきたときに取り出せるようにする必要があります。(記事に記載してませんが)今回の実装では、セッションに保存しました。

認証デバイスからレスポンスされる公開鍵(json)を保管する

次に、上記のオプション(json)をJavascriptに渡します。
ライブラリのドキュメントにあるJavascriptを、ほぼそのまま使ってます。

なお、このJavascriptについてもライブラリは提供してくれません。各システムでVueJSとかJqueryとか使われていると思うので、そちらに合わせて使ってください。

下記では、ボタンを押したら認証デバイスを呼び出すようにし、認証が成功したらsubmitしてます。(たぶん)

javascript
const publicKey = ここに上記の$jsonの中身を入れる;

function arrayToBase64String(a) {
    return btoa(String.fromCharCode(...a));
}

function base64url2base64(input) {
    input = input
        .replace(/=/g, "")
        .replace(/-/g, '+')
        .replace(/_/g, '/');

    const pad = input.length % 4;
    if(pad) {
        if(pad === 1) {
            throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
        }
        input += new Array(5-pad).join('=');
    }

    return input;
}

publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);});
publicKey.user.id = Uint8Array.from(window.atob(publicKey.user.id), function(c){return c.charCodeAt(0);});
if (publicKey.excludeCredentials) {
    publicKey.excludeCredentials = publicKey.excludeCredentials.map(function(data) {
        data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);});
        return data;
    });
}

document.myform.btn.addEventListener('click', function() {
    navigator.credentials.create({ 'publicKey': publicKey })
        .then(function(data){
            const publicKeyCredential = {
                id: data.id,
                type: data.type,
                rawId: arrayToBase64String(new Uint8Array(data.rawId)),
                response: {
                    clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
                    attestationObject: arrayToBase64String(new Uint8Array(data.response.attestationObject))
                }
            };

            document.getElementById( "publicKeyCredential" ).value = JSON.stringify(publicKeyCredential);
            document.myform.submit();
        })
        .catch(function(error){
            alert('Open your browser console!');
            console.log('FAIL', error);
        });

});

このpublicKeyCredential を受け取りDBに保管します。

php
try {

    $publicKeyCredentialSource = $this->getServer()->loadAndCheckAttestationResponse(
        $publicKeyCredential, // 先ほどのpublicKeyCredentialをそのままセット
        $publicKeyCredentialCreationOptions, // セッションに保存していたオブジェクト
        $request  // PSR-7のHTTP Message
    );
    $credentialSourceRepository = new PublicKeyCredentialSourceRepository();
    $credentialSourceRepository->saveCredentialSource($publicKeyCredentialSource);
} catch (Throwable $exception){
}

$request には、PSRー7のHTTP Messageが必要らしいです。ライブラリのドキュメントにも記載されてますが、nyholm/psr7-server のライブラリを使うと取得できるようですが、今回は既存ライブラリにあったので、特に実装せず、そのまま既存ライブラリを使いました。本記事では、割愛してます。

loadAndCheckAttestationResponse関数でリクエストの中身をチェックしているのですが、リクエストにNGがあった場合、全て例外で処理されます。試してないですが、メッセージの内容をログに残すには、$this->getServer()->setLogger($Logger);Psr\Log\LoggerInterfaceを実装したオブジェクトをセットすれば、ログに残すことができそうです。

実装中、「Invalid challenge.」に結構悩まされました。Javascriptに渡した$publicKeyCredentialCreationOptionsとセッションからとった内容が異なると出るメッセージです。

ログインするために認証デバイスにオプション(json)を作成

ログインの流れは下図の通りです。
ninnsyou.png

まずは、オプション(json)を作成します。

php
$UserEntityRepository = new PublicKeyCredentialUserEntityRepository();
$userEntity = $UserEntityRepository->findWebauthnUserByUsername($login);

$credentialSourceRepository = new PublicKeyCredentialSourceRepository();
$credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);

$allowedCredentials = array_map(function ($credential) {
    return $credential->getPublicKeyCredentialDescriptor();
}, $credentialSources);

$publicKeyCredentialRequestOptions = $this->getServer()->generatePublicKeyCredentialRequestOptions(
    PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value
    $allowedCredentials
);

$json = json_encode($publicKeyCredentialRequestOptions);

認証は、ログインIDを入力されているものとし、ログインIDからuserEntityを取得します。

この$publicKeyCredentialRequestOptionsを登録時と同様にセッションに保存しておきます。

認証デバイスからレスポンスされる署名されたchallengeを含むjsonを検証する

登録時と同様に、次に、上記のjsonをJavascriptに渡します。
例によって、ライブラリのドキュメントにあるJavascriptを、ほぼそのまま使ってます。

javascript
const publicKey = ここに上記の$jsonの中身を入れる;
function arrayToBase64String(a) {
    return btoa(String.fromCharCode(...a));
}
function base64url2base64(input) {
    input = input
        .replace(/-/g, '+')
        .replace(/_/g, '/');
    const pad = input.length % 4;
    if(pad) {
        if(pad === 1) {
            throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
        }
        input += new Array(5-pad).join('=');
    }
    return input;
}
publicKey.challenge = Uint8Array.from(window.atob(base64url2base64(publicKey.challenge)), function(c){return c.charCodeAt(0);});
if (publicKey.allowCredentials) {
    publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) {
        data.id = Uint8Array.from(window.atob(base64url2base64(data.id)), function(c){return c.charCodeAt(0);});
        return data;
    });
}

document.myform.btn.addEventListener('click', function() {
    navigator.credentials.get({ 'publicKey': publicKey })
        .then(function(data){
            const publicKeyCredential = {
                id: data.id,
                type: data.type,
                rawId: arrayToBase64String(new Uint8Array(data.rawId)),
                response: {
                    authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)),
                    clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)),
                    signature: arrayToBase64String(new Uint8Array(data.response.signature)),
                    userHandle: data.response.userHandle ? arrayToBase64String(new Uint8Array(data.response.userHandle)) : null
                }
            };

            document.getElementById( "publicKeyCredential" ).value = JSON.stringify(publicKeyCredential);
            document.myform.submit();
        })
        .catch(function(error){
            alert('Open your browser console!');
            console.log('FAIL', error);
        });
});

そして、上記のpublicKeyCredentialをサーバ側で検証します。

php
try {
    $UserEntityRepository = new PublicKeyCredentialUserEntityRepository();
    $userEntity = $UserEntityRepository->findWebauthnUserByUsername($login);

    $publicKeyCredentialSource = $this->getServer()->loadAndCheckAssertionResponse(
        $publicKeyCredential, // 先ほどのpublicKeyCredentialをそのままセット
        $publicKeyCredentialRequestOptions, // セッションに保存していたオブジェクト
        $userEntity,  // ログインIDから取得したuserEntity
        $request      // PSR-7のHTTP Message
    );
} catch(Throwable $exception) {
}

ここで例外エラーがなければ、検証完了です。
残りは、各システムごとの認証処理を実行し認証完了です。

まとめ

ライブラリを使うことで簡単に実装ができます。
対応するブラウザも増えてきているので、今後色々なサイトで実装が増えてくると思います。
早くパスワードがなくなると良いですね。

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

mPDFでfloat:leftで並べると高さがずれてしまう問題を解決する

PHPと書いたけどHTMLのことしか書いてません。すみません。

mPDFで下記のHTMLをPDF化すると二段目以降が画像1つごとに高さがズレてしまう問題が発生する。

<head>
<style type="text/css">
    @page {
      margin: 0;
    }
    body {
      margin: 0;
    }
    div.imagebox {
       width: 46mm;
       height:46mm;
       float: left;
       margin-left: 2mm;
       margin-right: 2mm;
       margin-top: 1mm;
       margin-bottom: 1mm;
    }
    div.wrapper {
        width:100%;
        margin-top:4mm;
        margin-left:6mm;
    }
    img {
       width: 46mm;
       height:46mm;         
    }
</style>
</head>
<body>
<div class="wrapper">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>

    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>

    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
</div>

</body>

どうやら各画像を囲っているdivタグごとにmarginの指定をするのが影響するらしいので、下記のように書き換える

<head>
<style type="text/css">
    @page {
      margin: 0;
    }
    body {
      margin: 0;
    }
        div.image-col {
           margin-top:1mm;
           margin-bottom:1mm;
        }
    div.imagebox {
       float: left;
       width: 46mm;
       height:46mm;
       margin-left: 2mm;
       margin-right: 2mm;
    }
    div.wrapper {
        width:100%;
    }
    img {
       width: 46mm;
       height:46mm;         
    }
</style>
</head>
<body>
<div class="wrapper">
    <div class="image-col">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
    </div>
    <div class="image-col">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
    </div>
    <div class="image-col">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
    </div>
</div>

</body>

これで段差ができてしまう問題が解決できた。

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

mPDFでfloat:leftを使って画像を並べると高さがずれてしまう問題を解決する

PHPと書いたけどHTMLのことしか書いてません。すみません。

mPDFで下記のHTMLをPDF化すると二段目以降が画像1つごとに高さがズレてしまう問題が発生する。

<head>
<style type="text/css">
    @page {
      margin: 0;
    }
    body {
      margin: 0;
    }
    div.imagebox {
       width: 46mm;
       height:46mm;
       float: left;
       margin-left: 2mm;
       margin-right: 2mm;
       margin-top: 1mm;
       margin-bottom: 1mm;
    }
    div.wrapper {
        width:100%;
        margin-top:4mm;
        margin-left:6mm;
    }
    img {
       width: 46mm;
       height:46mm;         
    }
</style>
</head>
<body>
<div class="wrapper">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>

    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>

    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
</div>

</body>

どうやら各画像を囲っているdivタグごとにmarginの指定をするのが影響するらしいので、下記のように書き換える

<head>
<style type="text/css">
    @page {
      margin: 0;
    }
    body {
      margin: 0;
    }
        div.image-col {
           margin-top:1mm;
           margin-bottom:1mm;
        }
    div.imagebox {
       float: left;
       width: 46mm;
       height:46mm;
       margin-left: 2mm;
       margin-right: 2mm;
    }
    div.wrapper {
        width:100%;
    }
    img {
       width: 46mm;
       height:46mm;         
    }
</style>
</head>
<body>
<div class="wrapper">
    <div class="image-col">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
    </div>
    <div class="image-col">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
    </div>
    <div class="image-col">
    <div class="imagebox">
       <img src="images/photo1.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo2.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo3.jpg">
    </div>
    <div class="imagebox">
       <img src="images/photo4.jpg">
    </div>
    </div>
</div>

</body>

これで段差ができてしまう問題が解決できた。

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

PHPじゃんけん勝負プログラムを書いてみた

じゃんけん勝負をするコードをPHPで書いてみました。

実務経験もほどんどなく、我流で書いておりますので酷いコードです。
コードレビューも兼ねて厳しいコメントを募集しております。

問題点をガンガン指摘いただけますと喜びます!よろしくお願いいたします!

処理の流れ

Dateクラスから日時の要素を取り出しint型として返す。

じゃんけんのタイプ別の配列を用意する。
パータイプ:["パー","グー","パー","チョキ","パー"]など

取得したint型のパラメータの商のあまりを取得して、
それぞれ3つ作ったユーザーのじゃんけんタイプを選択します。

$a =(日時の戻り値 + ユーザー名の長さ) * 100 % 3

if($a === 0){
パータイプ;
}elseif(…


コンピューターとN回じゃんけんをします。

勝率を求めます。

勝負の結果をテキストファイルに保存します。
以上です。

ファイル構成について

ファイル構成は以下のようになります。
Main.php(インスタンスを生成し、各処理を表示する)
User.php(ユーザークラスを定義している)
Status.php(ユーザークラスに引き渡すパラメータを作る親クラス)
Enemy.php(作られたユーザークラスとじゃんけん勝負をするためのトレイト)
result.txt(じゃんけん勝負の結果を日付と共に記録するテキストファイル)

Main.php
<?PHP
    require_once('User.php');

    //Userインスタンスの生成
    $user = new User("aaa");

    $user->user_name();
    echo "<br />";
    $user->battle();
?>
User.php
<?php
require_once('Status.php');
require_once('Enemy.php');

//
class User extends Status{
    //Enemyトレイトの宣言
    use Enemy;

    //乱数の変数の最大値4
    const MAX = 4;
    //乱数の変数の最小値0
    const MIN = 0;
    //じゃんけんの各配列
    public $arr_gu;
    public $arr_chiki;
    public $arr_pa;
    public $user;

    //ユーザーの名前
    public $name;

    //変数の初期化
    public function __construct($name){

        //ユーザーの名前
        $this->name = $name;

        //じゃんけんの配列グータイプ
        $this->arr_gu = ["グー","チョキ","グー","パー","グー"];
        //じゃんけんの配列チョキタイプ
        $this->arr_choki = ["チョキ","グー","チョキ","パー","チョキ"];
        //じゃんけんの配列パータイプ
        $this->arr_pa = ["パー","グー","パー","チョキ","パー"];

    }

    //ユーザー名の表示
    public function user_name(){

        echo "ユーザー名: ".$this->name;

    }

    //じゃんけんのタイプを求める処理
    public function type_select(){

        //$lengにint型パラメータを引き渡す処理。ユーザー名以外のパラメータはStatusクラスより継承
        $leng = strlen($this->name) + parent::from_date();

        if($leng % 3 === 0){
            $arr = $this->arr_gu;
        }elseif($leng %3 === 1){
            $arr = $this->arr_choki;
        }else{
            $arr = $this->arr_pa;
        }
        return $arr;

    }

    //Userのじゃんけんを行う処理
    public function rand_a(){

        //じゃんけん配列に引き渡す乱数 変数MAX~MIN参照
        $this->rnd = mt_rand(self::MIN, self::MAX);
        //じゃんけんの表示
        //echo $this->rnd.":". $this->type_select()[$this->rnd];

        //ここでrand_aメソッドの処理結果を返す。
        return $this->type_select()[$this->rnd];

        //じゃんけんタイプの表示
        if($this->type_select() == $this->arr_gu){
            echo "Type:グー";
            echo "<br />";
        }elseif($this->type_select() == $this->arr_choki){
            echo "Type:チョキ";
            echo "<br />";
        }else{
            echo "Type:パー";
            echo "<br />";
        }
        //じゃんけんタイプの中身
        for($i = 0;$i < self::MAX + 1; $i++){
            echo "*".$this->type_select()[$i]." ";
        }


    }

    //UserとEnemyのじゃんけんの処理
    public function battle(){

        //パラメータの初期化
        $count = 7;
        $win = 0;


        echo "<br />";

        //じゃんけんの繰り返し処理
        for($i = 1; $i <= $count; $i++){       
             //じゃんけん勝負の条件分岐処理
            if($this->rand_a() === $this->enemy()){
                //ここでメソッドを変数に格納して、戻り値を確定させる
                //※戻り値には乱数要素があるため

                //引き分けの場合の処理
                $user = $this->rand_a();
                $enemy = $this->enemy();
                echo $this->name."は ".$user;
                echo "<br />";
                echo "敵は ".$enemy;
                echo "<br />";
                echo "どちらも ".$user." なので引き分け";
                echo "<br /><br />";

            }elseif(($this->rand_a() === "グー" && $this->enemy() ==="チョキ") || ($this->rand_a() === "チョキ" && $this->enemy() ==="パー") || ($this->rand_a() === "パー" && $this->enemy() ==="グー")){
                //ここでメソッドを変数に格納して、戻り値を確定させる
                //※戻り値には乱数要素があるため

                //ユーザーが勝利した場合の処理
                $user = $this->rand_a();
                $enemy = $this->enemy();
                echo $this->name."は ".$user;
                echo "<br />";
                echo "敵は ".$enemy;
                echo "<br />";
                echo $this->name." の勝ち";
                echo "<br /><br />";
                //勝利数をカウント
                $win++;
            }else{
                //ここでメソッドを変数に格納して、戻り値を確定させる
                //※戻り値には乱数要素があるため

                //敵が勝利した場合の処理
                $user = $this->rand_a();
                $enemy = $this->enemy();
                echo $this->name."は ".$user;
                echo "<br />";
                echo "敵は ".$enemy;
                echo "<br />";
                echo "敵 の勝ち";
                echo "<br /><br />";

            }
        }
        //ユーザーの勝率の表示
        $win_rate = $this->name."の勝率は ".(int)(($win * 100) / $count)."% "."\n";

        echo $win_rate;

        //ファイル名の設定
        $fileName = './result.txt';

        //書き込む文字列
        $string = date("Y/m/d H:i:s ").$win_rate;

        //データの書込み
        file_put_contents($fileName, $string, LOCK_EX | FILE_APPEND);

        echo "<br />勝率をテキストファイルに保存しました。<br>";


    }
}

?>
Status.php
<?php
    //Userクラスの親クラス
    //じゃんけんタイプを決める為のパラメータをステータスとして求めるクラス。
    class Status{

        //Dataクラスからじゃんけんタイプを決めるパラメータの取得処理
        public function from_date(){
            //月と年、秒の値をdateクラスから取得してint型に直す
            $date = (int)date('nys');
            return $date;
        }
    }
?>
Enemy.php
<?php

    trait Enemy{
        //敵のじゃんけんの手
        public function enemy(){
            //敵のじゃんけん配列に引き渡す乱数処理
            $rand = mt_rand(0, 2);
            $enm_arr = ["グー","チョキ","パー"];
            $enm = $enm_arr[$rand];
            return $enm;
        }

    }
?>

処理の結果について

Main.phpをブラウザで呼び出し、出力結果を表示すると次のようになりました。
Janken_result.png
この処理結果がresult.txtに保存されます。

result.txt
2020/02/14 14:13:08 aaaの勝率は 42% 
2020/02/14 14:15:25 aaaの勝率は 42% 
2020/02/14 14:15:25 aaaの勝率は 14% 
2020/02/14 14:15:26 aaaの勝率は 0% 
2020/02/14 14:15:26 aaaの勝率は 42% 
2020/02/14 14:15:26 aaaの勝率は 28% 
2020/02/14 14:15:26 aaaの勝率は 57% 
2020/02/14 14:15:27 aaaの勝率は 28% 
2020/02/14 14:15:27 aaaの勝率は 0% 
2020/02/14 14:15:28 aaaの勝率は 0% 
2020/02/14 14:15:28 aaaの勝率は 42% 
2020/02/14 14:15:28 aaaの勝率は 14% 

以上となります。

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

mac に asdf で php をインストールする

現時点で最高のバージョンマネージャであるところの asdf を使ってPHPのインストールをしたいんですが、思考停止して asdf install php 7.x.xx みたいにしてもダメなのて手順のメモです。

なお、fish なので、bashやzshの人は適宜読み替えが必要です。

asdf のインストール

https://asdf-vm.com/#/core-manage-asdf-vm

Gitで入れる方法もありますが、brewに頼りまくっていきましょう。
上のページに書いてあるままの手順でいれていきます。

$ brew install asdf
$ echo "source "(brew --prefix asdf)"/asdf.fish" >> ~/.config/fish/config.fish
$ brew install \
  coreutils automake autoconf openssl \
  libyaml readline libxslt libtool unixodbc \
  unzip curl

phpのインストールのための設定

asdf-php
https://github.com/asdf-community/asdf-php

上記のプラグインを利用します。

$ asdf plugin add php

READMEを読むと

Prerequirements
Check the .github/workflows/workflow.yml for dependencies, paths, and environment variables needed to install the latest PHP version. To be honest, supporting a major version other than the latest without any extra work from the user is an endless endeavor that won't ever really work too well. It's not that we don't support them at all, but it's almost impossible for us to support them.

的なことが書いてあります。
サラッと書いてあってアレなんですが、GithubActions用の設定ファイルを見て、必要なライブラリは事前にインストールしておけよってことみたいです。

https://github.com/asdf-community/asdf-php/blob/master/.github/workflows/workflow.yml

中身を見てみると、stepsにubuntuとmacos用の設定があるので、macos用の設定だけ抜き出すと以下みたいな感じです。

- name: Install packages for macOS
    if: matrix.os == 'macos-latest'
     run: brew install autoconf automake bison freetype gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip pkg-config re2c zlib

- name: Add PKG_CONFIG_PATH environment variable for macOS
    if: matrix.os == 'macos-latest'
    run: echo "::set-env name=PKG_CONFIG_PATH::$(brew --prefix icu4c)/lib/pkgconfig:$(brew --prefix krb5)/lib/pkgconfig:$(brew --prefix libedit)/lib/pkgconfig:$(brew --prefix libxml2)/lib/pkgconfig:$(brew --prefix openssl)/lib/pkgconfig"

- name: Add bison path for macOS
    if: matrix.os == 'macos-latest'
    run: echo "::add-path::$(brew --prefix bison)/bin"

brewでの必要なライブラリのインストールと、パスの設定みたいです。

$ brew install autoconf automake bison freetype gettext icu4c krb5 libedit libiconv libjpeg libpng libxml2 libzip pkg-config re2c zlib

asdfと重複してるのも多々ありますけど、とりあえず気にせずコピってinstallします。

~/.config/fish/config.fish
set PKG_CONFIG_PATH /usr/local/opt/icu4c/lib/pkgconfig /usr/local/opt/krb5/lib/pkgconfig /usr/local/opt/libedit/lib/pkgconfig /usr/local/opt/libxml2/lib/pkgconfig /usr/local/opt/openssl@1.1/lib/pkgconfig $PKG_CONFIG_PATH
set PATH /usr/local/opt/bison/bin $PATH

fishの設定に上記を追加します。
workflow.yml には (brew --prefix libxml2) みたいな感じで brew 経由でpathを取得する感じになってますが、重いので別途叩いたpathをコピペしてます。

で、ここまできたら完璧!と思いきや、composerのインストールにwgetが必要なので、それも入れておきます。

$ brew install wget

これで準備は完璧!(なはずです)

PHPのインストール

あとは普通にphpをasdf経由でビルドすればいいはずです。

# バージョンは適当に指定してください
$ asdf install php 7.x.x
$ asdf global php 7.x.x

これで完璧です(たぶん)

$ which php
/Users/xxxxx/.asdf/shims/php
$ which composer
/Users/xxxxx/.asdf/shims/composer
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP初級編 〜関数・フォーム〜

学んだこと

既存の関数の呼び出し方

カウント系:strlen()は文字数、count()は配列の要素数を数えてくれる

カウント系で似ているなと思ったこの2つだが、
strlen()は何かの文字列or変数を、
count()は配列を、
引数に入れるものっぽい。

rand()

これも便利な関数。

index.php
echo rand(1,4);

こうすると、1〜4のランダムな整数を返してくれるみたい。

関数の定義

関数定義の書き方はそんなに難しくない。
戻り値がいらない場合は

index.php
function 関数名(引数1,引数2){
  /*処理*/
}

こんな感じ。
で、
関数名(引数1,引数2);
の形式で呼び出せばOK

戻り値が必要な場合は、

index.php
function 関数名(引数1,引数2){
  /*処理*/
  return 戻り値;
}

って感じで書けばOK。
呼び出し方は戻り値がない時と同じ。

ちなみに引数がなくてもOK
関数名();
みたいに。

フォーム

formタグ

index.php
<form action="url" method="post">
//フォームの内容を書く
</form>

こんな感じでかく。タグの書き方覚えちゃえばHTMLと同じ感じだ。

methodは送信方法で、
送信先("url")に表示したい場合はgetを。

テキストボックス:一行文なら<input>でtype="text"、改行を含む大きいフォームは<textarea>を使う

よく、問い合わせフォームとかで
名前とかEmailを入力する時がある。
それはだいたい一行だから<input>を使う

よく、問い合わせフォームとかで、
フリー記入欄がある。備考とか。
それは<textare>をつ会う

その作り方は、inputタグでtypeをtextに指定すればできる。

まず、一行のテキストボックス。

index.php
<form action="sent.php" method~"post">

  Emailを入力してください
  <input type="text" name="email">

</form>

こうすると、
「Emailを入力してください」
という文字の下に一行分の入力フォームが表示される。

一方、広めの改行ありのテキストボックスは、

index.php
<form action="sent.php" method~"post">

<textarea name="content"></textarea>

</form>

★ここで注意したいのは、
<input>は閉じなくていい。
<textare>は閉じないといけない。
ってこと。

<input>
<input></input>

<textarea>
<textarea></textarea>

ややこし。。。
”inputが例外だな”って覚えるしかないか。

送信ボタンの書き方はHTMLと同じ。

送信ボタンは<form>の中で

index.php
<input type="submit" value="送信">

と書けばOK

フォームのデータの受け取りは$_POST['データ名']で受け取る

フォームで

index.php
<form action="sent.php" method="post">

Emailを入力してください
<input type="text" name="email">

....

</form>

となっていた時、
formタグで送信urlに指定したsent.phpの方では、

sent.php
echo $_POST['email'];

とすると、フォームで入力された情報が受け取れる。

セレクトボックスの作り方も基本HTMLと同じ

index.php
<select>
<option value="apple">りんご</option>
<option value="banana">ばなな</option>
<option value="orange">みかん</option>
</select>

★ここで理解したいのは、
<option></option>で挟まれた文字(ここではひらがなの「りんご」や「」ばなな」や「みかん」)は、
プルダウンの選択に表示される文字にすぎない。

実際にデータとして送信されるのは、valueで指定した、「apple」「banana」「orange」である。

応用

for文などのループを使ってもかける

書き方はこんな感じ

index.php
for($i=1; $i<4; $i++){
  echo '<option value='{$i}'>{$i}</option>';
}

★ポイント
・echoで表示させるので、option全体を' で囲う。
・変数は{}で囲って{$i}で表現
・タグ内の変数は、{$i}をさらに'で囲う。'{$i}'こんな感じ。

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

[学習11日目]PHP初級編 〜関数・フォーム〜

学んだこと

既存の関数の呼び出し方

カウント系:strlen()は文字数、count()は配列の要素数を数えてくれる

カウント系で似ているなと思ったこの2つだが、
strlen()は何かの文字列or変数を、
count()は配列を、
引数に入れるものっぽい。

rand()

これも便利な関数。

index.php
echo rand(1,4);

こうすると、1〜4のランダムな整数を返してくれるみたい。

関数の定義

関数定義の書き方はそんなに難しくない。
戻り値がいらない場合は

index.php
function 関数名(引数1,引数2){
  /*処理*/
}

こんな感じ。
で、
関数名(引数1,引数2);
の形式で呼び出せばOK

戻り値が必要な場合は、

index.php
function 関数名(引数1,引数2){
  /*処理*/
  return 戻り値;
}

って感じで書けばOK。
呼び出し方は戻り値がない時と同じ。

ちなみに引数がなくてもOK
関数名();
みたいに。

フォーム

formタグ

index.php
<form action="url" method="post">
//フォームの内容を書く
</form>

こんな感じでかく。タグの書き方覚えちゃえばHTMLと同じ感じだ。

methodは送信方法で、
送信先("url")に表示したい場合はgetを。

テキストボックス:一行文なら<input>でtype="text"、改行を含む大きいフォームは<textarea>を使う

よく、問い合わせフォームとかで
名前とかEmailを入力する時がある。
それはだいたい一行だから<input>を使う

よく、問い合わせフォームとかで、
フリー記入欄がある。備考とか。
それは<textare>をつ会う

その作り方は、inputタグでtypeをtextに指定すればできる。

まず、一行のテキストボックス。

index.php
<form action="sent.php" method~"post">

  Emailを入力してください
  <input type="text" name="email">

</form>

こうすると、
「Emailを入力してください」
という文字の下に一行分の入力フォームが表示される。

一方、広めの改行ありのテキストボックスは、

index.php
<form action="sent.php" method~"post">

<textarea name="content"></textarea>

</form>

★ここで注意したいのは、
<input>は閉じなくていい。
<textare>は閉じないといけない。
ってこと。

<input>
<input></input>

<textarea>
<textarea></textarea>

ややこし。。。
”inputが例外だな”って覚えるしかないか。

送信ボタンの書き方はHTMLと同じ。

送信ボタンは<form>の中で

index.php
<input type="submit" value="送信">

と書けばOK

フォームのデータの受け取りは$_POST['データ名']で受け取る

フォームで

index.php
<form action="sent.php" method="post">

Emailを入力してください
<input type="text" name="email">

....

</form>

となっていた時、
formタグで送信urlに指定したsent.phpの方では、

sent.php
echo $_POST['email'];

とすると、フォームで入力された情報が受け取れる。

セレクトボックスの作り方も基本HTMLと同じ

index.php
<select>
<option value="apple">りんご</option>
<option value="banana">ばなな</option>
<option value="orange">みかん</option>
</select>

★ここで理解したいのは、
<option></option>で挟まれた文字(ここではひらがなの「りんご」や「」ばなな」や「みかん」)は、
プルダウンの選択に表示される文字にすぎない。

実際にデータとして送信されるのは、valueで指定した、「apple」「banana」「orange」である。

応用

for文などのループを使ってもかける

書き方はこんな感じ

index.php
for($i=1; $i<4; $i++){
  echo '<option value='{$i}'>{$i}</option>';
}

★ポイント
・echoで表示させるので、option全体を' で囲う。
・変数は{}で囲って{$i}で表現
・タグ内の変数は、{$i}をさらに'で囲う。'{$i}'こんな感じ。

progateメモ

現在Lv. 18
bbpnuts21

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

【PHP】カンマ区切り(CSV)データ処理にループと配列と分岐をなくして高速化

データベースでカンマ区切り(CSV)のデータが入っていることがあります。
例えばこんな感じ( 6,8,10,12,15,18,19,20 )のデータです。

カンマ区切りデータは、PHPでよく配列に変換してループを回して処理します。
どの言語でもそうですが、配列やループは処理が遅くなりがちです。
そんな書き方ではいつまでも成長できないので、今回は他人のソースのリファクタリングに挑戦してみました。

今回のコードの前提

現在の時間が、処理すべき時間のリストに入っているか確認して、bool値で返します。
毎時バッチ処理や毎時アラームをイメージしていただければわかりやすいと思います。

リファクタリング前

$hoursList には、カンマ区切りの『時』リストが入ります。例: 6,8,10,12,15,18,19,20
$nowHour には、現在の『時』が入っています。例:15
戻り値は $hoursList 内に $nowHour があれば true 、なければ false です。

function checkHour($hoursList, $nowHour) {
    $ret = false;

    if ($hoursList == "") {
        return $ret;
    }

    $arrHoursList = explode(",", $hoursList);
    foreach ($arrHoursList as $hour) {
        if ($nowHour == $hour) {
            $ret = true;
        }
    }
    return $ret;
}

リファクタリング結果

1行で書けますね。

function checkHour($hoursList, $nowHour) {
    return (strpos(','.$hoursList.',', ','.$nowHour.',') !== false);
}

リファクタリングの過程

改めて、もともとのコードがこちらです。

function checkHour($hoursList, $nowHour) {
    $ret = false;

    if ($hoursList == "") {
        return $ret;
    }

    $arrHoursList = explode(",", $hoursList);
    foreach ($arrHoursList as $hour) {
        if ($nowHour == $hour) {
            $ret = true;
        }
    }
    return $ret;
}

ループは極力減らす

ループ処理はプログラムの中でも速度が遅くなる元凶です。
ループでの繰り返し処理回数を減らせば減らすほど、プログラムの速度は早くなり無駄なリソースを使いません。

まずは、配列をすべて確認している無意味なコードを修正します。
配列の中で検索しているものがヒットしたのであれば、その場で true を戻し処理を終えるべきです。

function checkHour($hoursList, $nowHour) {
    $ret = false;

    if ($hoursList == "") {
        return $ret;
    }

    $arrHoursList = explode(",", $hoursList);
    foreach ($arrHoursList as $hour) {
        if ($nowHour == $hour) {
            return true;
        }
    }
    return $ret;
}

変数や配列は最小限にする

変数はプログラムを追跡する際に、常に気にかけてなければならない対象となります。
変数が少なければ少ないほど、コードレビューのときに負担が減ります。
また、配列はメモリ消費量が大きいのでできるだけ使用は控えたいものです。

今回は $ret はなくても動作するので削除します。

function checkHour($hoursList, $nowHour) {
    if ($hoursList == "") {
        return false;
    }

    $arrHoursList = explode(",", $hoursList);
    foreach ($arrHoursList as $hour) {
        if ($nowHour == $hour) {
            return ture;
        }
    }
    return false;
}

別のアルゴリズムを検討する

iffor などはプログラムの基本です。
これは私の好みですが、分岐や繰り返し処理は最低限しか使わないほうが好きです。
(特に else は使わなくてもいいケースがほとんど。)
分岐や繰り返しはテストケースの増大も招く恐れもあります。

今回のケースは、文字列の検索で解決できそうです。
例えば、6,8,10,12,15,18,19,20 という時間を表すカンマ区切りの文字列があったとします。
9時が対象になっているかどうかは、9時が上記の文字列の中に入っているかを検索すればよいだけの話なのです。

ここで大事なのは、9 を検索してはいけません。
上記の文字列を見てもらうとわかりますが、199 がヒットしてしまいます。
,9, のように、カンマで区切られたところを含めて検索すればよいのです。

更に落とし穴があります。上記の検索キーでは先頭と末尾が検索対象から外れてしまいます。
ですので、文字列にもひと工夫してあげます。
6,8,10,12,15,18,19,20先頭と末尾にカンマを加えて ,6,8,10,12,15,18,19,20, としてあげるのです。
これで配列に分解してループを回さなくても、同じ判定処理ができるようになります。

また、strpos 関数は文字列が見つからなかったときに false を返す仕様となっています。
https://www.php.net/manual/ja/function.strpos.php
これを利用することで false が返ってきていないなら一律 true と判定することができます。

ついでにガード節も不要となったので削除します。
ループと配列、分岐の処理をなくすことができました。

function checkHour($hoursList, $nowHour) {
    return (strpos(','.$hoursList.',', ','.$nowHour.',') !== false);
}

最後に

見直してみると、逆にリファクタリングしすぎですね。。。

あと今回のケースは、できるならテーブルの正規化が正解でしょう!
カンマ区切りのデータが入っているテーブルなんて、非正規化テーブルと同じですから。。。

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

Ubuntu 18.04.4 LTS、Apache/2.4.29、php 7.2.24で、HTTP/2に対応する方法

はじめに

まず最初に理解しておくことは、
UbuntuのApacheデフォルト設定の"prefork" MPM (Multi-Processing Module)は、HTTP/2をサポートしていないということです。
場所: /etc/apache2/mods-enabled/mpm_prefork.load

したがって、単純にHTTP/2の設定をするだけでなく"prefork" MPMモジュールも別のものに切り替える必要があります。

下記がその全手順です。

fpm版のphpインストール

デフォルトのphpが、preforkモジュールに依存しているので、fpm版のphpに切り替えます。

sudo apt install php7.2-fpm
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php7.2-fpm
sudo a2dismod php7.2
sudo service apache2 restart

preforkモジュールからeventモジュールに切り替え

preforkモジュールをeventモジュールに切り替える

sudo a2dismod mpm_prefork
sudo a2enmod mpm_event
sudo service apache2 restart
sudo service php7.2-fpm restart

HTTP/2の設定

/etc/apache2/sites-available/000-default-le-ssl.conf
<VirtualHost>に下記を追加します。

Protocols h2 h2c http/1.1

http2モジュールをONに

http2モジュールをONにします。

sudo a2enmod http2
sudo service apache2 restart

以上です。お疲れさまでした。

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

PHPerKaigi 2020 セッション参加メモ

PHPerKaigi 2020で聞いたセッションの記録です。

日次・場所

  • 日次: 2020-02-09 ~ 2020-02-11 (参加したのは2日目)
  • 練馬区立区民・産業プラザ Coconeriホール

余談ですが、駅近くに唐苑という、大きい黒酢豚をナイフとフォークで切って食べるランチが900円ちょっとで食べられます。
お肉が柔らかくて美味しいのでおすすめ。

E2Eテストに向き合う

スライド

https://speakerdeck.com/sizuhiko/phperkaigi2020

Testing Pyramid

  • 10% E2E Test
  • 20% Integration Test
  • 70% Unit Test

A Software Testing By Alister Scott

下に行くほど、テストスイート数の割合が少なくなるのがベスト

  • Automated Unit Tests
  • Automated Component Tests
  • Automated Integration Tests
  • Automated API Tests
  • Automated e2e Tests
  • Manual Test​

手動でテストを行わなくて良いわけではなく、最終段階として手動のテストが入る。
E2Eのテストスイート数 > Unitのテストスイート数となってちゃんと理解していない人がたまにいる。
Testing Pyramidを理解しよう

Practical Test Pyramid

26 Feburuary 2018
Javaのソースコードかつ英語だけど、テストの粒度とか流れとかが理解できるので読んでみると良い。

リンク
https://martinfowler.com/articles/practical-test-pyramid.html

UIテスト vs E2Eテスト

同じものと言われることがある。
アプリケーションをE2Eでテストすることは、多くの場合UIを介してテストを実行することを意味する。ただし逆は当てはまらない。

UIのテストはE2Eで行う必要はなく、バックをスタブ化させてフロントエンドをユニットテスト化させる。

E2Eテストでは

メンテナンスコストが高いのでなるべく最小限に抑える。
ユーザーがアプリケーションで行う価値の高い部分や、中核の部分をテストする。
パフォーマンスのテストとかは見落としがちだが、E2Eテストでできる(デプロイしたら7秒かかるようになってしまったとか)。

Tools

Selenium Web Driver

今まで一番メジャーだった。

Puppeteer

Node.jsからChromeを操作。
npmでブラウザがインストールされるので環境構築が必要ない。

Puphpeteer

PHPからpuppeteerを動かすライブラリ。

その他

  • jest Puppeteer
  • puppetry
  • Puppeteer firefox

これまでの課題を解決するPuppeteer

Webテストは遅いという都市伝説。
Browser Context APIで解決。
テストがFlakyという都市伝説。

waitforで壊れやすい。
WebDriverは判定処理がないので、Seleniumが定期的にチェックに行き結果的にタイムアウトすることがしばしば。
waitForX API で解決(Puppetterではブラウザ側で実装されている)。

どこでテストが失敗したかわからない。

Playwright

2ヶ月前くらいにマイクロソフトのリポジトリにinitial commitされる。
Puppeteerの反響を受けて作り始めた。
Puppeteerの開発チームのメンバーが何人か入ってやっている。
PuppeteerはGoogleの管理。

組織に広めるには

例えば、QAチームが毎回やっているリグレッションテストを置き換えてみる。
その上でどのくらいQAの作業が軽減されるかを事実ベースで話すのが良い。

もっと気軽にOSSにプルリクを出そう

スライド

https://speakerdeck.com/dqneo/lets-make-a-pr-to-oss-more-easily

4つの着眼点

  • 作者の関心ゾーンの外側を見る
  • 使っていなくてもコントリビュート
  • 業務で得た知識を横展開
  • ダメ元でも送ってみる

作者の関心ゾーンの外側を見る

  • メインのコードの設計、実装、可読性(関心ゾーン
  • メインのコードの異常ケース
  • コメント、ドキュメント
  • テストコードの可読性、コメント
  • CI選定、環境変化への追従

PR説明欄を丁寧に書く

相手をまず肯定して、「でもうまく動かないからこうしてみたよ」みたいな感じ。
"I think" を使うと、断定より表現が柔らかくなるので便利。

ケース別の例

関心ゾーン以外

  • テストコードの可読性
  • パラメータの可読性向上
  • 文章の間違いを修正
  • 異常ケース ​躊躇しないでシュッとPR作ってみるのも大事

環境変化への追従

ライブラリはほおっておくだけで古くなる
* PHPのバージョン
* PHPUnitのバージョン
* 新しいPSRが策定される
* travis.ymlにPHPのバージョンを追加したりなど
* php_cs.distを追加したり

業務で得たノウハウを横展開

  • PHPUnit ver4 → ver7 にあげた
  • 古いPHPUnitに依存したライブラリが多数あったりなど

使っていなくてもコントリビュート

読むだけでも見つかる
PHPStormの警告で気づいたり

ダメ元でも送ってみる
失うものはない

やった方が良いのにたまたま誰もやっていなかったケースもある
Goコンパイラ変数名をリファクタなど​

PHPの現場 公開収録

PHPの現場ホームページ

https://php-genba.shin1x1.com/

最近よくご登壇される成瀬さんという方を交えた対談。
新しい書籍を出すので、その話も絡めてのインタビュー。

クリーンアーキテクチャ

チームになった時に「こういう時はこうすれば良い」の答えが出やすいよう明示されているアーキテクチャ。
10年後を見越して書くべき場所がちゃんと明示されているアーキテクチャを選定する

あらゆる層を実装しなければいけないので、普通に書いていると時間がかかる。
そこはコードジェネレーターなどを使って、エンジニアリングの力で解決する。
手数を減らす意味合いもあるけど、開発者にちゃんとしたアーキテクチャの雛形を指し示す意味合いもある。

組織に広めていくのは信頼を得て空気を作っていくしかない。
「あの人外で登壇もしてるし」などなど。
結局人と人の関係の問題。ソフトウェアは人の要因が強い。

Webとか記事でクリーンアーキテクチャに入門し、そのあと本を読むと、自分の中で「ここってこうだからだろうな」という裏付けがされた状態で本を読むことになる。
そうすると、本から得た知識が自分で考えた知識のように自信が持てる。

クリーンアーキテクチャはパターンが決まっているので、コードの自動生成と相性が良い。

DDD

書籍を出そうと思ったきっかけ。
主題はモデリングだが、パターンでつまづいている人が多い。
パターンとしてはさほど珍しいものではない。
Webだと本当に知って欲しい人たちに届かない。興味がある人は検索してくれたりしてキャッチアップしてくれるが。

Webで省いている根底にある考え方などを噛み砕いて説明しているのが今回の書籍。

人によって解釈が違うところが出てしまいやすい。
パターンとモデリングの話は別。
パターンに興味のない人がコードを書くより、パターンを知っている人がコードを書く方が害はない。

DDDのパターンを使った実装と、ドメインを中心に考える話は全く別物。
前者をDDDと言ってしまうのはちょっと恐ろしい。

よく相談受けるつまづきどころは、実装するの大変ということ。
回答としては、「コードジェネレータ作ろう」ということ。
面倒な部分はツールを作ろう。

ユビキタス言語は、お互いが意思疎通するための共通基盤としての言語。
例えば海外に行った時のボディランゲージはユビキタス言語。
どっちかの言葉でも単語でもなく、お互いがコミュニケーションを取るためのもの。

ビジネスがドメインになるので、ビジネス側の言葉を使うことが多い。
エキスパート側の言葉を開発側が使いにくかったりしたら、「使いにくいから少し変えよう」と持ち上げなければいけない。
捉え方をお互い見つめ直す。言葉の定義が揺れていないかなど。

コードを見れば業務がわかるのが理想。​
コードを書くときは英語になるが、日本語との紐付けに関しては、変換Excelファイルを作ったりする必要がある。
そこは仕方ない。

知らないWebアプリケーションの開発に途中からjoinした時、どこから切り込むか

スライド

https://speakerdeck.com/k1low/phperkaigi-2020

ゴール

  • 途中joinについて改めて考える
  • 途中joinのノウハウ共有のきっかけを作る

joinしたサービスの概要

  • PHP + MySQL
  • 本番、stg、共有開発環境
  • 開発環境はVagrantで用意
  • インフラもChefで
  • 歴史が長いサービス

開発にとりかかれなかった

一通り技術スタック的には問題ない

ゼロから開発する時

  1. サービスの概要
  2. 全体のアーキテクチャを決める
  3. データ保持、テーブル設計を考える
  4. 開発環境の種類(本番,stg)
  5. ソフトウェア側のアーキテクチャ

の順で決める​。
途中joinの場合は1、3、5を知らない状態。

「どこから切り込むか?」の回答は、「どのようなサービスか」を知るところから。

「どのようなサービスか」がなぜ必要か

開発をする上での気づきを多くするため。

機能を1つ作る時、その機能だけでサービスが成り立つだけではないため(他との連携)。
「この機能だと、こことバッティングしませんか?」など。

「どのようにデータを保持するか」がなぜ必要か

サービスの機能や実装上の制約の情報が色濃く出ているのはコードではなくデータベースのテーブル設計。
Webアプリケーションは極論してしまえばデータの出し入れ。
つまり、技術スタックの知識があったとしても、開発開始までには手間と時間がかかる。

開発開始までのオーバーヘッドを小さくすることを目指す。

開発開始までのオーバーヘッドを小さくするメリット

  • 属人性が薄くなる
  • サービス単位ではなく、その時のプロジェクト単位で動ける
  • スペシャリストの結成ができる

削減パターン

  • joinするメンバーが工夫して削減するパターン
  • サービスとして削減するパターン​

実践パターン事例

スコープを絞る

  • 「ユーザーテーブルってどれ?」
  • 「リクエストのエントリポイント教えて」

専用Slackチャンネル作る

「私が通る開発開始までの道のりはきっと次の人も通る」

閉じた開発環境を手に入れる

  • メール送信機能をメール送信サーバーを使用せずできるように
  • 共通データベースを使わない

その他

  • デプロイのChatbot化
  • マイグレーションファイル + Pull Request

オーバーヘッドをエンジニアリングの力で解決する

オーバーヘッドが小さいことを前提とした組織を作ることができる。
ここに辿り着くまでに人や工数がかかっても、最終的には開発スピードが上がって幸せになれる。

STNS

DBドキュメントのCIと周辺情報のリンクを管理

tbls

CIに組み込めるので、ドキュメントの更新忘れを機械的に防ぐことができる。
ドキュメントをMarkdown出力できる。

全体を通しての所感

  • 当日の参加者120名だったこともあり、大規模カンファレンスという感じではなく、中くらいの規模で参加者同士のコミュニケーションを大切にする空気感だった
  • 自動テストとかチーム開発とか、組織づくりに関するセッションが多かった印象
  • 気軽に参加しやすい感じだったので、普段行きづらいと感じている方には是非来年参加してほしい
  • ノベルティのトートバッグがデニム生地っぽくてオシャレだった
  • 来年も参加しよう

リンク

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

laravel-async-queueでLaravelの非同期キューを小さく使い始める

Laravelでキューを使うにはキューワーカーを立てる必要がありますが、キューワーカーのプロセス監視方法や、キューワーカーを使用しているアプリケーションのデプロイ方法を考える必要があり、結構面倒臭いです。
キューワーカーが不要な同期キュードライバもありますが、これだと非同期処理の旨味がありません。

laravel-async-queue

https://github.com/barryvdh/laravel-async-queue

laravel-async-queueというパッケージが非同期キュードライバを提供しています。
この非同期キュードライバを使うと、ジョブをバックグラウンドプロセスで即時に実行できるようになり、キューワーカーを立てることなく非同期処理の旨味を得ることができます。

インストール方法

なお、この記事では下記のバージョンのソフトウェアを使用しています。

ソフトウェア バージョン
PHP 7.2.12
Laravel 5.5.44
laravel-async-queue v0.7.3

laravel-async-queueをComposerでインストールします。

composer require barryvdh/laravel-async-queue

config/app.phpにサービスプロバイダを追加します。

config/app.php
Barryvdh\Queue\AsyncServiceProvider::class,

config/queue.phpdefaultオプションをasyncに変更します。

config/queue.php
'default' => 'async', 

さらにconnectionsオプションに非同期キュードライバ用の設定を追加します。

config/queue.php
'async' => [
    'driver' => 'async',
    'table' => 'jobs',
    'queue' => 'default',
    'expire' => 60,
],

動作テスト

動作テストをしてみます。

テスト用のコントローラとイベントリスナーとイベントを用意します。

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

namespace App\Http\Controllers;

use App\Events\TestEvent;
use App\Http\Controllers\Controller;
use Log;

class TestController extends Controller
{
    public function test()
    {
        Log::debug('1');
        event(new TestEvent());
        Log::debug('3');
    }
}
app/Listeners/TestListener.php
<?php

namespace App\Listeners;

use App\Events\TestEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Log;

class TestListener implements ShouldQueue
{
    public function handle(TestEvent $event)
    {
        Log::debug('2')
    }
}
app/Events/TestEvent
<?php

namespace App\Events;

class TestEvent
{
}
app/Providers/EventServiceProvider.php
protected $listen = [
    'App\Events\TestEvent' => [
        'App\Listeners\TestListener',
    ],
];

コントローラのアクションを実行すると、下記のようなログが出力されます。

[2020-02-13 23:36:45] local.APP.DEBUG: 1 [] {"uid":"93eb38d","process_id":1017}
[2020-02-13 23:36:45] local.APP.DEBUG: 3 [] {"uid":"93eb38d","process_id":1017}
[2020-02-13 23:36:47] local.APP.DEBUG: 2 [] {"uid":"535fd9a","process_id":1046}

1→3→2となっており、非同期処理されていることがわかります。
プロセスIDとUIDもコントローラとイベントリスナーで別になっています。

ちなみに同期ドライバの場合です。

[2020-02-13 23:38:22] local.APP.DEBUG: 1 [] {"uid":"a9173e4","process_id":1020}
[2020-02-13 23:38:22] local.APP.DEBUG: 2 [] {"uid":"a9173e4","process_id":1020}
[2020-02-13 23:38:22] local.APP.DEBUG: 3 [] {"uid":"a9173e4","process_id":1020}

1→2→3となっており、同期処理です。
もちろんプロセスIDとUIDもコントローラとイベントリスナーで同じです。

失敗ジョブを扱えるようにする

非同期キュードライバは、キューワーカーを使っていないため、ジョブが1回失敗すると自動ではリトライされません。

また、ジョブ失敗ベントが発行されず、失敗したジョブはjobsテーブルに残ったままで、failed_jobsテーブルに入りません。

これだと運用がつらいので、ジョブ失敗をSlackに通知したりできるように、ジョブ失敗イベントが発行されるようにします。

イベントリスナーの$tiresプロパティを1に設定すると、ジョブ失敗ベントが発行されるようになります1

class TestListener implements ShouldQueue
{
    public $tires = 1;
    ...
}

トレイトにしておくと便利かもしれません。

trait AsyncQueueable
{
    public $tires = 1;
}
class TestListener implements ShouldQueue
{
    use AsyncQueueable;
    ...
}

これでジョブ失敗イベントが発生するようになります。
同時に、失敗ジョブがjobsテーブルから消えるようになります。

ただ、このままだと失敗ジョブがjobsテーブルにもfailed_jobsテーブルにも残らなくなってしまうので、失敗ジョブをfailed_jobsテーブルに入れる処理をジョブ失敗イベントのコールバックとして書くことにします。
これはAppServiceProviderbootメソッドに書くのがいいと思います。

app/Providers/AppServiceProvider
\Illuminate\Support\Facades\Queue::failing(function (\Illuminate\Queue\Events\JobFailed $event) {
    if ($event->job->getConnectionName() == 'async') {
        // 失敗ジョブをfaled_jobsテーブルに保存する
        $id = $this->app['queue.failer']->log(
            $event->connectionName,
            $event->job->getQueue(),
            $event->job->getRawBody(),
            $event->exception
        );
    }
    // Slackへの通知など
    ...
});

ちなみに、logメソッドはfailed_jobsテーブルのIDが返ってきます。
Slackに通知する時に使うと、後でジョブを手動でリトライする際に便利でいいと思います。

これで失敗ジョブがfailed_jobsテーブルに入るようになります。

リトライが必要な場合は、下記のArtisanコマンドを実行します。

artisan queue:retry <failed_jobsテーブルのID>

これでfailed_jobsテーブルの失敗ジョブがjobsテーブルに戻ります。

さらに下記のArtisanコマンドを実行するとジョブを実行できます。

artisan queue:async <jobsテーブルのID>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravelのキューを小さく使い始める

Laravelでキューを使うにはキューワーカーを立てる必要がありますが、キューワーカーのプロセス監視方法や、キューワーカーを使用しているアプリケーションのデプロイ方法を考える必要があり、結構面倒臭いです。
キューワーカーが不要な同期キュードライバもありますが、これだと非同期処理の旨味がありません。

laravel-async-queue

https://github.com/barryvdh/laravel-async-queue

laravel-async-queueというパッケージが非同期キュードライバを提供しています。
この非同期キュードライバを使うと、ジョブをバックグラウンドプロセスで即時に実行できるようになり、キューワーカーを立てることなく非同期処理の旨味を得ることができます。

インストール方法

なお、この記事では下記のバージョンのソフトウェアを使用しています。

ソフトウェア バージョン
PHP 7.2.12
Laravel 5.5.44
laravel-async-queue v0.7.3

laravel-async-queueをComposerでインストールします。

composer require barryvdh/laravel-async-queue

config/app.phpにサービスプロバイダを追加します。

config/app.php
Barryvdh\Queue\AsyncServiceProvider::class,

config/queue.phpdefaultオプションをasyncに変更します。

config/queue.php
'default' => 'async', 

さらにconnectionsオプションに非同期キュードライバ用の設定を追加します。

config/queue.php
'async' => [
    'driver' => 'async',
    'table' => 'jobs',
    'queue' => 'default',
    'expire' => 60,
],

動作テスト

動作テストをしてみます。

テスト用のコントローラとイベントリスナーとイベントを用意します。

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

namespace App\Http\Controllers;

use App\Events\TestEvent;
use App\Http\Controllers\Controller;
use Log;

class TestController extends Controller
{
    public function test()
    {
        Log::debug('1');
        event(new TestEvent());
        Log::debug('3');
    }
}
app/Listeners/TestListener.php
<?php

namespace App\Listeners;

use App\Events\TestEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Log;

class TestListener implements ShouldQueue
{
    public function handle(TestEvent $event)
    {
        Log::debug('2')
    }
}
app/Events/TestEvent
<?php

namespace App\Events;

class TestEvent
{
}
app/Providers/EventServiceProvider.php
protected $listen = [
    'App\Events\TestEvent' => [
        'App\Listeners\TestListener',
    ],
];

コントローラのアクションを実行すると、下記のようなログが出力されます。

[2020-02-13 23:36:45] local.APP.DEBUG: 1 [] {"uid":"93eb38d","process_id":1017}
[2020-02-13 23:36:45] local.APP.DEBUG: 3 [] {"uid":"93eb38d","process_id":1017}
[2020-02-13 23:36:47] local.APP.DEBUG: 2 [] {"uid":"535fd9a","process_id":1046}

1→3→2となっており、非同期処理されていることがわかります。
プロセスIDとUIDもコントローラとイベントリスナーで別になっています。

ちなみに同期ドライバの場合です。

[2020-02-13 23:38:22] local.APP.DEBUG: 1 [] {"uid":"a9173e4","process_id":1020}
[2020-02-13 23:38:22] local.APP.DEBUG: 2 [] {"uid":"a9173e4","process_id":1020}
[2020-02-13 23:38:22] local.APP.DEBUG: 3 [] {"uid":"a9173e4","process_id":1020}

1→2→3となっており、同期処理です。
もちろんプロセスIDとUIDもコントローラとイベントリスナーで同じです。

失敗ジョブを扱えるようにする

非同期キュードライバは、キューワーカーを使っていないため、ジョブが1回失敗すると自動ではリトライされません。

また、ジョブ失敗ベントが発行されず、失敗したジョブはjobsテーブルに残ったままで、failed_jobsテーブルに入りません。

これだと運用がつらいので、ジョブ失敗をSlackに通知したりできるように、ジョブ失敗イベントが発行されるようにします。

イベントリスナーの$tiresプロパティを1に設定すると、ジョブ失敗ベントが発行されるようになります1

class TestListener implements ShouldQueue
{
    public $tires = 1;
    ...
}

トレイトにしておくと便利かもしれません。

trait AsyncQueueable
{
    public $tires = 1;
}
class TestListener implements ShouldQueue
{
    use AsyncQueueable;
    ...
}

これでジョブ失敗イベントが発生するようになります。
同時に、失敗ジョブがjobsテーブルから消えるようになります。

ただ、このままだと失敗ジョブがjobsテーブルにもfailed_jobsテーブルにも残らなくなってしまうので、失敗ジョブをfailed_jobsテーブルに入れる処理をジョブ失敗イベントのコールバックとして書くことにします。
これはAppServiceProviderbootメソッドに書くのがいいと思います。

app/Providers/AppServiceProvider
\Illuminate\Support\Facades\Queue::failing(function (\Illuminate\Queue\Events\JobFailed $event) {
    if ($event->job->getConnectionName() == 'async') {
        // 失敗ジョブをfaled_jobsテーブルに保存する
        $id = $this->app['queue.failer']->log(
            $event->connectionName,
            $event->job->getQueue(),
            $event->job->getRawBody(),
            $event->exception
        );
    }
    // Slackへの通知など
    ...
});

ちなみに、logメソッドはfailed_jobsテーブルのIDが返ってきます。
Slackに通知する時に使うと、後でジョブを手動でリトライする際に便利でいいと思います。

これで失敗ジョブがfailed_jobsテーブルに入るようになります。

リトライが必要な場合は、下記のArtisanコマンドを実行します。

artisan queue:retry <failed_jobsテーブルのID>

これでfailed_jobsテーブルの失敗ジョブがjobsテーブルに戻ります。

さらに下記のArtisanコマンドを実行するとジョブを実行できます。

artisan queue:async <jobsテーブルのID>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP 三項演算子

目的

  • 三項演算子の概要をまとめる

書き方の例

  • 下記に三項演算子の例を記載する。
$変数 = (条件式) ? 真の時変数に格納する値 : 偽の時変数に格納する値;
  • 下記に前述の三項演算子をif文で書く例を記載する。
if (条件式) {
  $変数 = 真の時変数に格納する値;
} else {
  $変数 = 偽の時変数に格納する値;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む