20201218のPHPに関する記事は20件です。

php基本情報

はじめに

このページには基本情報を載せていく。

ブラウザでMAMPを開く方法

 
localhostって打つと…
スクリーンショット 2020-12-18 22.59.20.png

こんな感じでパスを指定しなくても楽に開けるよ。

MAMPのポート指定

デフォルトでは8888番になっているので、80番に変更しよう。スクリーンショット 2020-12-18 23.02.04.png
こんな感じにしよう。

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

【備忘録】フィボナッチ数列について

フィボナッチ数列について

直接的な言語で出てくることがあるのかは、今後に任せるとして…
このような数列があるということだけは覚えておきたい。

1,1,2,3,5,8,13,21,34...

n番目の数(n-1)番目と(n-2)番目の数の和からなっている数列。
この数列を覚えるというよりは、算出するときの考え方・頭の柔らかい使い方を覚えておきたい。

$a = 1;
$b = 0;
// 以下部分をwhile文だったりfor文だったりでループさせる。
$s = $a + $b; // この値をechoで出力したりする。そしてこの後の考え方が重要だと思った。
$a = $b;
$b = $s; // $aに$bを、$bに$sを入れていくことで、n-1番目とn-2番目を表現していくことができる。

とても素敵な考え方だと思った。
覚えておきたい考え方…多いなぁ…

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

AtCoder Beginners SelectionのPHPでの解答例

AtCoder Beginners SelectionをPHPで解いてみたのでメモしておきます。時間があったら、解説を加えていきたいです...。

(ないと思いますが)ご質問や、(あると思いますが)ご指摘があれば、コメント欄にてお願いします。

Welcome to AtCoder

PHP
<?php
    fscanf(STDIN, '%d', $a);
    fscanf(STDIN, '%d %d', $b, $c);
    fscanf(STDIN, '%s', $s);

    echo $a + $b + $c . " " . $s . "\n";

?>

ABC086A - Product

PHP
<?php
    fscanf(STDIN, "%d %d", $a, $b);
    $x = $a * $b;

    echo $x % 2 == 1 ? "Odd\n" : "Even\n"; //三項演算子。下記if文と同義。

    /*if ($x % 2 == 1) {
        echo "Odd";
    } else {
        echo "Even";
    }*/
?>

ABC081A - Placing Marbles

PHP
<?php
    fscanf(STDIN, "%s", $s);
    echo substr_count($s, "1") . "\n";
?>

ABC081B - Shift only

PHP
<?php
    fscanf(STDIN, '%d', $n);
    $array = explode(" ", trim(fgets(STDIN)));
    $n = count($array);
    $count = 0;
    $continue = TRUE;

    while ($continue == TRUE) {
        for ($i = 0; $i < $n; $i++) {
            $array[$i] = (int)$array[$i];
            if ($array[$i] % 2 == 1) {
                $continue = FALSE;
                break;
            }
            $array[$i] /= 2;
        }
        $continue == FALSE ? FALSE : $count++;
    } 

    echo $count . "\n";

ABC087B - Coins

PHP
<?php
    fscanf(STDIN, "%d", $a);
    fscanf(STDIN, "%d", $b);
    fscanf(STDIN, "%d", $c);
    fscanf(STDIN, "%d", $x);
    $count = 0;

    for ($i = 0; $i <= $a; $i++) {
        for ($j = 0; $j <= $b; $j++) {
            for ($k = 0; $k <= $c; $k++) {
                if (500 * $i + 100 * $j + 50 * $k == $x) {
                    $count++;
                }
            } 
        } 
    }

    echo $count . "\n";

ABC083B - Some Sums

PHP
<?php
    fscanf(STDIN, "%s %d %d", $n, $a, $b);

    for ($i = 1; $i <= $n; $i++) {
        $sum = array_sum(str_split($i, 1));
        if ($sum >= $a && $sum <= $b) {
            $array [] = $i;
        }
    }

    echo array_sum($array) . "\n";

$array[] = $i;のとこを$count += $i;でカウントアップしていくのもまた善し。

ABC088B - Card Game for Two

PHP
<?php
    fscanf(STDIN, "%d", $n);
    $stdin = explode(" ", trim(fgets(STDIN)));
    foreach ($stdin as $val) {
        $array [] = (int)$val;
    }
    rsort($array);
    $total = array_sum($array);
    $alice = 0;

    for ($i = 0; $i < $n; $i++) {
        if ($i % 2 == 0) {
            $alice += $array[$i];
        }
    }
    $bob = $total - $alice;
    echo $alice - $bob . "\n";

ABC085B - Kagami Mochi

PHP
<?php
    fscanf(STDIN, "%d", $n);
    for ($i = 0; $i < $n; $i++) {
        fscanf(STDIN, "%d", $array[$i]);
    }
    echo count(array_unique($array)) . "\n";

ABC085C - Otoshidama

PHP
<?php
    fscanf(STDIN, "%d %d", $n, $y);

    for ($i = 0; $i <= $n; $i++) {
        for ($j = 0; $i + $j <= $n; $j++) {
            $k = $n - $i - $j;
            if (10000 * $i + 5000 * $j + 1000 * $k === $y) {
                printf("%d %d %d", $i, $j, $k,) . "\n"; exit;
            }
        }
    }
    printf("%d %d %d", -1, -1, -1) . "\n";

ABC049C - 白昼夢(Daydream)

PHP
<?php
    fscanf(STDIN, "%s", $s);
    $s = strrev($s);
    while($s !== "") {
        if(substr($s, 0, 5) == "maerd") {
            $s = substr($s, 5);
            continue;
        }
        if(substr($s, 0, 7) == "remaerd") {
            $s = substr($s, 7);
            continue;
        } 
        if(substr($s, 0, 5) == "esare") {
            $s = substr($s, 5);
            continue;
        }
        if(substr($s, 0, 6) == "resare") {
            $s = substr($s, 6);
            continue;
        }
        echo "NO";
        exit();
    }
    echo "YES";
?>

ABC086C - Traveling

PHP
<?php
    fscanf(STDIN, "%d", $n);
    $t = 0;
    $xy = [0, 0];

    for ($i = 0; $i < $n; $i++) {
        fscanf(STDIN, "%d %d %d", $nt, $nxy[0], $nxy[1]);
        $dt = $nt - $t;
        $dxy = abs($nxy[0] - $xy[0]) + abs($nxy[1] - $xy[1]);
        if ($dt < $dxy) {
            exit("No\n");
        }
        if (($dt + $dxy) % 2 == 1) {
            exit("No\n");
        }
        $t = $nt;
        $xy = [$nxy[0], $nxy[1]];
    }

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

【備忘録】GCEでのLaravelの環境構築

GCEでLaravelの環境構築をよく行うので備忘録です。
間違い等ございましたら、ご指摘いただけますと幸いです。

目次

  • 構築する環境
  • PHPの導入
  • Gitの導入
  • Composerの導入
  • Apacheの導入
  • Laravelの導入
  • LaravelとApacheの設定

構築する環境

  • debian 10
  • PHP 7.3
  • Composer
  • Laravel 6.0
  • git
  • Apache2

PHPの導入

パッケージ一覧を更新

sudo apt update && sudo apt -y upgrade

PHP7.3 と 必要なPHPモジュールのインストール

sudo apt-get install php7.3 php-pear
sudo apt-get install libapache2-mod-php7.3 php7.3-xml php7.3-gd php7.3-opcache php7.3-mbstring php7.3-curl zip unzip php7.3-zip php7.3-mysql

PHPが導入されたか確認

php -v

バージョンが表示されればOK!

Gitの導入

sudo apt-get install git

Composerの導入

composer-setup.phpのダウンロード

curl -sS https://getcomposer.org/installer -o composer-setup.php

公式サイトにてHash値を確認し、変数に格納

HASH=公式サイトで確認したHash値
# (v2.0.8の場合は756890a4488ce9024fc62c56153228907f1545c228516cbf63f885e036d37e9a59d27d63f46af1d4d07ee0f76181c7d3)

php -r "if (hash_file('SHA384', 'composer-setup.php') === '$HASH') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
# 実行結果: Installer verified

sudo php composer-setup.php --install-dir=/bin --filename=composer
# 実行結果
# All settings correct for using Composer
# Downloading...

# Composer (version 2.0.8) successfully installed to: /bin/composer
# Use it: php /bin/composer

Composerが導入されたか確認

composer

# 実行結果
#    ______
#   / ____/___  ____ ___  ____  ____  ________  _____
#  / /   / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
# / /___/ /_/ / / / / / / /_/ / /_/ (__  )  __/ /
# \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
#                     /_/
# Composer version 2.0.8 2020-12-03 17:20:38

Composerの文字がデカデカと出力されればOK!!

Apacheの導入

Apacheと必要なモジュールのインストール

sudo apt-get install apache2
sudo a2dismod mpm_event
sudo a2enmod mpm_prefork
sudo systemctl restart apache2

Laravelの導入

Laravelの導入場所に移動

cd /var/www/html

/var/www/html内のユーザーの権限を変更

sudo chown -R ユーザー名 /var/www/html

既存のプロジェクトを使用する場合は、gitでcloneする
新規でプロジェクトを作成する場合は、

sudo composer create-project --prefer-dist laravel/laravel プロジェクト名 "6.*"

権限の変更

sudo chgrp -R www-data /var/www/html/プロジェクト名
sudo chmod -R 775 /var/www/html/プロジェクト名/storage

LaravelとApacheの設定

cd /etc/apache2/sites-available
sudo nano プロジェクト名.conf

プロジェクト名.confの記述は、

プロジェクト名.conf
<VirtualHost *:80>
    ServerName 使用するサーバー名(なければプロジェクト名).com

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html/プロジェクト名/public

    <Directory /var/www/html/プロジェクト名>
        AllowOverride All
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

次にsites-availableディレクトリからsites-enabledディレクトリへのシンボリックリンクの変更,
apacheの再起動を行います。

sudo a2dissite 000-default.conf
sudo a2ensite プロジェクト名.conf
sudo a2enmod rewrite
sudo service apache2 restart

以上で設定は終了です!
Laravelの設定やDBはその都度で!!

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

Laravel(多対多)でphp artisan migrateエラー(SQLSTATE[42S01]: Base table or view already exists:)が出た時

Laravelで多対多のデータベース接続を行っている際、php artisan migrateコマンドを入力すると、下記のエラーが出ました。
すでにデータベースにテーブルが存在すると言われて困惑しましたが、
一度データベースから直接テーブルを削除して、再度php artisan migrateを行うと問題なく解決しました。

何も変更を加えず、やり直すだけで解決する場合もあるので、一度試してみてください。

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

PHP の Google Cloud SDK でサポートされていない API をお手軽にリクエストする

はじめに

GameWith AdventCalendar 2020 の18日目の記事になります。

概要

今回の内容は非常にニッチですが、 PHP の Google Cloud SDK でサポートされていない API は直接リクエストをする必要があります。
直接リクエストをする場合は、 OAUTH2_TOKEN が必要になりますが発行方法について、あまり記事が見当たらなかったので、ご紹介いたします。

どうやるのか

PHP で Google Auth のライブラリが公開されているので、こちらを利用します。
https://github.com/googleapis/google-auth-library-php

また、今回は GCP のサービスアカウントの認証情報を元にソースコードの記述をしています。

セットアップ

まずは、 composer でインストールをします。

$ ./composer.phar require google/auth

google/auth の依存関係で含まれているのでインストールしなくても動作しますが、
いつ依存関係が外れるか分からないので guzzle も明示的にインストールします。

$ ./composer.phar require guzzlehttp/guzzle

ソースコード

ライブラリの準備は整ったので、早速コードを書いていきましょう。
今回は SDK サポートはされていますが、Google Cloud Storage の Bucket 一覧を取得するコードを記述いたします。

command.php
<?php
declare(strict_types=1);

use Google\Auth\ApplicationDefaultCredentials;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

require __DIR__ . '/vendor/autoload.php';

// どのリソースにアクセス出来るかスコープを設定します。
// スコープの内容は Google Cloud の API ドキュメントに記載されています。
// https://cloud.google.com/storage/docs/authentication
$scopes = ['https://www.googleapis.com/auth/devstorage.read_only'];

// 下記の middleware を guzzle に適用すると、リクエストする際に `Authorization: Bearer xxxx` を HEADER に挿入してくれます。
$middleware = ApplicationDefaultCredentials::getMiddleware($scopes);
$stack = HandlerStack::create();
$stack->push($middleware);

// 以下の API ドキュメントを参考にし、 base_uri を設定する。
// https://cloud.google.com/storage/docs/json_api/v1
$client = new Client([
    'handler' => $stack,
    'base_uri' => 'https://storage.googleapis.com',
    'auth' => 'google_auth'
]);

// バケット一覧を取得する
// https://cloud.google.com/storage/docs/json_api/v1/buckets/list
$response = $client->get('/storage/v1/b', [
    'query' => [
        'project' => '[Bucket 一覧を取得したい project id]'
    ]
]);

print_r((string) $response->getBody());

実行

credentials ファイルパスを環境変数にセットしておく必要がありますので、以下のコマンドを実行します。

$ export GOOGLE_APPLICATION_CREDENTIALS=[credentials の filepath]

準備が整ったので早速実行してみましょう。

$ php command.php

無事取得できました。

{
  "kind": "storage#buckets",
  "items": [
    {
      "kind": "storage#bucket",
      "selfLink": "https://www.googleapis.com/storage/v1/b/xxxx",
      "id": "xxx",
      "name": "xxx",
      "projectNumber": "xxx",
      "metageneration": "1",
      "location": "US",
      "storageClass": "STANDARD",
      "etag": "CAE=",
      "timeCreated": "1970-01-01T00:00:00.000Z",
      "updated": "1970-01-01T00:00:00.000Z",
      // ...
    }
  ]
}

トークンだけ取得したい場合

以下の様に CredentialsLoader を呼び出すことで取得が可能です。

ソースコード

token.php
<?php
declare(strict_types=1);

use Google\Auth\CredentialsLoader;

require __DIR__ . '/vendor/autoload.php';

// 環境変数 の GOOGLE_APPLICATION_CREDENTIALS を参照して認証情報を取得します。
$jsonKey = CredentialsLoader::fromEnv();

// どのリソースにアクセス出来るかスコープを設定します。
// スコープの内容は Google Cloud の API ドキュメントに記載されています。
// https://cloud.google.com/storage/docs/authentication
$scopes = ['https://www.googleapis.com/auth/devstorage.read_only'];

// トークンを取得します
$token = CredentialsLoader::makeCredentials($scopes, $jsonKey)->fetchAuthToken();

var_export($token);

実行

credentials ファイルパスを環境変数にセットしておく必要がありますので、以下のコマンドを実行します。

$ export GOOGLE_APPLICATION_CREDENTIALS=[credentials の filepath]

先ほど作成した、 token.php を実行します。

$ php token.php

無事取得できました。
以下の情報をもとに リクエストヘッダーに Authorization: [token_type] [access_token] を追加して API にアクセスすることも可能です。

array (
    'access_token' => 'xxx',
    'expires_in' => 3600,
    'token_type' => 'Bearer',
)

終わりに

ネタ探しにひたすら迷い続け・・・ネタを思いついて途中まで書いては、
「あ、このネタ没だ」と、スプラトゥーン2で現実逃避をし行き着いた果てがこれです :sweat_smile:
来年も機会あれば書きたいと思いますので、よろしくお願いいたします。

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

【初心者】PHPで配列の重複を削除する

array_uniqueを使ってみる。

配列内で同じ値があって、それを削除したい!
いきなりそんなことを思うなんてことはないでしょう。
ただ、私がそんな事態に直面してしまったのか、他に方法があったのかわからないが、これを使うことによって一つ問題を解くことができたので、良かった。というだけのことです。
例えば…

$array = ["A", "D", "A", "G", "B", "C", "B"];

こんな配列があったとしましょう。
この配列内で重複したものを削除したいと思った時、

array_unique(配列);

これを使ってみます。
print_r関数で出力しみます。すると、

$array = ["A", "D", "A", "G", "B", "C", "B"];
print_r(array_unique($array));
// 以下出力結果です。
Array
(
    [0] => A
    [1] => D
    [3] => G
    [4] => B
    [5] => C
)

このような出力結果となりました。
この状態だと、見てお分かりだと思いますが配列のキーが飛び飛びになってしまいます。
せっかくなので、この配列のキーもきちんとしてしまいましょう。

array_valuesも使ってみる。

array_values(配列)

この関数を使うとこのようになります。

print_r(array_values(array_unique($array)));
// 以下出力結果です。
Array
(
    [0] => A
    [1] => D
    [2] => G
    [3] => B
    [4] => C
)

これで色々なことに使える形になったような気がします。

今回の学びは私にとって大きな一歩になったような気がします。

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

Azure Cosmos DBの2つのAPIをPHPで試す

センシンロボティクス開発部の黒田です。
弊社で取り組んでいる「インフラDX」においては、いわゆるIoT的なセンサデータやドローン・UGV等で取得したマルチメディアデータの解析結果データなど、多種多様な時系列データを扱う必要があります。
そこでMicrosoft Azureの「Cosmos DB」をデータインフラとして利用できないか調査したのですが、結構概念やサービス構成がややこしかったり、PHPで利用しようとすると情報が少なかったりしたので、メモも兼ねてまとめたいと思います。

Azure Cosmos DB (旧DocumentDB)とは

いわゆるNoSQL Databaseで、AWSで言うところのDynamoDBやDocumentDBと同じ部類のマネージドデータベースですが、APIが5種類あるなど結構ややこしいところがあるので一旦整理してみました(全部詳しく調査することはできなかったので、一部不正確なまとめです)

  • 高スループットと高可用性
  • RUというスループット性能に対する課金
  • 5つのAPI(データモデル)がある(以下、個々のAPIにおけるデータモデルの関係性)
API Database Container Item
SQL/Core API Database Container Document
Cassandra API Keyspace Table Row
MongoDB API Database Collection Document
Gremlin API Database Graph Node/Edge
Table API N/A Table Entity

今回はこの中で「SQL/Core API」と「Table API」について、調査とPHP(laravel環境)でのお試し実装にトライしました。

SQL/Core API 概要

全てのデータエントリはjson形式で保存され、極めて自由度が高い非構造データをRDBライクなSQLで取り扱うことができます。
イメージしやすくするため、下記にデータとクエリの例を記載します。

[data]
{
  "id": "1608223603",
  "value": 1.25,
  "device": {
    "type": "drone",
    "name": "SENSYN DRONE 1GO"
  },
  "_rid": "I9pmAKenR8NHFwAAAAAAAA==",
  "_self": "dbs/I9pmAA==/colls/I9pmAKenR8M=/docs/I9pmAKenR8NHFwAAAAAAAA==/",
  "_etag": "0000751a-0000-2300-0000-5fdbdbb80000",
  "_attachments": "attachments/",
  "_ts": 1608244152
}

[query]
SELECT * FROM SensorData s WHERE s.device.type = 'drone'

valueやdeviceは開発者が自由に決めたプロパティで、これをSQL内で指定できるというわけです。
おそらく、多種多様なセンサデータのようにプロパティ構造が定義しずらいものには一番利用しやすいAPIになると思われます。

Table API 概要

こちらはAzure Storage Serviceというコアサービスの1つである「Azure Storage Table」と互換のAPIです。
最も考えられるユースケースとしては、既存のAzure Storage Tableを利用しているシステムにおいて、スループット・可用性向上のためにCosmos DBに移行するというケースだと思われます。
余談ですが、個人的にはこのAzure Storage Serviceが何なのか理解するのに少し時間がかかりました。
サービス名から「AWSのS3のようなものかな」と思っていたのですが、Storage Serviceはあくまでサービス群の名称であり、以下のような複数のServiceが属していたからです

  • BLOB Service : いわゆるS3と同じobject storage
  • File Service : SMBベースのファイル共有マネージドサービス
  • Queue Service : クラウドリソース間の非同期メッセージキュー
  • Table Service : ベーシックなNoSQLデータベース
  • Disk Service : 仮想ハードディスク

(マニュアルやポータルごとに「Azure BLOB」や「Azure Blob Service」や「Blob Storage」など表記揺れも散見されるので、分かり難さに拍車がかかっています。。)

それはさておき、Table APIはベーシックなNoSQLデータベースなため、SQL/Core APIのような自由度高めのデータ投入や参照はできません。
AWS DynamoDBと同じようにPartitionKeyおよびRowKey(=SortKey)を指定してしか参照できませんし、セカンダリインデックスを作ることもできません。
そのため、キーの設計は検索要件を踏まえてしっかりと設計しておく必要があります。
また、Entity Group Transaction(EGT)という複数のEntityをアトミックに操作する機能もあるのですが、「同一Partitionに属するデータのみ」という制限がついているので、トランザクション管理の観点でも設計しておく必要があります。

データとクエリの例は次のようなイメージになります。

[data]
{
  "PartitionKey": "drone",
  "RowKey": "1608223603",
  "Timestamp": "2020-12-18T10:11:12.1234567Z",
  "value": 1.25,
  "device_type": "drone",
  "device_name": "SENSYN DRONE 1GO"
}

[query(filter)]
$client->queryEntities("SensorData", "PartitionKey eq 'drone'");

クエリを含め、あらゆる操作にPartitionKeyを指定する必要があります。

試してみる

SQL/Core API

1. ポータルからCosmos DBアカウントを作成する

基本的にはフォームのデフォルト値でOK

image.png

2. コンテナの作成

image.png

image.png

ちなみにデータエクスプローラーからデータ参照・編集可能
image.png

3. PHPでのREST API呼び出し実装

残念ながらphp版のCosmos DBがサポートされたSDKはないので、素のREST APIをGuzzleなどで呼び出すしかないです。
とりあえずデータ投入とクエリ実行の実装例です。

<?php

namespace App\Services\Azure\Cosmos;

use App\Exceptions\Error;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class AzureCosmosDBClient {
    private $host = 'https://kurocosmos.documents.azure.com';
    private $key = 'Your Access Key';
    private $client;

    public function __construct() {
        $this->client = new Client();
    }

    public function createDocument(
        string $dbId, string $collId, ?string $partitionKey,
        string $json
    ) {
        $url = $this->endpoint("/dbs/{$dbId}/colls/{$collId}/docs");
        $headers = [
            'Content-Type' => 'application/json',
        ];
        if (isset($partitionKey)) {
            $headers['x-ms-documentdb-query-enablecrosspartition'] = 'False';
            $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]';
        }
        $ops = $this->authedOptions('post', 'docs', $collId, $headers, $json);
        return $this->doRequest('POST', $url, $ops);
    }

    public function queryDocuments(
        string $dbId, string $collId, ?string $partitionKey,
        string $query, array $queryParams
    ) {
        $url = $this->endpoint("/dbs/{$dbId}/colls/{$collId}/docs");
        $json = json_encode([
            'query' => $query,
            'parameters' => collect($queryParams)->map(function($v, $k){
                return [
                    'name' => "@{$k}",
                    'value' => $v,
                ];
            })->values()->toArray()
        ]);
        $headers = [
            'Content-Type' => 'application/query+json',
            'x-ms-max-item-count' => 1000,
            'x-ms-documentdb-isquery' => 'True',
            'x-ms-documentdb-query-enablecrosspartition' => 'True'
        ];
        if (isset($partitionKey)) {
            $headers['x-ms-documentdb-query-enablecrosspartition'] = 'False';
            $headers['x-ms-documentdb-partitionkey'] = '["'.$partitionKey.'"]';
        }
        $ops = $this->authedOptions('post', 'docs', $collId, $headers, $json);
        return $this->doRequest('POST', $url, $ops);
    }

    // private

    private function doRequest(string $method, string $url, array $options) {
        $resp = null;
        try {
            switch($method) {
            case 'GET':
                $resp = $this->client->get($url, $options);
                break;
            case 'PUT':
                $resp = $this->client->put($url, $options);
                break;
            case 'DELETE':
                $resp = $this->client->delete($url, $options);
                break;
            case 'POST':
                $resp = $this->client->post($url, $options);
                break;
            default:
                throw Error::InternalError("unexpected method {$method}");
            }
        }
        catch (RequestException $e) {
            \Log::error($e->getResponse()->getBody()->getContents());
            throw $e;
        }
        $content = $resp->getBody()->getContents();
        return json_decode($content);
    }

    private function endpoint(string $path): string {
        return "{$this->host}{$path}";
    }

    private function authedOptions(
        string $verb, string $resourceType, string $resourceLink,
        array $headers=[], ?string $body=null
    ): array {
        $keyType = 'master';
        $tokenVer = '1.0';
        $xMsVersion = '2018-12-31';
        $xMsDate = gmdate('D, d M Y H:i:s T');
        $sig = base64_encode($this->sig($verb, $resourceType, $resourceLink, $xMsDate));

        $options = [
            'headers' => collect([
                'Authorization' => urlencode("type={$keyType}&ver={$tokenVer}&sig={$sig}"),
                'Accept' => 'application/json',
                'Content-Length' => is_null($body) ? 0 : strlen($body),
                'x-ms-version' => $xMsVersion,
                'x-ms-date' => $xMsDate,
            ])->merge($headers)->all(),
        ];
        if (!is_null($body)) {
            $options['body'] = $body;
        }
        return $options;
    }

    private function sig(string $verb, string $resourceType, string $resourceLink, string $dateStr): string {
        $message = "{$verb}\n{$resourceType}\n{$resourceLink}\n{$dateStr}\n\n";
        return hash_hmac('sha256', strtolower($message), base64_decode($this->key), true);
    }
}

なお、上記の\$dbIdや\$collIdは、作成時に入力した名称ではなく、作成後に割り当てられた識別子のようです。
次のようにazure cliでも確認できます。

$ az cosmosdb sql container list -g myresourcegroup -a kurocosmos -d SensorData | jq
[
  {
    "id": "xxxx",
    "location": null,
    "name": "SENSYN",
    "options": null,
    "resource": {
      "_conflicts": "conflicts/",
      "_docs": "docs/",
      "_self": "dbs/I9pmAA==/colls/I9pmAKenR8M=/", # これ
      "_sprocs": "sprocs/",
      "_triggers": "triggers/",
    ...
  }
]

とりあえずlaravelのコマンド作成機能を利用し、コマンド経由で叩いてみたいと思います。

4. Write/Read実行

折角なので軽く性能もみるためにWrite操作について呼び出し間隔を変化させて呼び出してみました。
結果だけ記載します。

CosmosDB(SQL/Core API) Write
No RU wait(msec) 書き込みDocument数 処理時間 処理/sec
1 400 10 1000 22.2sec 45.0
2 400 8 1000 16.5sec 60.6
3 400 5 1000 - ERROR

投入データはせいぜい100byte~200byte程度のものでしたが、RUが400だと大体60req/secくらいが上限のように見えました。
この辺りはDynamoDBにしろCosmosDBにしろ、実際の使われ方が読めないと、設定値に頭を悩ますところですね。。

ちなみに、RUの上限オーバー時のエラーは次のようなresponseが返ってきました

{
  "code": "429",
  "message": "Message: {\"Errors\":[\"Request rate is large. More Request Units may be needed, so no changes were made. Please retry this request later. Learn more: http://aka.ms/cosmosdb-error-429\"]}\r\nActivityId: ..."
}
CosmosDB(SQL/Core API) Read

Read操作については限界値まで試す気力がなかったので、とりあえず10000件の読み込みだけ実施しました。

No RU wait(msec) 読み込みDocument数 処理時間 処理/sec
1 400 - 10000 0.25sec 40000

少し時間に振れ幅があったので、キャッシュが効いているかどうかなどが影響していそうです(最後適当)。

Table API

1. ポータルからCosmos DBアカウントを作成する

APIが異なる場合、Cosmos DBアカウントも別にする必要があるようです。

image.png

2. テーブルの作成

image.png

テーブル名のみの指定

image.png

こちらもデータエクスプローラで参照・編集が可能(よりDynamoDBライクなUI)

image.png

3. PHPでのEntity挿入・クエリ実装

こちらはAzure Storage Service用のSDKがPHP向けにあるので、そちらを使用します。

PHP から Azure Storage Table service API または Azure Cosmos DB Table API を使用する方法
https://docs.microsoft.com/ja-jp/azure/cosmos-db/table-storage-how-to-use-php

一応実装例は以下です

<?php

namespace App\Services\Azure\Cosmos;

use App\Exceptions\Error;
use MicrosoftAzure\Storage\Table\TableRestProxy;
use MicrosoftAzure\Storage\Table\Models\EdmType;
use MicrosoftAzure\Storage\Table\Models\Entity;
use MicrosoftAzure\Storage\Table\Models\Filters\Filter;
use MicrosoftAzure\Storage\Table\Models\QueryEntitiesOptions;

class AzureStorageTableClient {
    private $conn_cosmos = 'YOUR COSMOS CONNECTION STRING';
    private $conn_table = 'YOUR STORAGE TABLE CONNECTION STRING';

    private $service;

    public function __construct() {
        $this->service = TableRestProxy::createTableService($this->conn_cosmos);
    }

    public function createEntity(
        string $tableName, string $partitionKey, string $rowKey,
        array $props
    ) {
        $entity = new Entity();
        $entity->setPartitionKey($partitionKey);
        $entity->setRowKey($rowKey);
        collect($props)->each(function($p, $k) use($entity) {
            $entity->addProperty($k, EdmType::propertyType($p), $p);
        });
        $r = $this->service->insertEntity($tableName, $entity);

        return $this->entityToArray($entity);
    }

    public function queryEntities(string $tableName, string $filter) {
        $options = new QueryEntitiesOptions();
        $options->setFilter(Filter::applyQueryString($filter));
        $options->setTop(1000); // Storage Tableの場合は1000を超える値はセットできない模様
        $result = $this->service->queryEntities($tableName, $options);
        $entities = $result->getEntities();
        return collect($entities)->map(function($entity) {
            return $this->entityToArray($entity);
        })->values()->all();
    }

    private function entityToArray(Entity $entity): array {
        return collect($entity->getProperties())->map(function($p, $k) {
            return [ $k => $p->getValue()];
        })->flatMap(function($values) {
            return $values;
        })->toArray();
    }
}

4. Write/Read実行

CosmosDB(Table API) Write

SQL/Core APIと同様に試したので、結果だけ記載します。

No RU wait(msec) 書き込みEntity数 処理時間 処理/sec
1 400 100 1000 110sec 9.1
2 400 80 1000 90sec 11.1
3 400 50 1000 60sec 16.7
4 400 30 1000 60sec 16.7
5 400 25 1000 - ERROR

完全に同じデータではないし、実装も違うには違うのですが、それでもSQL/Core APIと比較すると多少性能は落ちるようですね。
また、Table APIはAzure Storage Table互換なので、Azure Storage Tableを使用して検証してみました(接続文字列をStorage Table用に変えるだけ)

Storage Table Write
No RU wait(msec) 書き込みEntity数 処理時間 処理/sec
1 - 10 1000 20sec 50
2 - 8 1000 18sec 55.6
3 - 3 1000 11sec 90.9
4 - 1 1000 8sec 125

なんと。。Storage Tableに変えた方が書き込み処理性能が向上してしまいました。。
こうなってくると、CosmosDBのTable APIを使用する意義が結構薄れてしまいますね;
おそらくお金を積んでRUをしっかり増やしてやるとCosmos DBの方が性能が出そうな気はしますが、Storage Tableでもそこそこ性能は出ているのでコストに見合っているのかどうかは悩ましいです。

CosmosDB(Table API) Read

とりあえず3000Entityのクエリまでで力尽きました。。

No RU wait(msec) 読み込みEntity数 処理時間 処理/sec
1 400 - 3000 0.6sec 5000
Storage Table Read

読み込みについてはStorage Tableだと一度のクエリで1000Entityまでしか返却できない制限があるようで、そこはCosmosにするメリットといえそうです。

No RU wait(msec) 読み込みEntity数 処理時間 処理/sec
1 - - 1000 0.24sec 4167

まとめ的なもの

  • 以下のデータベースAPIをPHPで触ってみた
    • Cosmos DBのSQL/Core API
    • Cosmos DBのTable API
    • Storage TableのTable API
  • SQL/Core APIについては柔軟なクエリと、key設計にあまり悩まなくてよい点がGood
  • Table APIについては正直微妙な結果。既存のStorage TableをあえてCosmos DBに移行して嬉しいことがあるか分からなかった
    • 唯一、一つのクエリで1000件以上とってこれる部分はよいが、Storage Tableを既に使っているのであればnext tokenの処理なども既に実装してしまっているだろうし。。
    • お金を沢山積めばもっとデキる子なのだと信じる
  • サービス名の表記揺れやドキュメントの誤記に負けない強い気持ちを持とう

参考

Cosmos DBのREST API
https://docs.microsoft.com/ja-jp/rest/api/cosmos-db/

クエリ
https://docs.microsoft.com/ja-jp/rest/api/cosmos-db/querying-cosmosdb-resources-using-the-rest-api

Azure Storage Table の API
https://docs.microsoft.com/ja-jp/rest/api/storageservices/table-service-rest-api

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

セッション管理とスレッドモデル(PHP編)

セッション管理とスレッドモデル(PHP編)

結論

複数のセッションの処理はマルチスレッド処理(平行処理)が行われているかもしれないが、同一セッションの内部では処理は一つずつシリアルで処理されているということ。

実験

親のphpでセッションを作成、その後、ko0.phpとko1.phpに対して同時にアクセスされるはずなので、その様子を見てみよう。

oya.php
<?php
echo "<html><head><body>";
echo "SessionId is ";
echo session_id();
echo "<br><hr>\n";
echo "<iframe src=\"ko0.php\"></iframe><br>";
echo "<hr>\n";
echo "<iframe src=\"ko1.php\"></iframe><br>\n";
echo "</body></html>";
?>

ko0.phpとko1.phpは同じ内容で、10秒間スリープするだけの処理

ko0.php
<?php
echo "SessionID is ";
echo session_id();
echo "<br>\n";
echo "start is ";
echo date("H:i:s");
echo "<br>\n";
sleep(10);
echo "  end is ";
echo date("H:i:s");
echo "<br>\n";
phpinfo();
?>

結果

01.jpg

ko0.php が処理されてから、ko1.phpが処理されているのがわかる。

つまり・・・

同一セッションで大量にアクセスしても・・・早く終わるわけではない、ということ

考えてみたら・・・

セッション・オブジェクトに複数スレッドから同時アクセスを制御するようなロック処理がないのだから当然といえば当然か・・・

戻る

セッション管理とスレッドモデル

以上

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

PHP-FPM のステータスを CloudWatch でモニタリングしてみた

本記事はサムザップ #1 Advent Calendar 2020の 19 日目の記事です。

はじめに

弊社で運用しているゲームアプリは、サーバ言語として PHP を使用していることが多いです。

今回は、現在のリクエスト数における PHP のアクティブなプロセス数がおおよそどれくらいかを可視化したく、CloudWatch のメトリクスに出力してみたので、そちらを紹介します。

前提

PHP の実行環境としては、Nginx + PHP-FPM を使用しています。

設定方法

PHP-FPM

PHP-FPM のステータスを出力するには、conf で以下の 1 行を追加するのみで OK です。

pm.status_path = /phpfpm_status

パスの値はわかりやすければ何でも良いです。
上記の設定を入れ、curlなどでリクエストしてみましょう。

$ curl http://localhost/phpfpm_status
pool:                 www
process manager:      dynamic
start time:           18/Dec/2020:03:12:27 +0000
start since:          1609
accepted conn:        4
listen queue:         0
max listen queue:     0
listen queue len:     128
idle processes:       1
active processes:     1
total processes:      2
max active processes: 1
max children reached: 0
slow requests:        0

このように PHP-FPM のステータスが取れます。

ちなみに、 クエリストリングとして?fullを追加するとプロセスごとの詳細も見れます。

$ curl http://localhost/phpfpm_status?full
pool:                 www
process manager:      dynamic
start time:           18/Dec/2020:03:12:27 +0000
start since:          1622
accepted conn:        5
listen queue:         0
max listen queue:     0
listen queue len:     128
idle processes:       1
active processes:     1
total processes:      2
max active processes: 1
max children reached: 0
slow requests:        0

************************
pid:                  7
state:                Idle
start time:           18/Dec/2020:03:12:27 +0000
start since:          1622
requests:             2
request duration:     2814
request method:       GET
request URI:          /status
content length:       0
user:                 -
script:               -
last request cpu:     0.00
last request memory:  2097152

************************
pid:                  8
state:                Running
start time:           18/Dec/2020:03:12:27 +0000
start since:          1622
requests:             3
request duration:     1460
request method:       GET
request URI:          /status?full
content length:       0
user:                 -
script:               -
last request cpu:     0.00
last request memory:  0

また、?jsonをつけると、json 形式での出力になります。

$ curl http://localhost/phpfpm_status?json
{"pool":"www","process manager":"dynamic","start time":1608261147,"start since":1877,"accepted conn":6,"listen queue":0,"max listen queue":0,"listen queue len":128,"idle processes":1,"active processes":1,"total processes":2,"max active processes":1,"max children reached":0,"slow requests":0}

Lambda

実際に、PHP-FPM のステータス取得をリクエストするための Lambda 関数を作成します。
関数は以下のように作ってみました。

lambda_function.py
import os
import time
import json
import logging
import requests

LOGGER = logging.getLogger()
LOGGER.setLevel(logging.INFO)

config = json.load(open('config.json'))

def lambda_handler(event, context):
    # TODO implement
    """
    event:
        (Required)
        target: [string]

        (Optional)
        params: 
            env: (string)
            interval_sec: (int)
            request_cont: (int)
    """

    try:
        if "target" not in event:
            raise ValueError("targetが定義されていません。")
        target = event["target"]
        params = event["params"] if "params" in event else {}

        if "env" not in params:
            params["env"] = int(os.environ['ENV'])
        if "request_cont" not in params:
            params["request_cont"] = int(os.environ['REQUEST_COUNT'])
        if "interval_sec" not in params:
            params["interval_sec"] = int(os.environ['INTERVAL_SEC'])

        _request_status(target, params)

    except ValueError as e:
        LOGGER.error(e)
    except Exception as e:
        LOGGER.error(e)


def _request_status(target, params):
    settings = config[params["env"]]
    if target not in settings["url"]:
        raise ValueError("ステータス取得URLが存在しません。config.jsonを確認してください。")

    url = settings["url"][target]

    for i in range(params["request_cont"]):
        response = requests.get(url)
        if response.status_code == 200:
            status = json.dumps(response.json())
            print("[STATUS CHECK]|target:{target}|env:{env}|status:{status}".format(target=target, env=params["env"], status=status))
        time.sleep(params["interval_sec"])
config.json
{
    "development": {
        "url": {
            "phpfpm": "https://【ドメイン】/phpfpm_status?json"
        }
    },
    "staging": {
        "url": {
            "phpfpm": "https://【ドメイン】/phpfpm_status?json"
        }
    },
    "production": {
        "url": {
            "phpfpm": "https://【ドメイン】/phpfpm_status?json"
        }
    }
}

環境変数としては以下のように設定しました。

変数名 説明
ENV 環境名 development
INTERVAL_SEC リクエストのスリープインターバル(秒) 1
REQUEST_COUNT 一回の関数実行におけるリクエスト数 10

Lambda 関数の実装内容としては、 一回の実行で、ENV環境の URL に対して、INTERVAL_SECで設定した秒数のスリープインターバルで REQUEST_COUNTの回数分リクエストを送るような処理をしています。
これらは、Lambda の環境変数にデフォルト値を設定しつつ、実行の際のevent引数でも上書きで設定できるようにしてます。

config.jsonに関しては、環境ごとに PHP-FPM のステータスを取得する URL を記載しています。

例えばインターバル 1 秒で 10 回のリクエストとして実行した際は、CloudWatch Logs には以下のように 1 秒間隔で 10 行のログレコードが生成されます。

[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51874, "accepted conn": 16454, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51875, "accepted conn": 16455, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51876, "accepted conn": 16456, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51877, "accepted conn": 16457, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51878, "accepted conn": 16458, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51880, "accepted conn": 16459, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51881, "accepted conn": 16460, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51882, "accepted conn": 16461, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51883, "accepted conn": 16465, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 46, "active processes": 2, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}
[STATUS CHECK]|target:phpfpm|env:development|status:{"pool": "www", "process manager": "dynamic", "start time": 1608213868, "start since": 51884, "accepted conn": 16466, "listen queue": 0, "max listen queue": 0, "listen queue len": 128, "idle processes": 47, "active processes": 1, "total processes": 48, "max active processes": 3, "max children reached": 1, "slow requests": 0}

この Lambda 関数を CloudWatch Events を使用して定期実行させ、CloudWatch Logs に上記のようなログが定常的に出力されるように設定します。
今回は 1 分に一回のサイクルでこの Lambda 関数が実行されるように CloudWatch Events に設定しました。

CloudWatch Logs Insights

指定のロググループに対してクエリを用いて検索できるのが、CloudWatch Logs Insights です。
クエリの書き方などは少しクセがありますが、慣れてしまえば何かと便利です。
ログが json で出力されていれば、json のキーを自動認識してくれるので、そのままクエリ内で使用することができます。

そして、statsコマンドを使用すれば、ログ内の数値情報を集計することもできます。
今回はこの statsコマンドを使用して、以下のようなクエリを作ってみました。

filter @message like "STATUS CHECK"
|filter @message like "target:phpfpm"
|stats max(`active processes`) by bin(1m)

これは、ログストリーム内のログの中から、「STATUS CHECKtarget:phpfpm という文字列の入ったログを抽出し、1分間ごとに active processesキーの値の最大値をプロットする」というようなクエリです。

これをにより、PHP-FPM の 1 分間の中でのおおよその最大アクティブプロセス数がグラフ化できます。

スクリーンショット 2020-12-18 13.48.26.png

1 回のリクエストで言うと、ロードバランサに紐づくターゲットインスタンスのどれかに対してのみリクエストが飛ぶので、厳密に言うとすべてのインスタンスにおける PHP の最大のアクティブプロセス数を出力しているわけではないですが、統計上の標本を増やすために Lambda 関数の一回の実行で何度かリクエストを飛ばすように実装しておりました。

まとめ

今回は PHP-FPM のアクティブなプロセス数をグラフ化するために、Lambda 関数や CloudWatch Logs Insights を用いました。
特に CloudWatch Logs Insights はロググループ内の数値情報か簡単な集計処理も行えるので、PHP-FPM に限らず、他の用途でも使用できます。
例えば、PHP のエラーログを CloudWatch Logs に転送しておき、特定のワードで絞り込んだ際の件数(count) をグラフ化しておき、一定の件数を越えたらアラートが飛ぶような設定を行うといったシチュエーションにも使えます。

AWS が用意してくれているデフォルトのメトリクスでは足りないといった場合は、ぜひご活用ください。

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

Laravel8 DBのデータを取得する処理をサービスに分割する

目的

  • laravel6のアプリのコントローラでDBから値を取得する処理をサービス層に分割する方法をまとめる

実施環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.8 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする
Laravel バージョン 8.X commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.19 for osx10.13 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

前提条件

  • 実施環境に近い環境が構築されていること

前提情報

  • 今回サービスに処理を分割するlaravel8のアプリではAuthを用いた認証機能が付与されており、ユーザが登録されていることが望ましい。
  • laravel8のアプリでの方法を紹介してるがルーティング情報の記載部分とusersテーブルのモデルファイルのデフォルト位置以外はlaravel7、laravel6でも方法は同じである。
  • 筆者はMacのローカルに環境を構築し本記載の確認を行った。
  • Laravel8のアプリでユーザ情報をDBから取得する処理をサービスに分割する方法を記載する。
  • どんなことをするかを簡単に図に記載する。

    • 対応前

      mermaid-diagram-20201217090302.png

    • 対応後

      mermaid-diagram-20201217090428.png

  • ルーティング、コントローラ、ビューにはそれぞれ下記の記載がされており、/userにアクセスするとユーザIDとユーザ名が下記のように表示される。

    127_0_0_1_8000_user.png

    • ルーティングファイル

      アプリ名ディレクトリ/routes/web.php
      <?php
      
      use Illuminate\Support\Facades\Route;
      use App\Http\Controllers\ContentController;
      use App\Http\Controllers\UserController;
      
      /*
      |--------------------------------------------------------------------------
      | Web Routes
      |--------------------------------------------------------------------------
      |
      | Here is where you can register web routes for your application. These
      | routes are loaded by the RouteServiceProvider within a group which
      | contains the "web" middleware group. Now create something great!
      |
      */
      
      Route::get('/', function () {
          return view('welcome');
      });
      
      Auth::routes();
      
      Route::get('/user', [UserController::class, 'index'])->name('user.index');
      
    • コントローラ

      アプリ名ディレクトリ/app/Http/Controllers/UserController.php
      <?php
      
      namespace App\Http\Controllers;
      
      use Illuminate\Http\Request;
      use App\Models\User;
      use Illuminate\Support\Facades\Auth;
      
      class UserController extends Controller
      {
          public function index()
          {
              $user_id = Auth::id();
              $user_info = User::select('*')->find($user_id);
              return view('users.index', ['user_info' => $user_info]);
          }
      }
      
    • ビュー

      アプリ名ディレクトリ/resources/views/users/index.blade.php
      <p>user_id: {{$user_info['id']}}</p>
      <p>user_name: {{$user_info['name']}}</p>
      

概要

  1. サービスファイルの作成と記載
  2. コントローラファイルの修正
  3. 確認

詳細

  1. サービスファイルの作成と記載

    1. アプリ名ディレクトリで下記コマンドを実行してサービスファイルを格納するディレクトリを作成する。

      $ mkdir app/Services
      
    2. 下記コマンドを実行してサービスファイルを作成する。サービスファイル名はUserService.phpとする。

      $ vi app/Services/UserService.php
      
    3. 下記の内容を記載する。

      アプリ名ディレクトリ/app/Services/UserService.php
      <?php
      
      namespace App\Services;
      
      use App\Models\User;
      
      class UserService
      {
          /**
           *
           * @var User
           */
          private $user;
      
          public function __construct(User $user)
          {
              $this->user = $user;
          }
      
          /**
           * ユーザIDからユーザ情報を取得する
           *
           * @param int $user_id
           * @return model
           */
          public function getUserInfoByUserId($user_id)
          {
              return $user_info = User::select('*')->find($user_id);
          }
      }
      
  2. コントローラファイルの修正

    1. アプリ名ディレクトリで下記コマンドを実行してコントローラファイルを開く。

      $ vi app/Http/Controllers/UserController.php
      
    2. 下記のように修正する。

      アプリ名ディレクトリ/app/Http/Controllers/UserController.php
      <?php
      
      namespace App\Http\Controllers;
      
      use Illuminate\Http\Request;
      use App\Models\User;
      use Illuminate\Support\Facades\Auth;
      // 下記を追記
      use App\Services\UserService;
      
      class UserController extends Controller
      {
          // 下記を追記
          /**
           *
           * @var UserService
           */
          private $userService;
      
          public function __construct(UserService $userService)
          {
              $this->userService = $userService;
          }
          // 上記までを追記
      
          public function index()
          {
              $user_id = Auth::id();
              // 下記を修正
              $user_info = $this->userService->getUserInfoByUserId($user_id);
              return view('users.index', ['user_info' => $user_info]);
          }
      }
      
  3. 確認

    1. ローカルサーバを起動し下記にアクセスする。
    2. ページをリロードしてページの表示に変化がないか確認する

      127_0_0_1_8000_user.png

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

BEAR.Sundayの出力を探す

Hello

こんにちは。 @yuki777 です。
これはBEAR.Sunday Advent Calendar 2020の19日目の記事です。
BEAR.Sundayの出力を追ってみました。

BEAR.Sundayの出力を探す

  • BEAR.SundayはPHPのフレームワークです。
  • PHPならばどこかでechoprintを実行して出力しているのではないでしょうか?
  • 出力している箇所を探してフレームワークへの理解を深めてみます。
  • grepなどを使わずに、エントリポイントから処理を順に追ってみます。
  • ソースはマニュアルのクイックスタートを使います。
composer create-project -n bear/skeleton MyVendor.MyProject
cd MyVendor.MyProject

エントリポイント

bin/page.php (CLI)

  • CLIの場合は php bin/page.php get / なのでbin/page.phpを見てみます。Bootstrapを実行しています。
exit((new Bootstrap())(PHP_SAPI === 'cli' ? 'cli-hal-app' : 'hal-app', $GLOBALS, $_SERVER));

public/index.php (Web)

  • Webの場合は composer servecomposer.jsonにより下記のとおりです。
"serve": "php -dzend_extension=xdebug.so -S 127.0.0.1:8080 -t public"
-t オプションを使えば、ドキュメントルートを明示的に指定することができます。 
URI リクエストにファイルが含まれない場合は、指定したディレクトリにある index.php あるいは index.html を返します。
  • というわけで public/index.php を見てみます。こちらもBootstrapを実行しています。
exit((new Bootstrap())(PHP_SAPI === 'cli-server' ? 'hal-app' : 'prod-hal-app', $GLOBALS, $_SERVER));

MyVendor\MyProject\Bootstrap

  • エントリポイントはCLIもWEBも Bootstrap が実行されていました。
final class Bootstrap
{
    public function __invoke(string $context, array $globals, array $server): int
    {
        $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5
        assert($app instanceof App);
        if ($app->httpCache->isNotModified($server)) {
            $app->httpCache->transfer(); // *1

            return 0;
        }

        $request = $app->router->match($globals, $server);
        try {
            $response = $app->resource->{$request->method}->uri($request->path)($request->query); // *2
            assert($response instanceof ResourceObject); // *3
            $response->transfer($app->responder, $server); // *4

            return 0;
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request)->transfer();

            return 1;
        }
    }
  • エントリポイントから__invoke()がコールされます。
    • __invoke() メソッドは、 スクリプトがオブジェクトを関数としてコールしようとした際にコールされます。
  • $app->httpCache->transfer(); // *1 は名前の通りキャッシュをレスポンスしているようです。
  • $app->resource->{$request->method}->uri($request->path)($request->query); // *2 はリクエストされたPathやQueryを引数にリソースをリクエストしています。
  • assert($response instanceof ResourceObject); // *3により$responseResourceObjectです。
    • assertによると
      • assertionfalse であるかどうかを調べる
      • assert() は PHP 7 で言語構造となり、expectation の定義を満たすようになりました。
      • すなわち、開発環境やテスト環境では有効であるが、運用環境では除去されて、まったくコストのかからないアサーションということです
    • instanceof あるPHP変数が特定のクラスのオブジェクトのインスタンスであるかどうかを調べます
  • そして、$response->transfer($app->responder, $server); // *4 で、ResourceObjecttransferが実行されます。
  • 今度はResourceObjecttransferを見てみます。

BEAR\Resource\ResourceObject

  • エントリポイント から Bootstrap そして ResourceObjecttransferが実行されている。という流れでした。
// ... 省略 ...
    public function transfer(TransferInterface $responder, array $server)
    {
        $responder($this, $server);
    }
// ... 省略 ...
  • TransferInterfaceimplementsした$responderという実装クラスが関数としてコールされるので__invoke()が実行されるということです。
  • では$responderを見てみます。これはBootstrap$response->transfer($app->responder, $server); // *4として渡されていました。
  • もういちどBootstrapを見てみます。

MyVendor\MyProject\Bootstrap

// もういちど
final class Bootstrap
{
    public function __invoke(string $context, array $globals, array $server): int
    {
        $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5
        assert($app instanceof App); // *6
        if ($app->httpCache->isNotModified($server)) {
            $app->httpCache->transfer(); // *1

            return 0;
        }

        $request = $app->router->match($globals, $server);
        try {
            $response = $app->resource->{$request->method}->uri($request->path)($request->query); // *2
            assert($response instanceof ResourceObject); // *3
            $response->transfer($app->responder, $server); // *4

            return 0;
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request)->transfer();

            return 1;
        }
    }
  • $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5$appがセットされています。
  • Injectorのドキュメントによると、
    • BEAR\Package\Injector::getInstance()ではコンテキストに応じたインジェクターが渡されます。
    • プロダクションでは従来のDIのスクリプトファイルを書き出すScriptInjector、開発用ではDIファイルを書き出さないRay\Di\Injectorが渡されます。
  • assert($app instanceof App); // *6により、$appMyVendor\MyProject\Module\Appです。つぎはAppを見てみます。

MyVendor\MyProject\Module\App

namespace MyVendor\MyProject\Module;

final class App implements AppInterface
{
    public HttpCacheInterface $httpCache;
    public RouterInterface $router;
    public TransferInterface $responder;
    public ResourceInterface $resource;
    public ThrowableHandlerInterface $throwableHandler;

    public function __construct(
        HttpCacheInterface $httpCache,
        RouterInterface $router,
        TransferInterface $responder, // *7
        ResourceInterface $resource,
        ThrowableHandlerInterface $throwableHandler
    ) {
        $this->httpCache = $httpCache;
        $this->router = $router;
        $this->responder = $responder; // *8
        $this->resource = $resource;
        $this->throwableHandler = $throwableHandler;
    }
}
  • __construct()TransferInterface $responder, // *7が引数に指定されています。
    • コンストラクタによると、 コンストラクタメソッドを有するクラスは、新たにオブジェクトが 生成される度にこのメソッドをコールします。
  • $this->responder = $responder; // *8$responderがセットされています。
  • Appにはこれ以上の処理はないため、またまたBootstrapに戻りヒントを探してみます。

MyVendor\MyProject\Bootstrap

// 本日三回目
final class Bootstrap
{
    public function __invoke(string $context, array $globals, array $server): int
    {
        $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5
        assert($app instanceof App); // *6
        if ($app->httpCache->isNotModified($server)) {
            $app->httpCache->transfer(); // *1

            return 0;
        }

        $request = $app->router->match($globals, $server);
        try {
            $response = $app->resource->{$request->method}->uri($request->path)($request->query); // *2
            assert($response instanceof ResourceObject); // *3
            $response->transfer($app->responder, $server); // *4

            return 0;
        } catch (Throwable $e) {
            $app->throwableHandler->handle($e, $request)->transfer();

            return 1;
        }
    }
  • $app = Injector::getInstance($context)->getInstance(AppInterface::class); // *5の処理は中を追っていませんでしたので、見てみます。

BEAR\Package\Injector

// ... 省略 ...
final class Injector
{
    // ... 省略 ...
    public static function getInstance(string $appName, string $context, string $appDir, ?CacheProvider $cache = null): InjectorInterface
    {
        $injectorId = $appName . $context;
        if (isset(self::$instances[$injectorId])) {
            return self::$instances[$injectorId];
        }

        $meta = new Meta($appName, $context, $appDir);
        $cache = $cache ?? new PhpFileCache($meta->tmpDir . '/injector');
        $cache->setNamespace($injectorId);
        /** @var ?InjectorInterface $cachedInjector */
        $cachedInjector = $cache->fetch(InjectorInterface::class);
        if ($cachedInjector instanceof InjectorInterface) {
            return $cachedInjector;
        }

        $injector = self::factory($meta, $context); // *9
        $injector->getInstance(AppInterface::class);
        if ($injector instanceof ScriptInjector) {
            $cache->save(InjectorInterface::class, $injector);
        }

        self::$instances[$injectorId] = $injector;

        return $injector;
    }

    private static function factory(Meta $meta, string $context): InjectorInterface
    {
        $scriptDir = $meta->tmpDir . '/di';
        ! is_dir($scriptDir) && ! @mkdir($scriptDir) && ! is_dir($scriptDir);
        $module = (new Module())($meta, $context, ''); // *10
        $rayInjector = new RayInjector($module, $scriptDir);
        $isProd = $rayInjector->getInstance('', Compile::class);
        assert(is_bool($isProd));
        if ($isProd) {
            return new ScriptInjector($scriptDir, static function () use ($scriptDir, $module) {
                return new ScriptinjectorModule($scriptDir, $module);
            });
        }

        return $rayInjector;
    }
}
  • new Meta...new PhpFileCache...$cache->setNamespace...をひとつずつ処理を追っていきます。
  • $injector = self::factory($meta, $context); // *9factory()が実行されています。
  • $module = (new Module())($meta, $context, ''); // *10Moduleを見てみます。

BEAR\Package\Module

namespace BEAR\Package;

// ... 省略 ...

class Module
{
    /**
     * Return module from $appMeta and $context
     */
    public function __invoke(AbstractAppMeta $appMeta, string $context, string $cacheNamespace = ''): AbstractModule
    {
        $contextsArray = array_reverse(explode('-', $context));
        $module = new AssistedModule();
        foreach ($contextsArray as $contextItem) {
            $module = $this->installContextModule($appMeta, $contextItem, $module); // *11
        }

        $module->override(new AppMetaModule($appMeta));
        $module->override(new CacheNamespaceModule($cacheNamespace));

        return $module;
    }

    private function installContextModule(AbstractAppMeta $appMeta, string $contextItem, AbstractModule $module): AbstractModule
    {
        $class = $appMeta->name . '\Module\\' . ucwords($contextItem) . 'Module';
        if (! class_exists($class)) {
            $class = 'BEAR\Package\Context\\' . ucwords($contextItem) . 'Module';
        }

        if (! is_a($class, AbstractModule::class, true)) {
            throw new InvalidContextException($contextItem);
        }

        /** @psalm-suppress UnsafeInstantiation */
        $module = is_subclass_of($class, AbstractAppModule::class) ? new $class($appMeta, $module) : new $class($module); // *12

        return $module;
    }
}
  • $this->installContextModule($appMeta, $contextItem, $module); // *11 で、installContextModule()を実行しています
  • new $class($appMeta, $module) : new $class($module); // *12 $classnewされています。
    • CLIでコンテキストがcli-hal-appの場合はAppModule,HalModule,CliModuleの順
    • WEBでコンテキストがhal-appの場合はAppModule,HalModuleの順
    • コンテキストにより後の束縛が優先されるというのはここですね。

MyVendor\MyProject\Module\AppModule

<?php

declare(strict_types=1);

namespace MyVendor\MyProject\Module;

use BEAR\Dotenv\Dotenv;
use BEAR\Package\AbstractAppModule;
use BEAR\Package\PackageModule;

use function dirname;

class AppModule extends AbstractAppModule
{
    protected function configure(): void
    {
        (new Dotenv())->load(dirname(__DIR__, 2));
        $this->install(new PackageModule()); // *13
    }
}
  • AbstractModuleクラスを継承して、configureメソッドをオーバーライドすることで束縛作成しています。
  • $this->install(new PackageModule()); // *13AppModuleが必要なモジュールとしてPackageModuleをインストールしています。
AppModule => PackageModule
namespace BEAR\Package;

class PackageModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new QueryRepositoryModule());
        $this->install(new WebRouterModule());
        $this->install(new VndErrorModule());
        $this->install(new PsrLoggerModule());
        $this->install(new StreamModule());
        $this->install(new CreatedResourceModule());
        $this->install(new DiCompileModule(false));
        $this->install(new SundayModule()); // *14
    }
}
  • new QueryRepositoryModule,new WebRouterModule,,, ひとつずつ見ていきます。
  • $this->install(new SundayModule()); // *14で、PackageModuleが必要なモジュールとしてSundayModuleをインストールしています。
AppModule => PackageModule => SundayModule
namespace BEAR\Sunday\Module;

class SundayModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->install(new AppModule());
        $this->install(new HttpCacheModule());
        $this->install(new DoctrineCacheModule());
        $this->install(new DoctrineAnnotationModule());
        $this->install(new ResourceModule('BEAR\Sunday'));
        $this->install(new RouterModule());
        $this->install(new HttpResponderModule()); // *15
        $this->install(new ErrorModule());
    }
}
  • $this->install(new HttpResponderModule()); // *15SundayModuleが必要なモジュールとしてHttpResponderModuleをインストールしています。
AppModule => PackageModule => SundayModule => HttpResponderModule
namespace BEAR\Sunday\Provide\Transfer;

use BEAR\Sunday\Extension\Transfer\TransferInterface;
use Ray\Di\AbstractModule;

class HttpResponderModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->bind(TransferInterface::class)->to(HttpResponder::class); // *16
        $this->bind(HeaderInterface::class)->to(Header::class);
        $this->bind(ConditionalResponseInterface::class)->to(ConditionalResponse::class);
    }
}
  • $this->bind(TransferInterface::class)->to(HttpResponder::class); // *16 で、リンク束縛しています。
    • リンク束縛 によると、 リンク束縛は最も基本の束縛です。インターフェイスとその実装クラスを束縛します。
AppModule => PackageModule => SundayModule => HttpResponderModule => HttpResponder
namespace BEAR\Sunday\Provide\Transfer;

class HttpResponder implements TransferInterface
{
    public function __invoke(ResourceObject $ro, array $server): void
    {
        /** @var array{HTTP_IF_NONE_MATCH?: string} $server */
        $isModifed = $this->condResponse->isModified($ro, $server);
        $output = $isModifed ? $this->getOutput($ro, $server) : $this->condResponse->getOutput($ro->headers);

        foreach ($output->headers as $label => $value) {
            // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName
            header("{$label}: {$value}", false);
        }

        // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly.ReferenceViaFallbackGlobalName
        http_response_code($output->code);

        echo $output->view; // *17 !!!
    }
}
  • echo $output->view; // *17に、echoがありました。WEBの場合はここで出力されているということですね。
  • ここは__invoke()ですので、オブジェクトを関数としてコールしようとした際にコールされます。
    • ResourceObjectでは$responder($this, $server);のようにコールしていましたね。

BEAR\Package\Context\HalModule

  • HalModuleにはTransferInterfaceの束縛がありません。

BEAR\Package\Context\CliModule

  • CliModuleにはTransferInterfaceインターフェイスにCliResponderの束縛があります
  • cli-hal-appコンテキストであれば、CliResponderが優先されて束縛されるということですね。
namespace BEAR\Package\Context;

class CliModule extends AbstractModule
{
    protected function configure(): void
    {
        $this->rename(RouterInterface::class, 'original');
        $this->bind(RouterInterface::class)->to(CliRouter::class);
        $this->bind(TransferInterface::class)->to(CliResponder::class); // *18
        $this->bind(HttpCacheInterface::class)->to(CliHttpCache::class);
        $stdIn = tempnam(sys_get_temp_dir(), 'stdin-' . crc32(__FILE__));
        $this->bind()->annotatedWith(StdIn::class)->toInstance($stdIn);
    }
}
CliModule => CliResponder
namespace BEAR\Package\Provide\Transfer;

final class CliResponder implements TransferInterface
{
    public function __invoke(ResourceObject $ro, array $server): void
    {
        /** @var array{HTTP_IF_NONE_MATCH?: string} $server */
        $isModified = $this->condResponse->isModified($ro, $server);
        $output = $isModified ? $this->getOutput($ro, $server) : $this->condResponse->getOutput($ro->headers);

        $statusText = (new Code())->statusText[$ro->code] ?? '';
        $ob = $output->code . ' ' . $statusText . PHP_EOL;

        // header
        foreach ($output->headers as $label => $value) {
            $ob .= "{$label}: {$value}" . PHP_EOL;
        }

        // empty line
        $ob .= PHP_EOL;

        // body
        $ob .= (string) $output->view;

        echo $ob; // *19 !!!
    }
}

まとめ

  • エントリポイントから処理を順に追ってechoによる出力を探してみました。
  • フレームワークの処理の流れを少し理解できました。

明日

  • 明日は日曜日です。お楽しみに!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ruby/php] herokuの自動スリープを「時間を指定」して抑制する

herokuは無料で使えて大変便利なサービスなんだけど、無料プランでは30分以上アクセスされないと自動でスリープするという制限がかかっている。

しかしherokuにはスケジューラーというcronみたいなサービスが別にあるので、こちらにcurl:webアプリのアドレスのコマンドを10分に一回で登録してあげれば、10分に一回スケジューラーがアクセスしてくれるので自動スリープを防いでくれるというTipsがある。

検索をかければ山のようにそういう情報はでてくれるが、これは「24時間動かす」という前提になる。無料プランの場合、クレカを登録しておけば無料の場合でも31日24時間動いてても時間は余るようにはなってるが、スクリプトなどを回す場合そちらもカウントされ、時間を食い合ってしまう。ならば、「アクセスが大してない時間は自動スリープを意図して受け入れる」というのもありなんじゃないか?と思う。

で、具体的にそういう方法がどこにもかかれてないので、適当にメモ的にかいておく。

lib/script/scheduler.rb
require 'time'
require 'uri'
require 'net/http'
ENV['TZ'] = "Asia/Tokyo"

# 16時から翌7時まで、20分刻みの時間の配列を生成
timetable = []
hary = [*"00".."07",*"16".."23"]
mary = ["00","20","40"]

hary.size.times do |h|

    mary.each do |e|

      a = hary[h] + ":" + e
      b = Time.parse(a)
      timetable.push(b)

    end
end

#現在時間を600秒(10分)ごとに整形し、時間オブジェクトに変換
tm1 = Time.now.to_i / 600
now = Time.at(tm1 * 600)

# 現在時間が時間配列に一致したらアドレスを開く
if timetable.include?(now)

  require 'timeout'

  Timeout.timeout(10){
    system("curl webアプリのアドレス")
  }

end

herokuで実行するのでタイムゾーンはスクリプト内で指定してあげる必要があるので、ENV['TZ'] = "Asia/Tokyo"は日本の時間帯前提で動かすのなら必須。

今回は午前7時から午後16時までは自動スリープを受け入れるので、それ以外の時間帯を["00:00","00:20","00:40","1:00","1:20",...]といった配列に生成し、Time.parse()で時間オブジェクトに変換する。

これだけだとherokuのタイムスケジュールと一瞬でしかマッチできないので、現在時間をUNIX時間に一旦変換し、10分ごとの判定になおすために600(秒)で割ってしまう。rubyの場合はインテグラのまま割り算をすると少数点以下は切り捨てなので、600(秒)をかけ直して時間オブジェクトに戻すと、現在時間は10分ごとの表記になる。

あとはさきほどつくった指定時間の配列に、今の10分ごとの時間が含まれているのかをチェックして、含まれていたらsystem("curl アドレス")を実行しますよってやればOK。(Timeoutは保険で書いてるだけ)

あとはこのスクリプトをlib/script/などのフォルダにいれてherokuにpushして、herokuスケジューラーにrails runner lib/script/scheduler.rbのコマンドで指定して、10分ごとの設定にしたらよい。

自分の思いつきでかいたのでもっとシンプルな方法があるのかもしれないけど、時間帯や日程の指定には結構簡単な書き方なので、よく使っている。

heroku外のcronでやりたい(PHP)

ただ、10分ごとにスクリプト回す性質上、当然これは頻繁にまわることになる。これ自体が無料時間を食ってしまうというのもあるし、なによりherokuの無料プランではwebアプリ以外を実行する枠は一つに制限されていて、他のスクリプトと頻繁に干渉してしまう。

なので外部のcronでもできるようにPHPなんかでも書いてみた。

schedule.php
<?php

$hour1 = range(0,7);
$hour2 = range(16,23);
$min = array("0","20","40",);

$hour = array_merge($hour1,$hour2);
$num = count($hour);
$add = 0;
$tt = array();

while ( $add < $num ){

    foreach ($min as $val){
        $a = $hour[$add] . ":" . $val;
        array_push($tt,$a);
    }
    $add++;

}

$timetable = array();

foreach ($tt as $val){
    $a = strtotime($val);
    array_push($timetable,$a);
}

$now =  time() / 600 ;
$now = ceil($now) * 600;

if (in_array($now,$timetable)){

$url = 'webアプリのアドレス';
$curl = curl_init($url);

curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'GET'); // メソッド指定
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // 証明書の検証を行わない
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); // レスポンスを文字列で受け取る

$response = curl_exec($curl);

curl_close($curl);

}

?>

やってることは全く同じなので特に説明の余地はない。PHPはぜんぜんわからないので適当だけど、今のところ動いてるから、まぁたぶんいいんだろう。たぶん。

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

Laravel(多対多)でphp artisan migrateエラー(SQLSTATE[HY000]: General error: 3780 Referencing column~are incompatible.)

Laravelでphp artisan migrateを実行し、SQLSTATE[HY000]: General error: 3780 Referencing column~are incompatibleといったエラーが出てきた場合、設定しているカラムタイプが異なることが原因である場合があります。
「are incompatible」は直訳すると互換性がないと言われています。

//符号なしBIGINTを使用した自動増分ID(主キー)
$table->bigIncrements('id');

//BIGINTカラム
$table->bigInteger('votes');

今一度、自分で設定したカラムタイプの整合性が取れているか確認してみてください。

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

doctrineアノテーションとPHP8アトリビュート

Hello

こんにちは。@koriymです。

この記事はBEAR.Sunday Advent Calendar 2020の18日目というか1日目というか最初の記事です。1つか2つでもカレンダーとして残れば良いかと、今年も今頃になって作成しました。 昨日公開したばかりのツールについて書きます。

PHP8 アトリビュート

PHP8でアトリビュートがサポートされました。以下はphp.netからの引用です。

アトリビュートの概要

アトリビュートを使うと、 コンピューターが解析できる構造化されたメタデータの情報を、 コードの宣言時に埋め込むことができます。 ... アトリビュートで定義されたメタデータは、 実行時に リフレクションAPI を使って調べることが出来ます。 よって、アトリビュートは、 コードに直接埋め込むことが出来る、 設定のための言語とみなすことができます。
アトリビュートを使うと、機能の抽象的な実装と、アプリケーションでの具体的な利用を分離できます。 この点でアトリビュートは、インターフェイスとその実装と比較できます。 インターフェイスとその実装はコードに関する情報ですが、 アトリビュートはコードの追加情報と設定に注釈を付けるものです。

BEAR.Sundayの開発はPHP5.4まで遡りますが、その時からAOPやDIをアプリケーションとフレームワーク全体に対して制約をもたらすものとし、その実現にコードにメタ情報をもたらすアノテーションを利用してきました。

Doctrineアノテーション

PHP8でAttributesがサポートされる前もPHPのライブラリの多くで、アノテーションがそれぞれの実装で実現されていました。多くの人に馴染みがあるのがphpunit@test@dataProvierアノテーションではないでしょうか。

それぞれが実装を持つことも少なくありませんでしたが、BEAR.Sundayもそうであったようにdoctrine/annotationを利用していたものも少なくありませんでした。

このようなカスタムアノテーションを作成して

/**
 * @Annotation
 */
final class MyAnnotation
{
    public $myProperty;
}

メソッドやクラスにphpdocで/** @MyAnnotation */とアノテートする事ができます。

パフォーマンス

phpdocを利用し、リフレクションで文字列の解析を行うのでコストはそれなりにかかり、通常はプロダクションではCacheAnnotatioReaderを利用します。

BEAR.Sundayでは一歩進んだ解決法を採用しました。すなわち設計時からプログラム実行のコンパイルタイムランタイムの区分を明確にし、プロダクションではアノテーションの読み込みそのものがほとんど存在しないようにしました。コンパイルタイムではアノテーションを利用したDI/AOPの束縛にしたがってコードをジェネレートし、ランタイムではそれを実行するだけです。

Ray.Aop

Ray.Aopではアノテーションで束縛した結果に基付いてAOPのPHPコードを作成し、ランタイムではそのコードが実行されるだけなのでプロダクションではアノテーションの読み込みは行われません。

<?php

declare (strict_types=1);
namespace {
    use Ray\Aop\WeavedInterface;
    use Ray\Aop\ReflectiveMethodInvocation as Invocation;
    /** doc comment of FakeMock */
    class FakeFoo_2030086604 extends \FakeGlobalNamespaced implements WeavedInterface
    {
        public $bind;
        public $bindings = [];
        public $methodAnnotations = 'a:0:{}';
        public $classAnnotations = 'a:0:{}';
        private $isAspect = true;

        public function doSomething(int $a)
        {
            if (!$this->isAspect) {
                $this->isAspect = true;
                return call_user_func_array([$this, 'parent::' . __FUNCTION__], func_get_args());
            }
            $this->isAspect = false;
            $result = (new Invocation($this, __FUNCTION__, func_get_args(), $this->bindings[__FUNCTION__]))->proceed();
            $this->isAspect = true;
            return $result;
        }
    }
}

コード生成のついでに、アノテーションも埋め込んでしまっていて、もしインターセプターがアノテーションをランタイムが読む必要がある時でも通常のアノテーショリーダーが起動することがありません。

Ray.Di

Ray.Diでも同様でインスタンスを生成するコードそのものをジェネレートしているのでプロダクション実行時にアノテーションが読まれることはありません。

<?php

namespace Ray\Di\Compiler;

$instance = new \BEAR\QueryRepository\HttpCache($prototype('BEAR\\QueryRepository\\ResourceStorageInterface-'));
$is_singleton = false;
return $instance;

問題点

IDEの十分なサポートもあり、言語機能のように十分機能してきたdoctrine/annotationですが問題がなかったわけではありません。

  • 開発時は遅い
  • クラスとメソッドにしか記述できず、引数にはアノテートできない。
  • 視認性にやや難あり

PHP8のアトリビュートではこれらの問題が解決します。

Koriym.Attribute

PHP8に新しくフルスクラッチでライブラリもアプリケーションも開発するなら問題はありませんが、旧来のdoctrineアノテーションリーダーを利用したコードでもPHP8アトリビュートでも読めれば既存のコードが活かせ、フォワードコンパチブルなコードも記述できます。

Doctrineの人達が作るのではと待っていたのですが、どうも出そうにありません

そこでdoctrine/annotationリーダーのインターフェイスを持ちながら、PHP8アトリビュートも読めるKoriym.Attributesを開発しました。

すでにこのリーダーを使ったRay.AopのマスターブランチではPHP8アトリビュートを利用することができます。Ray.Diももうすぐです。

BEAR.Sundayでアトリビュート

BEAR.SundayのResourceObjectはこのようになります。

#[Cacheable(expirySecond: 60)]
class Recent extends ResourceObject
{
    public function __construct(#[AuthKey] string $authKey)
    {
    }

    #[Inject]
    public function setRenderer(#[Json] RenderInterface $render): ResourceObject
    {
    }

    #[Link(rel: "tag", crawl: "meta", href: "app://self/article/{id}")]
    #[Embed(rel: "user", src: "app://self/users/{id}")]
    #[HttpCache(isPrivate: true, maxAge: 60)]
    public function onGet(int $id, #[Assisted] $pdo): static
    {
    }

    #[LoginUserOnly]
    #[Loggable]
    #[Transactional]
    #[Notify('post')]
    public function onPost(string $body): static

}

"コードが仕様を明らかにするべき"に沿ったものになっていると思います。

移行シナリオ

PHP8の移行にはいくつかのシナリオを考えてみます。

開発はPHP8でプロダクションはPHP7.x

プロダクションはまだPHP7でも、アノテーションは後述するアトリビュートとアノテーションが共用できる方式で記述し将来のPHP8移行に備えます。PHPコードはdocotrineアノテーション記法と共にPHP8アトリビュートでも記述します。

PHP8アトリビュートは#始まりでPHP7にとってはコメントとなり無視されますが、PHP8開発環境では高速な実行が可能です。またPHP7での動作はCIで保証します。

これはPHP7/PHP8双方で実行できるライブラリを開発する開発者にも必要な方法です。

プロダクションでもPHP8

rectorphp/rector を使って、docotrineアノテーションのコードを以下のようにPHP8アトリビュートに変更可能なようです。

-use Doctrine\Common\Annotations\Annotation\Target;
+use Attribute;

-/**
- * @Annotation
- * @Target({"PROPERTY", "ANNOTATION"})
- */
+#[Attribute(Attribute::TARGET_PROPERTY)]
 final class PHPConstraint extends Constraint
 {
 }

https://getrector.org/blog/2020/11/30/smooth-upgrade-to-php-8-in-diffs

docotrineアノテーションはアノテーションのネストができますが、PHP8アトリビュートはできない事に注意が必要です。1

共用アノテーション

上記のアノテーションのようにコンストラクタの無いdoctrineアノテーションを、PHP8アトリビュートで使えるとようにするためにはコンストラクタを作成する必要があります。

/**
 * @Annotation
 */
+#[Attribute]
final class MyAnnotation
{
    public $myProperty;
+    public function __constract($value)
+    {
+        $this->myProperty = $value['value'] ?? $value
+    }
}

doctrineアノテーションは全てのプロパティが1つの連想配列として渡されますが、PHP8では引数で渡されるのでそれを考慮してコンストラクタを記述します。

上記のアノテーションはdoctirnアノテーションの場合は以下のように記述しますが

/**
 * @MyAnnotation('foo')
 */

PHP8アトリビューとの場合には

#[MyAnnotation('foo')]

になります。

以下のようなプロパティを指定する場合には

#[HttpCache(isPrivate: true, MaxAge:60)]

このようなコードになります。doctrineアノテーションでは最初の$valueしか使われないので他の引数にはデフォルト値を設定する必要があります。

/**
 * @Annotation
 */
#[Attribute]
final class MyAnnotation
{
    public $isPrivate;
    public $maxAge;
+    public function __constract(array $value = [], bool $isPrivate = false, $maxAge = 0)
+    {
+        $this->isPrivate = $value['isPravate'] ?? $isPrivate;
+        $this->maxAge = $value['maxAge'] ?? $maxAge;
+    }
}

PHP8+専用であればこのようにシンプルです。

#[Attribute]
final class HttpCache
{
    public function __construct(
        public bool $isPrivate,
        public int $maxAge
    ){}
}

従来記法が煩雑だった@Namedアノテーションも引数に直接記述できます。

    #[Inject]
    public function setFeet(
        #[Named('right')] Foot $rightFoot,
        #[Named('left')] Foot $leftFoot
    ): void
    {

カスタムアノテーションを用意すればさらに可読性が良くなります。2

    #[Inject]
    public function setFeet(
        #[Right] Foot $rightFoot,
        #[Left] Foot $leftFoot
    ): void
    {

まとめ

マニュアルで「進化可能なメンテナンス性の良いコードが長期的に利用できることを重視します。」との価値観表明をしているBEAR.Sundayは、継続性を維持しながら共に新しい技術にも対応します。

PHP5.4の頃からアノテーションをコア技術として取り入れてきたBEAR.Sundayが、PHP8のアトリビュートをサポートするのは自然な事ですが3、同時に特別な感慨もあります。PHPのネイティブサポートを待つのに多くの年月が必要でした。

明日

明日はProject 8で一緒にBEAR.Sundayを使っている@yuki777さんの 「BEAR.Sundayの出力(echoとかprint)を追ってみる(仮)」です。お楽しみに!

リンク


  1. BEAR.Sundayフレームワーク側ではアノテーションのネストは行ってないので問題にならないでしょう。 

  2. ちなみにこれはDIの「ロボットの足問題」として知られるものをサンプルにしています。https://github.com/google/guice/wiki/FrequentlyAskedQuestions#how-do-i-build-two-similar-but-slightly-different-trees-of-objects 

  3. 名前付き引数もそうです! 

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

※学習用メモ デザインパターン:Factory Method編[PHP]

Factory Methodパターンとはどういうものか

Factoryとは工場という意味です。

Factory Method パターンは、他のクラスのコンストラクタをサブクラスで上書き可能な自分のメソッドに置き換えることで、 アプリケーションに特化したオブジェクトの生成をサブクラスに追い出し、クラスの再利用性を高めることを目的とする。(wikipediaより)

ポイントを要約すると、親クラスであるファクトリが、実際のオブジェクトの生成をサブクラスに委譲するということです。

登場するクラス

・抽象クラス
 Productクラス:生成物の抽象クラス
 Creatorクラス:Factoryクラス
・具象クラス
 ConcreteProductクラス:生成物の具象クラス
 ConcreteCreatorクラス:オブジェクト生成の具体的の実装が記述されたサブクラス

[参考]
https://ja.wikipedia.org/wiki/Factory_Method_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
https://qiita.com/shoheiyokoyama/items/d752834a6a2e208b90ca
https://www.techscore.com/tech/DesignPattern/FactoryMethod.html/
https://think-on-object.blogspot.com/2011/11/factoryfactory-methodabstract-factory.html

Factory Methodパターンを用いるメリット

・メリット
個々のインスタンス生成の実処理をサブクラス内で行うため、生成処理全体を小分けにし、簡潔な状態にすることができる。

・デメリット
インスタンスを変更する場合、Factoryクラスを変更する必要がある

サンプルコード

仕様

・AppleかAndroidかを判断する

生成物の抽象クラス(Product)
このクラスでは生成物のインターフェースを記述します。

Maker.class.php
abstract class Maker
{
    protected $name = "";
    protected $brouser = "";

    public function getName()
    {
        return $this->name;
    }

    public function getBrouser()
    {
        return $this->brouser;
    }

    public function getInformation()
    {
        return "Maker:" . $this->getName() . "\n" . "BrouserName:" . $this->getBrouser() . "\n";
    }
}

Factoryクラス(Creator)
このクラスでオブジェクトを生成する流れを実装します。
今回は抽象メソッドにしていますが、createSmartphoneメソッドにデフォルトの処理を入れても良いです。

SmartphoneFactory.class.php
abstract class SmartphoneFactory
{
    public function create()
    {
        $smartphone = $this->createSmartphone();
        return $smartphone;
    }

    abstract protected function createSmartphone(); // デフォルト処理を設定したい場合はここに入れればよい
}

生成物の具象クラス(ConcreteProduct)
実際に生成するオブジェクトの設定を行います。

Apple.class.php
class Apple extends Maker
{
    protected $name = "Apple";
    protected $brouser = "Safari";
}
Google.class.php
class Google extends Maker
{
    protected $name = "Android";
    protected $brouser = "GoogleChrome";
}

Factory Methodを含むサブクラス(ConcreteCreator)
このクラスで先のFactoryクラスのcreateSmartphoneメソッドをオーバーライドし、それぞれで異なるインスタンスを生成している。
この場合それぞれオーバーライドしたcreateSmartphoneメソッドがFactory Methodである。

AppleSmartphoneFactory.class.php
class AppleSmartphoneFactory extends SmartphoneFactory
{
    // Factory Method
    protected function createSmartphone()
    {
        return new Apple();
    }
}
AndroidSmartphoneFactory.class.php
class AndroidSmartphoneFactory extends SmartphoneFactory
{
    // Factory Method
    protected function createSmartphone()
    {
        return new Google();
    }
}

使ってみる

$appleSmartPhoneFactory = new AppleSmartphoneFactory();
$appleSmartPhone = $appleSmartPhoneFactory->create();

$androidSmartphoneFactory = new AndroidSmartphoneFactory();
$androidSmartphone = $androidSmartphoneFactory->create();

echo $appleSmartPhone->getInformation();
echo $androidSmartphone->getInformation();

/*結果
Maker:Apple
BrouserName:Safari
Maker:Android
BrouserName:GoogleChrome
*?

まとめ

・Factory Methodパターンは、異なるインスタンスを作成する機構を使用したい場合、専用のサブクラスを用意することでオブジェクトの生成部分のみ変更する形となっている。
・オブジェクトの生成自体を動的に処理する形(Factoryパターン)が多く使用される印象であるが、ここでは一旦Factory Methodなのでメソッドのみに注目しました。

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

インフラストラクチャ層のモジュール設計

この記事はLaravel Advent Calendar 2020の18日目です。


はじめに

ドメイン駆動設計で開発していてインフラストラクチャ層がとっ散らかりがちになるのが悩みの種でした。
実践ドメイン駆動設計』の第9章にモジュールを使ってクラスを整理する方法が書かれていますが、ドメイン層のモジュールについての話がほとんどで、インフラストラクチャ層のモジュールについては書かれていません。

そんな折に見つけた『Domain-Driven Design in PHP』という本に、いいなと思うインフラストラクチャ層のモジュール設計が書かれていたので紹介したいと思います。

『Domain-Driven Design in PHP』で推奨されているインフラストラクチャ層のモジュール設計

この本ではインフラストラクチャ層にドメイン層と同じ構成のモジュールを作ることを推奨しています。

|-- Domain
|   `-- Model
`-- Infrastructure
    `-- Domain
        `-- Model

ドメイン層のインターフェースをインフラストラクチャ層で実装するときは、対応するモジュールに配置します。

|-- Domain
|   `-- Model
|       `-- Bill
|           `-- BillRepositoryInterface.php
`-- Infrastructure
    `-- Domain
        `-- Model
            `-- Bill
                `-- EloquentBillRepository.php

私がこの設計をいいなと思った理由は、インターフェースの実装がどこにあるのかわかりやすくなる点と、インフラストラクチャ層のモジュール名にもユビキタス言語が使われるようになる点です。

ORMなどドメイン層に対応しないものは、その技術に関連したモジュールに配置します。

Infrastructure
|-- Domain
`-- Persistence
    `-- Eloquent
        `-- Models
            `-- Bill.php

本には書かれていませんでしたが、アプリケーション層のインターフェースをインフラストラクチャ層で実装するときもドメイン層のときと同じようにするのがいいと思います。

|-- Application
|   `-- DataTransformer
|       `-- Bill
|           `-- BillDataTransformerInterface.php
`-- Infrastructure
    `-- Application
        `-- DataTransformer
            `-- Bill
                `-- BillLaravelPaginatorDataTransformer.php

Webアプリケーションフレームワークとの付き合い方

この本にはWebアプリケーションフレームワークとの付き合い方についても書かれています。
Webアプリケーションフレームワークを使う場合は、インフラストラクチャ層にデリバリーメカニズム(Web、API、コンソールなど)ごとのモジュールを作り、さらにその中にフレームワークごとのモジュールを作ることを推奨しています。

Infrastructure
|-- Delivery
|   |-- Api
|   |   `-- Laravel
|   |-- Console
|   |   `-- Symfony
|   `-- Web
|       `-- Silex
`-- Domain

上記の例ではAPIにLaravelを使い、コンソールにSymfonyを使い、WebにSilexを使っています。

ユーザーインターフェイス(コントローラやビューのことだと思います)に関しては、各フレームワークのモジュールに配置することを推奨しています。

本では各フレームワークのモジュール内部については特に書かれていませんでしたが、

Frameworks should obey you, and not the other way around. (フレームワークがあなたに従うべきであり、その逆ではない。)

との言葉があったので、各フレームワークのモジュール内部はフレームワークの規約に縛られることなく自由に設計していいということだと思います。

Laravelの例

以下は本に書かれていることではないですが、本を読んでみてLaravelを使う場合どうするか自分なりに考えてみました。

Laravelモジュールの中に自分のアプリケーションで使うLaravel関連のファイルを好きなように配置します。

Infrastructure
`-- Delivery
    `-- Web
        `-- Laravel
            |-- Controllers
            |-- Middleware
            |-- Providers
            |-- Requests
            |-- resources
            |   |-- fonts
            |   |-- js
            |   |-- lang
            |   |-- less
            |   `-- views
            `-- routes

これらのファイルをLaravelに使わせるには設定ファイルを書き換えることになります。

また、設定ファイルを書き換える以外の方法として、パッケージ機能を使ってLaravelに公開することもできると思います。
この方法であれば設定ファイルを書き換えることすら不要になるので、Laravelをほとんど素のままで使うことができるはずです。

Laravelのドキュメントには書かれていませんが、ミドルウェアやイベントもパッケージとして公開できます。

ミドルウェアはIlluminate\Support\ServiceProviderを継承したサービスプロバイダーを作って、bootメソッドの中でLaravelのAPIを使うことで登録できます。

ミドルウェアの登録
public function boot()
{
    $this->app['router']->aliasMiddleware('my_middleware', MyMiddleware::class);
}

イベントもIlluminate\Foundation\Support\Providers\EventServiceProviderを継承したサービスプロバイダーを作れば、あとは普通にイベントを登録するときと同じように登録できます。

イベントの登録
protected $listen = [
    // ...
];

おわりに

『Domain-Driven Design in PHP』に書かれていたインフラストラクチャ層のモジュール設計について紹介しました。
また、Laravelを使う場合どうするか自分なりに考えてみました。

参考

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

インフラストラクチャ層のモジュール設計について

この記事はLaravel Advent Calendar 2020の18日目です。


はじめに

ドメイン駆動設計で開発していてインフラストラクチャ層がとっ散らかりがちになるのが悩みの種でした。
実践ドメイン駆動設計』の第9章にモジュールを使ってクラスを整理する方法が書かれていますが、ドメイン層のモジュールについての話がほとんどで、インフラストラクチャ層のモジュールについては書かれていません。

そんな折に見つけた『Domain-Driven Design in PHP』という本に、いいなと思うインフラストラクチャ層のモジュール設計が書かれていたので紹介したいと思います。

『Domain-Driven Design in PHP』で推奨されているインフラストラクチャ層のモジュール設計

この本ではインフラストラクチャ層にドメイン層と同じ構成のモジュールを作ることを推奨しています。

|-- Domain
|   `-- Model
`-- Infrastructure
    `-- Domain
        `-- Model

ドメイン層のインターフェースをインフラストラクチャ層で実装するときは、対応するモジュールに配置します。

|-- Domain
|   `-- Model
|       `-- Bill
|           `-- BillRepositoryInterface.php
`-- Infrastructure
    `-- Domain
        `-- Model
            `-- Bill
                `-- EloquentBillRepository.php

私がこの設計をいいなと思った理由は、インターフェースの実装がどこにあるのかわかりやすくなる点と、インフラストラクチャ層のモジュール名にもユビキタス言語が使われるようになる点です。

ORMなどドメイン層に対応しないものは、その技術に関連したモジュールに配置します。

Infrastructure
|-- Domain
`-- Persistence
    `-- Eloquent
        `-- Models
            `-- Bill.php

本には書かれていませんでしたが、アプリケーション層のインターフェースをインフラストラクチャ層で実装するときもドメイン層のときと同じようにするのがいいと思います。

|-- Application
|   `-- DataTransformer
|       `-- Bill
|           `-- BillDataTransformerInterface.php
`-- Infrastructure
    `-- Application
        `-- DataTransformer
            `-- Bill
                `-- BillLaravelPaginatorDataTransformer.php

Webアプリケーションフレームワークとの付き合い方

この本にはWebアプリケーションフレームワークとの付き合い方についても書かれています。
Webアプリケーションフレームワークを使う場合は、インフラストラクチャ層にデリバリーメカニズム(Web、API、コンソールなど)ごとのモジュールを作り、さらにその中にフレームワークごとのモジュールを作ることを推奨しています。

Infrastructure
|-- Delivery
|   |-- Api
|   |   `-- Laravel
|   |-- Console
|   |   `-- Symfony
|   `-- Web
|       `-- Silex
`-- Domain

上記の例ではAPIにLaravelを使い、コンソールにSymfonyを使い、WebにSilexを使っています。

ユーザーインターフェイス(コントローラやビューのことだと思います)に関しては、各フレームワークのモジュールに配置することを推奨しています。

本では各フレームワークのモジュール内部については特に書かれていませんでしたが、

Frameworks should obey you, and not the other way around. (フレームワークがあなたに従うべきであり、その逆ではない。)

との言葉があったので、各フレームワークのモジュール内部はフレームワークの規約に縛られることなく自由に設計していいということだと思います。

Laravelの例

以下は本に書かれていることではないですが、本を読んでみてLaravelを使う場合どうするか自分なりに考えてみました。

Laravelモジュールの中に自分のアプリケーションで使うLaravel関連のファイルを好きなように配置します。

Infrastructure
`-- Delivery
    `-- Web
        `-- Laravel
            |-- Controllers
            |-- Middleware
            |-- Providers
            |-- Requests
            |-- resources
            |   |-- fonts
            |   |-- js
            |   |-- lang
            |   |-- less
            |   `-- views
            `-- routes

これらのファイルをLaravelに使わせるには設定ファイルを書き換えることになります。

また、設定ファイルを書き換える以外の方法として、パッケージ機能を使ってLaravelに公開することもできると思います。
この方法であれば設定ファイルを書き換えることすら不要になるので、Laravelをほとんど素のままで使うことができるはずです。

Laravelのドキュメントには書かれていませんが、ミドルウェアやイベントもパッケージとして公開できます。

ミドルウェアはIlluminate\Support\ServiceProviderを継承したサービスプロバイダーを作って、bootメソッドの中でLaravelのAPIを使うことで登録できます。

ミドルウェアの登録
public function boot()
{
    $this->app['router']->aliasMiddleware('my_middleware', MyMiddleware::class);
}

イベントもIlluminate\Foundation\Support\Providers\EventServiceProviderを継承したサービスプロバイダーを作れば、あとは普通にイベントを登録するときと同じように登録できます。

イベントの登録
protected $listen = [
    // ...
];

おわりに

『Domain-Driven Design in PHP』に書かれていたインフラストラクチャ層のモジュール設計について紹介しました。
また、Laravelを使う場合どうするか自分なりに考えてみました。

参考

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

macでいきなりはじめるPHPにチャレンジする!(3) mySQL編

はじめに

前回までの記事ではPHPを始めるにあたっての設定を書いてきました。「いきなりはじめるPHP」の本では初期設定の後、アンケートページを作成し、それをmySQLに保存するというプロセスで進んでいきます。
このうちアンケートページの作成は、本の通りに作れば問題なく動作するのでここでは省きます。

これまでの記事でも書いたように、mySQLの起動、本ではXAMPPを使用していますが、macではMAMPを使用した方が良いとされています。MAMPでは操作方法や、コードなどが一部異なっているので、本の丸写しだとエラーとなってしまったり、また操作方法がわかりにくいことがあります。ここではその辺りの操作の違いを中心に書いていきます。

mySQLの起動、データベースの作成

mySQLの起動は、MAMPではローカルサーバーの起動と同時に行われます。詳しくは(1)に記載しています。
ローカルサーバーを起動すると、スタートアップ画面が表示されますが、それを下にスクロールするとPHP、MySQL、SQLiteの3つの管理メニューがあります。MySQLをクリックすると画像のような画面になります。
mysql2.png
ここからphpMyadminをクリックすると、データベース参照、作成ページに飛ぶことができます。もちろんこれ以外の方法でもデータベースをいじくることはできるそうですが、本ではphpMyadminを使用する方法が書かれているので、それで進めていきます。

phpMyadminについて、操作方法は本と現在ではややレイアウトが異なるものの、ほぼ同じです。
mysql3.png
アクセスするとこんな画面から始まります。Languageで日本語を選ぶと日本語化されるのでより操作しやすいと思います。

mysql4.png
データベースの作成もこの新規作成をクリックして、名前、照合順序を入れるだけで簡単に作れます。ちなみにmySQLのフォルダーの中に新しく作るのではなく、普通に新規作成をクリックしたらいいそうです、僕はなぜか読み違えて、mySQLのフォルダーの中に丁寧に作っていました・・・
作成したデータベースをクリックするだけで、テーブル、カラムの作成がかんたんにできます。やはりグラフィカルに操作できるのは素晴らしいなと思いました。

データベースへの接続

データベースへの接続は本に載っているコードだと以下のようになっています。

thanks.php
<?php
$dsn='mysql:dbname=phpkiso;host=localhost';
$user='root';
$password='';
$dbh=new PDO($dsn, $user, $password);
$dbh->query('SET NAMES utf8');

〜(真ん中は省略)

$sql = 'INSERT INTO anketo(nickname,email,goiken)VALUES("'.$nickname.'","'.$email.'","'.$goiken.'")';
$stmt = $dbh->prepare($sql);
$stmt->execute();

MAMPの場合、このコードだとエラーが出ます。その理由はmySQLのパスワードが空っぽになっていることです。上に貼った1枚目の画像にある通り、passwordは初期だとrootになっているので、それを指定しないといけません。
それに加えて修正すべき点として、文字コードの指定部分です。文字コードはデータベースの文字化けを防ぐため指定する必要がありますが、現在のバージョンのPHPでは脆弱性が発生するのを避けるためSETNAMESを使用しないそうです。
ちなみに$sql=以下の部分は、変更の必要はないそうです。
そこで以下のようなコードに、調べながら変更しました。

thanks.php
<?php
$dsn='mysql:dbname=phpkiso;host=localhost;charset=utf8';
$user='root';
$password='root';
$dbh=new PDO($dsn, $user, $password);

このようにpasswordを指定し、また文字コードの指定部分を頭に持ってきました。これでエラーなくMySQLに接続できました!

MySQLのみ切断する

MAMPでは前述の通り、ローカルサーバーを起動すると自動的にMySQLに接続されてしまいます。なのでサーバーだけを起動させて、MySQLを止めるということができません。これの何が問題かというと、本の中にもデータベースに接続できなかった時の障害発生を示す文章を表示させる方法がありますが、それが実際に表示できるかどうか容易に確認できません。
そこでmySQLのみを切断する方法を調べたところ、ターミナルで以下のコードを入れればいいそうです。

/Applications/MAMP/bin/stopMysql.sh

みたままMAMPのstopMysqlを作動させるという形のコマンドになっています。これを入れるとMySQLを止めることができるのですが・・・MAMPの最新バージョンだとMySQLの作動ランプがなく、一目で作動が分からなくなっています。なので実際にデータを送信して試すしかないようです。
ちなみに一度サーバーを停止し、改めて起動するとmySQLも起動することができます。

まとめ

ここまで3つの記事でまとめたように進めると、macおよびMAMPを使用している環境下でも無事にこの本一冊を終えることができました。少しでも参考になれば幸いです!初学者なので間違いなどがあるかもしれません、何かあればご指摘をよろしくお願いいたします。

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

PHPでYoutubeのAPIを利用したい

この記事ですること

この記事では私のようなPHP初心者がYouYubeのAPIを使った様子を紹介致します。
PHPはもちろん、プログラミングスキルも低いです。初心者が初心者なりに頑張った様子を皆さまに見ていただきたいです。よろしくお願いいたします。

ちなみに開発環境ですが、

OS == Windows10
PHP == 7.4.11
Composer == 2.0.6

です。

YouTube Data API

YouTube Data APIはYouTubeとデータのやり取りをするため、デペロッパーを対象に提供されているサービスです。Googleアカウントを持っている人であれば気軽に使用できるAPIです。

YouTube Data APIでやりたいこと

私はこのYouTube Data APIで下記のことをやってみたいと考えました。

1.フォームからキーワード検索を行う
2.検索結果を取得する(YouTubeのようにサムネ、タイトル、概要など)
3.取得した検索結果を使用してみる。

実装

まず最初に...

phpを実行する場所(ディレクトリ)を作成します。
私はXAMPPを使っているので
xampp/htdocsに新しく「youtube_api」というディレクトリを作成します

C:\xampp\htdocs>mkdir youtube_api 

YouTube Data APIを使うために「Google APIs Client Library for PHP」が必要です。まずはこれをComposerでインストールします。

*Composerをインストールしていない方はこちらへ

Composerのインストール、コマンドプロンプトで動くことを確認できたら先ほど作成したディレクトリ内で下記のコマンドを打ちこみます。

composer require google/apiclient:"^2.7"

すると
vendor(ディレクトリ)
composer.json(ファイル)
composer.lock(ファイル)

というファイルが追加されます。
そして、ディレクトリ構造は以下のようになります。

youtube_api
|
+-----vendor
|       |
|       +-----諸々のファイル
|
|
+-----composer.json
|
+-----composer.lock
|
+-----sample.php

vendorディレクトリにあるautoload.phpをソースコードに取り込んでいきます。

APIキーを手に入れる


つぎにソースコードで実際にYouTube Data APIを使用するために必要な「APIコード」を手に入れます。
APIキーはGoogleアカウントをお持ちの方であれば手に入れることができます。
今回使うAPIのキーの手に入れ方を詳しく説明してくれる記事がすでに存在します。


[PHP]YouTube APIの利用(動画情報の取得) 筆者: @kuzira_vimmer さん


図を用いて非常にわかりやすく説明してあります。ぜひご参考にしてください。(私も参考にさせていただきました。)

まずはサンプルコードから

先ほど紹介させていただいた記事にも記載されていますが、PHPでYouTube Data API v3を使うサンプルコードが公式に用意されています。まずはこれを実行し、理解していきました。

ディレクトリ「youtube_api」内にsample.phpという名前のファイルを作成し、コピーします。

sample.php
<?php

/**
 * Library Requirements
 *
 * 1. Install composer (https://getcomposer.org)
 * 2. On the command line, change to this directory (api-samples/php)
 * 3. Require the google/apiclient library
 *    $ composer require google/apiclient:~2.0
 */
if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
  throw new \Exception('please run "composer require google/apiclient:~2.0" in "' . __DIR__ .'"');
}

require_once __DIR__ . '/vendor/autoload.php';

$htmlBody = <<<END
<form method="GET">
  <div>
    Search Term: <input type="search" id="q" name="q" placeholder="Enter Search Term">
  </div>
  <div>
    Max Results: <input type="number" id="maxResults" name="maxResults" min="1" max="50" step="1" value="25">
  </div>
  <input type="submit" value="Search">
</form>
END;

// This code will execute if the user entered a search query in the form
// and submitted the form. Otherwise, the page displays the form above.
if (isset($_GET['q']) && isset($_GET['maxResults'])) {
  /*
   * Set $DEVELOPER_KEY to the "API key" value from the "Access" tab of the
   * Google API Console <https://console.developers.google.com/>
   * Please ensure that you have enabled the YouTube Data API for your project.
   */
  $DEVELOPER_KEY = 'REPLACE_ME';

  $client = new Google_Client();
  $client->setDeveloperKey($DEVELOPER_KEY);

  // Define an object that will be used to make all API requests.
  $youtube = new Google_Service_YouTube($client);

  $htmlBody = '';
  try {

    // Call the search.list method to retrieve results matching the specified
    // query term.
    $searchResponse = $youtube->search->listSearch('id,snippet', array(
      'q' => $_GET['q'],
      'maxResults' => $_GET['maxResults'],
    ));

    $videos = '';
    $channels = '';
    $playlists = '';

    // Add each result to the appropriate list, and then display the lists of
    // matching videos, channels, and playlists.
    foreach ($searchResponse['items'] as $searchResult) {
      switch ($searchResult['id']['kind']) {
        case 'youtube#video':
          $videos .= sprintf('<li>%s (%s)</li>',
              $searchResult['snippet']['title'], $searchResult['id']['videoId']);
          break;
        case 'youtube#channel':
          $channels .= sprintf('<li>%s (%s)</li>',
              $searchResult['snippet']['title'], $searchResult['id']['channelId']);
          break;
        case 'youtube#playlist':
          $playlists .= sprintf('<li>%s (%s)</li>',
              $searchResult['snippet']['title'], $searchResult['id']['playlistId']);
          break;
      }
    }

    $htmlBody .= <<<END
    <h3>Videos</h3>
    <ul>$videos</ul>
    <h3>Channels</h3>
    <ul>$channels</ul>
    <h3>Playlists</h3>
    <ul>$playlists</ul>
END;
  } catch (Google_Service_Exception $e) {
    $htmlBody .= sprintf('<p>A service error occurred: <code>%s</code></p>',
      htmlspecialchars($e->getMessage()));
  } catch (Google_Exception $e) {
    $htmlBody .= sprintf('<p>An client error occurred: <code>%s</code></p>',
      htmlspecialchars($e->getMessage()));
  }
}
?>

<!doctype html>
<html>
  <head>
    <title>YouTube Search</title>
  </head>
  <body>
    <?=$htmlBody?>
  </body>
</html>

このサンプルコード内にある $DEVELOPER_KEY = 'REPLACE_ME';というコードの 'REPLACE_ME'の部分を先ほど手に入れたAPIキーに変更します。

ここで補足情報ですが、APIキーやアクセストークンは基本、他人に知られてはいけません。

ですので環境変数でAPIキーを設定し、使うようにしましょう

環境変数をセットするためにWindowsの方はコマンドプロンプトで「set」コマンドを使いましょうAPIキーを格納する環境変数は「API_KEY」という名前にします

C:\xampp\htdocs\youtube_api>set API_KEY=自分のAPIキー

またsample.phpないの37行目にある $DEVELOPER_KEY = 'REPLACE_ME';も書き換えていきます。

PHPにあるライブラリ getenv()を使用して変数DEVELOPER_KEYに環境変数で設定したAPI_KEYを格納します

$DEVELOPER_KEY = getenv('API_KEY');

これでサンプルコードを動かす準備ができました。動かしてみましょう

*注意
私のXAMPP環境で実行したところAPIキーが上手く機能せず、認証エラーになりました。
そこで、コマンドラインからコマンドでプログラムを実行したところ正常に動きました。

作成したディレクトリ内で下記のコマンドを入力し、http://localhost:8080/sample.phpにアクセスしてください

php -S 127.0.0.1:8080

これでしっかりプログラムが動くと思います。

サンプルコードを簡単に読み解く

ではこのサンプルコードからどのような動きをしているのか、また公式ドキュメントからどんな情報が取得可能なのか把握していきます。

まず最初にサンプルコード17行目、78行目にある「$htmlBody」ですが、これはブラウザでHTMLを表示するための部分です。 ヒアドキュメントと言います。ヒアドキュメントを使うことで長い文字列を扱うことができます。
また、$htmlbodyが二つあるのは検索フォームを表示する時、YouTube Data APIで動画を検索した時の2パターンを作るためです。



次にAPIを使用するための認証、承認です。(サンプルコード39,40行目)

$client = new Google_Client(); //39行目
$client->setDeveloperKey($DEVELOPER_KEY); //40行目

$youtube = new Google_Service_YouTube($client);  //43行目

39行目でGoogle_Clientをインスタンス化,40行目で自身のAPIキーを使ってGoogleの提供しているAPIの使用を可能にします。
43行目のコードはYouTube Data APIのリクエストを行う変数を定義します。今後APIに対してリクエストを行う時はこの変数$youtubeを使います。

APIに実際にリクエストを行うためのパラメータ入力、検索結果の格納は50~53行目にあります。

$searchResponse = $youtube->search->listSearch('id,snippet', array(
      'q' => $_GET['q'],
      'maxResults' => $_GET['maxResults'],
    ));

このコードによってフォームの「q」,「maxResults」を受け取り、API機能のひとつ「search」の中に存在する「listSearch」にフォームから受け取った情報をパラメータとして代入、$searchResponseに検索結果を格納できます。

変数「searchResponse」はキーワードを検索したときに表示される動画を上からかぞえて$maxResult個取得します。

ここまで理解できれば、あとは検索結果がどんなプロパティを所持しているのか確認することで様々なデータを取得、表示することができます。

*ちなみにlistSearchによって検索結果が複数取得できます。
そのため出力方法がfor文での出力になっています。

YouTube Data API searchで取得できるデータ

これから話すことは全て公式ドキュメントに記述されています。

公式のページを閲覧したいかたはこちらへ(YouTube Data API Search)
もう1つご紹介させていただきます。こちら(YouTube Data API Search:list)
私の記事と同時進行で読んでいただけると記事の理解が深まると思います。

サンプルコードでは以下の3つのプロパティが登場しています。

$searchResult['snippet']['title'] // -> 検索結果のタイトル
$searchResult['id']['videoId'] // -> 動画のクエリ("https://www.youtube.com/watch?v="ここに入る文字列)
$searchResult['id']['channelId'] // -> 検索ワードに合致するチャンネルID("https://www.youtube.com/channel/"ここに入る文字列)
$searchResult['id']['playlistId'] // -> プレイリストを開いた時,URL: "https://www.youtube.com/watch?v="動画のクエリ"&list=ここに入る文字列"

確認も兼ねてテストをします。
「キーワード=櫻坂46」、「動画取得数=10」でAPIから情報を収集してみます。
youtubeDataAPI(テスト).PNG

ソースコード内ではcase文であらかじめ取得データが動画なのか、チャンネル名なのかチェックしています。
プレイリストは見つかりませんでしたが、動画タイトル、動画ID、チャンネル名、チャンネルIDの取得ができました。

他にもたくさんのデータが取得可能です。ドキュメントは日本語版もありますので是非参考にしてください。

YouTube Data APIで動画情報を取得し、色々表示させてみる

駆け足ですが、かんたんな構造が理解でき、どんな情報を取得できるのかわかってきましたので少しコードを書き替えていきます。

まず最初に自分がYouTube Data APIを使ってやってみたいことを列挙してみます。


  1. 表示するものを動画とチャンネルのみに絞る

  2. サムネの表示

  3. チャンネルの画像表示

  4. サムネをクリックすると動画リンクへ移動する

  5. 動画の投稿者を特定する

  6. 動画投稿者のチャンネルへ移動する

  7. 動画,チャンネルに番号を振ってしっかり検索ができているのか確認できるようにする

上記の機能を持つプログラムを実装していきます。
また、実現にあたって1度紹介させていただいたリンクを今一度共有させていただきます。
YouTube Data APIのメソッド,プロパティ一覧(公式)

完成したコードがこちらになります。コメントも書いてありますが、わかりずらい可能性が高いです。

<?php

if (!file_exists(__DIR__ . '/vendor/autoload.php')) {
  throw new \Exception('please run "composer require google/apiclient:~2.0" in "' . __DIR__ .'"');
}

require_once __DIR__ . '/vendor/autoload.php';

//検索ワードが入っていないときに表示されるHTML
$htmlBody = <<<END
<form method="GET">
  <div>
    Search Term: <input type="search" id="q" name="q" placeholder="Enter Search Term">
  </div>
  <div>
    Max Results: <input type="number" id="maxResults" name="maxResults" min="1" max="50" step="1" value="25">
  </div>
  <input type="submit" value="Search">
</form>
END;

// This code will execute if the user entered a search query in the form
// and submitted the form. Otherwise, the page displays the form above.
if (isset($_GET['q']) && isset($_GET['maxResults'])) {
  /*
   * Set $DEVELOPER_KEY to the "API key" value from the "Access" tab of the
   * Google API Console <https://console.developers.google.com/>
   * Please ensure that you have enabled the YouTube Data API for your project.
   */
  //$DEVELOPER_KEY = 'REPLACE_ME';
  $DEVELOPER_KEY = getenv('API_KEY');//環境変数から取得
  $client = new Google_Client();//インスタンス化
  $client->setDeveloperKey($DEVELOPER_KEY); //APIKEYを入力して承認する

  // Define an object that will be used to make all API requests.
  $youtube = new Google_Service_YouTube($client);//YouTube Data APIは今後この変数を使用する

  $htmlBody = '';//HTMLに表示する中身を初期化
  try {

    // Call the search.list method to retrieve results matching the specified
    // query term.
    $searchResponse = $youtube->search->listSearch('id,snippet', array(
      'q' => $_GET['q'], //検索するキーワードを取得
      'maxResults' => $_GET['maxResults'], //取得する数の制限
    ));

    $videos = '';
    $channels = '';
    //$playlists = '';

    // Add each result to the appropriate list, and then display the lists of
    // matching videos, channels, and playlists.



    $video_number = 1; //動画のカウント
    $channel_number = 1; //チャンネルのカウント
    foreach ($searchResponse['items'] as $searchResult) {//検索にヒットした結果分処理を繰り返します。
      switch ($searchResult['id']['kind']) {
        case 'youtube#video':
          $videos .= sprintf('<div id="contents"><p>検索結果: %d<br>%s</p><p><a href="https://www.youtube.com/watch?v=%s" target="_blank"><img src ="%s"></a> <br>チャンネル名: %s <a href="https://www.youtube.com/channel/%s" target="_blank">投稿者のページへ</a></p></div>',
              $video_number, $searchResult['snippet']['title'], $searchResult['id']['videoId'], $searchResult['snippet']['thumbnails']['high']['url'], $searchResult['snippet']['channelTitle'],$searchResult['snippet']['channelId']);
              $video_number++;
          break;
        case 'youtube#channel':
          $channels .= sprintf('<div id="contents"><p>検索結果: %d</p><img src="%s"> <p>%s <a href="https://www.youtube.com/channel/%s" target="_blank">チャンネルページへ</a></p></div>',
              $channel_number, $searchResult['snippet']['thumbnails']['medium']['url'],$searchResult['snippet']['title'], $searchResult['id']['channelId']);
              $channel_number++;
          break;
        /*
        case 'youtube#playlist':
          $playlists .= sprintf('<li>%s (%s)</li>',
              $searchResult['snippet']['title'], $searchResult['id']['playlistId']);
          break;
          */
      }
    }

    $htmlBody .= <<<END
    <div id="title">
    <h2>検索結果はコチラです</h2>
    *動画をクリックすると動画のページへ移動します*
    </div>
    <h3>動画一覧</h3>
    <ul>$videos</ul>
    <h3>チャンネル一覧</h3>
    <ul>$channels</ul>

END;
  } catch (Google_Service_Exception $e) {
    $htmlBody .= sprintf('<p>A service error occurred: <code>%s</code></p>',
      htmlspecialchars($e->getMessage()));
  } catch (Google_Exception $e) {
    $htmlBody .= sprintf('<p>An client error occurred: <code>%s</code></p>',
      htmlspecialchars($e->getMessage()));
  }
}
?>

<!doctype html>
<html>
  <head>
    <title>YouTube Search</title>
    <!--CSSを直接埋め込んでいく-->
    <style>
      #title{
        text-align: center;
      }
      h3{
        text-align: center;
      }
      #contents{
        position: relative;
        left: 19%;
        text-align: center;
        width: 750px; 
        height: 500px;
        border-style: solid;
        border-color: lightblue;
      }
    </style>
  </head>
  <body>
    <?=$htmlBody?>
  </body>
</html>

実際に実行してみます。実行方法は先ほどと同様です。

検索キーワード: ポケモン
取得動画数: 10

実行結果1
youtubeDataAPI(改造結果1).PNG
実行結果2
youtubeDataAPI(改造結果2).PNG

しっかり動画とチャンネルが表示されていることが確認できます。また、リンクの接続も確認したところ上手くいきました。
APIから情報を取得し、表示することに成功しました。

最後に

今記事ではPHP初心者の私がAPIを使って簡単なプログラムを作成する過程を記事にさせていただきました。YouTube Data APIはまだまだたくさんの機能があるので是非自分で使っていただきたいです。

記事執筆を通じて「APIの使い勝手の良さ」、「公式ドキュメントの重要さ」を今一度確認することができました。
今後はAPIから取得したデータを収集、加工することでデータの活用を意識した開発を目指していたいです。
文章が拙い部分、厳密ではない部分があったと思いますが、読んでいただきありがとうございました。

ソースコードはGitHubアカウントにて公開しています。お時間がある方は覗いてみてください
最後に作成したコード(GitHub)

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