20200806のPHPに関する記事は8件です。

【PHP】標準エラー出力する4つの方法

PHP で標準エラー出力にメッセージを出力する方法(各種)

Qiita 記事に絞って「php 標準エラー出力」でググっても、ピンポイントのタイトルの記事がなかったので、自分のググラビリティとして。

TL; DR

シンプルで確実な標準エラー出力
fputs(STDERR, 'my error message');
標準出力を潰して標準エラー出力だけ確認してみる
$ # 標準出力へ出力(1>/dev/nullで標準出力を潰している)
$ php -r 'fputs(STDOUT, "hoge". PHP_EOL);' 1>/dev/null
$
$ # 標準エラー出力へ出力
$ php -r 'fputs(STDERR, "hoge". PHP_EOL);' 1>/dev/null
hoge
$ # STDERR定数の内容確認
$ php -r 'echo STDERR . PHP_EOL;'
Resource id #3

TS; DR

I/O ストリーム編

fwrite + STDERR定数

標準エラー出力への「出力ストリーム・リソース割り当て済み定数」である「STDERR」定数を使う。fclose() の心配はいらない。

入出力ストリーム定数を使った方法
$msg_error = 'standard error' . PHP_EOL;
fwrite(STDERR, $msg_error);

fputs + STDERR定数

fwrite を使うと fclose も使わないといけない印象を与えてしまうため、fputs のエイリアスが用意されている。可読性を優先するならこちら。

fwriteのエイリアス関数fputsを使ってもOK
$msg_error = 'standard error' . PHP_EOL;
fputs(STDERR, $msg_error);
  • 参考文献: fputs | ファイルシステム関数 @ PHP 公式マニュアル

エラー処理関数編

error_log 関数の出力先を、標準エラー出力のファイル・ポインタ(php://stderr)に設定する。後日、ファイル出力が必要な場合にも対応できるメリットがある。バイナリセーフではありません。

PHPの設定ディレクティブ"error_log"に標準エラー出力へのファイルポインタを割り当てて
ini_set('error_log', 'php://stderr');

$msg_error = 'standard error' . PHP_EOL;
error_log($msg_error);
$ # 設定ディレクティブの確認(未設定を確認)
$ php -r 'echo ini_get("error_log") . PHP_EOL;'

$ php -r 'error_log("hoge");' 1>/dev/null
hoge

$ # 設定を変更してログ出力
$ php -r 'ini_set("error_log", "php://stderr"); echo ini_get("error_log") . PHP_EOL;'
php://stderr
$ php -r 'error_log("hoge");' 1>/dev/null
hoge
  • 参考文献: error_log | エラー処理関数 @ PHP 公式マニュアル

ファイル・ストリーム編

php:// ファイルストーリム

「site:php.net/manual/ja 標準エラー出力」と PHP 公式マニュアルに絞ってググると出てくる方法。古いブロクでも良く見かける方法。

$msg_error= 'standard error' . PHP_EOL;

$stderr = fopen('php://stderr', 'w');
fwrite($stderr, $msg_error);
fclose($stderr);

「ファイルを開く」「書き込む」「クローズ」という王道であるものの、いささか冗長に見えてしまう。また「ファイルのクローズ」といった無駄な心配事が増える気もするため、可読性が良いとは言えない。

  • 参考文献
    • php:// | プロトコルラッパー @ PHP 公式マニュアル
    • コメント | error_log @ PHP 公式マニュアル
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

タイムゾーンはちゃんと設定しよう

サービスを開発する際、最初にタイムゾーンをしっかり設定していないと、MySQLのcreated_at や updated_at などをうまく使えないというようなことになります。

MySQLでは、カラムにcreated_at や updated_atなどを加えると、それぞれSQLのinsertやupdateが実行されたときに自動でtimestampを入れてくれたりします。

始めにしっかり設定しておかないと、あとから必要になったときに、自力でそれらの値を取得して挿入するはめになってしまいます。(実体験)

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

【PHP入門】CRUD機能

はじめに

今回は、PHPとMySQLを使ってMVCモデルでCRUD機能を実装していきます。

CRUD機能とは?

ほぼ全てのソフトウェアが有する4つの永続的な基本機能の頭文字をそれぞれ並べた用語のこと。
その4つの機能とは、Create(生成)、Read(読み取り)、Update(更新)、Delete(削除)を指す。

バージョン

PHP:7.4.5
phpMyAdmin:5.0.2
MySQL:5.7.30

今回作成するファイル

conf / html / model / view の4つのディレクトリを作って進めます。

conf(繰り返し使用するもの定義)
- const.php

html(controller)
- bbs.php
- bbs_insert.php
- bbs_update.php
- bbs_delete.php

model
- db.php
- functions.php
- bbs.php

view
- bbs_view.php

テーブルの作成

bbs.sql
CREATE TABLE `bbs` (
    `id` int(11) NOT NULL,
    `name` varchar(100) NOT NULL,
    `comment` varchar(100) NOT NULL,
    `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `updated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)

ALTER TABLE `bbs`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,
  ADD PRIMARY KEY (`id`);

定義

const.php
<?php
// ディレクトリパス
define('MODEL_PATH', $_SERVER['DOCUMENT_ROOT'] . '/../model/');
define('VIEW_PATH', $_SERVER['DOCUMENT_ROOT'] . '/../view/');

// MySQL接続用
define('DB_HOST', 'ホスト名');
define('DB_NAME', 'データベース名');
define('DB_USER', 'ユーザ名');
define('DB_PASS', 'パスワード');
define('DB_CHARSET', 'utf8');

// URL
define('BBS_URL', '/bbs.php');

Viewの作成

bbs_view.php
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>ひとこと掲示板</title>
  </head>

  <body>
    <h1>ひとこと掲示板</h1>

    <!-- 投稿フォーム -->
    <form method="post">
      <div>
        <label>名前</label>
        <input type="text" name="name">
      </div>
      <div>
        <label>コメント</label>
        <input type="text" name="comment">
      </div>
      <input type="submit" value="送信">
    </form>

    <!-- 表示 -->
    <table>
      <thead>
        <tr>
          <th>名前</th>
          <th>コメント</th>
          <th>投稿時間</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
        </tr>
      </tbody>
    </table>
  </body>

</html>

Create機能の実装

Modelの作成

modelの中にdb.phpを作成(データベース関係)

db.php
<?php

function get_db_connect(){
    // MySQL用のDSN文字列
    $dsn = 'mysql:dbname='. DB_NAME. ';host='. DB_HOST. ';charset='. DB_CHARSET;

    try{
      // DB接続
      $dbh = new PDO($dsn, DB_USER, DB_PASS, array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4'));
      $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
      $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
      $dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    }catch(PDOException $e){
        exit('接続できませんでした。理由:'.$e->getMessage());
    }
    return $dbh;
}

function execute_query($db, $sql, $params = array()){
  try{
    $statement = $db->prepare($sql);
    return $statement->execute($params);
  }catch(PDOException $e){
    set_error('更新に失敗しました。');  
  }
  return false;
}

modelの中にfunctions.phpを作成(その他機能)

functions.php
<?php

function redirect_to($url){
  header('Location:'. $url);
  exit;
}

function get_post($name){
  if(isset($_POST[$name]) === true){
    return $_POST[$name];
  };
  return '';
}  

// セッション
function get_session($name){
  if(isset($_SESSION[$name]) === true){
      return $_SESSION[$name];
  };
  return '';
}

function set_session($name, $value){
  $_SESSION[$name] = $value;
}

// エラーメッセージ
function set_error($error){
  $_SESSION['__errors'][] = $error;
}

function get_errors(){
  $errors = get_session('__errors');
  if($errors === ''){
      return array();
  }
  set_session('__errors', array());
  return $errors;
}

// メッセージ
function set_message($message){
  $_SESSION['__message'][] = $message;
}

function get_messages(){
  $messages = get_session('__message');
  if($messages === ''){
      return array();
  }
  set_session('__message', array());
  return $messages;
}

modelの中にbbs.phpを作成(掲示板関係)

bbs.php
<?php
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'db.php';

// Create
function post_comment($db, $name, $comment){
  $sql = "
    INSERT INTO
      bbs(
        name,
        comment
      )
    VALUES(?,?)
  ";
  return execute_query($db, $sql, array($name, $comment));
}

Viewの作成

bbs_view.phpにCreate処理へ遷移するよう追記
- 処理結果を表示できるよう追記
- formタグにactionを追記

bbs_view.php
<!-- 処理結果のメッセージ -->
<?php foreach(get_errors() as $error){ ?>
    <p><?php print $error; ?></p>
<?php } ?>
<?php foreach(get_messages() as $messages){ ?>
    <p><?php print $messages; ?></p>
<?php } ?>

<form method="post" action="bbs_insert.php">
  <div>
    <label>名前</label>
    <input type="text" name="name">
  </div>
  <div>
    <label>コメント</label>
    <input type="text" name="comment">
  </div>
  <input type="submit" value="送信">
</form>

Controllerの作成

htmlの中にbbs.phpを作成(bbs_view.php表示用)

bbs.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'bbs.php';

session_start();

include_once VIEW_PATH. 'bbs_view.php';

htmlの中にbbs_insert.phpを作成(Insert処理用)

bbs_insert.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'bbs.php';

session_start();

// データベース接続
$db = get_db_connect();

// Postされたものを定義
$name = get_post('name');
$comment = get_post('comment');

// Insert
if(post_comment($db, $name, $comment)){
    set_message('コメントを投稿しました。');
}else{
    set_error('投稿に失敗しました。');
}

redirect_to(BBS_URL);

Read機能の実装

Createで挿入されたデータを表示されるようにしていきます。

Modelの作成

db.phpにRead機能を追記

db.php
function fetch_all_query($db, $sql, $params = array()){
  try{
    $statement = $db->prepare($sql);
    $statement->execute($params);
    return $statement->fetchAll();
  }catch(PDOException $e){
    set_error('データ取得に失敗しました。');
  }
  return false;
}

bbs.phpに追記

bbs.php
// Read
function get_comments($db, $id){
  $sql = "
    SELECT
      id,
      name,
      comment,
      created
    FROM
      bbs
  ";
  return fetch_all_query($db, $sql);
}

Viewの作成

bbs_view.phpに追記
- tableタグ内のtbodyタグに追記

bbs_view.php
<tbody>
  <?php foreach($bbs as $data){ ?>
  <tr>
    <td><?php print($data['name']); ?></td>
    <td><?php print($data['comment']); ?></td>
    <td><?php print($data['created']); ?></td>
    <td></td>
  </tr>
  <?php } ?>
</tbody>

Controllerの作成

bbs.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'bbs.php';

session_start();

// SELECT文で選択したデータを表示させる為に追記
$db = get_db_connect();
$bbs = get_comments($db);

include_once VIEW_PATH. 'bbs_view.php';

Update機能の実装

Createで生成されたデータを変更できるようにしていきます。

Modelの作成

bbs.phpにUpdate機能を追記

bbs.php
// Update
function update_comment($db, $id, $comment){
  $sql = "
    UPDATE
      bbs
    SET
      comment = ?
    WHERE
      id = ?
  ";
  return execute_query($db, $sql, array($comment, $id));
}

Viewの作成

bbs_view.phpにUpdate処理へ遷移するよう追記
- tableタグ内のtbodyに追記

bbs.php
<tbody>
  <?php foreach($bbs as $data){ ?>
  <tr>
    <td><?php print($data['name']); ?></td>
    <td>
      <form method="post" action="bbs_update.php">
        <input type="text" name="comment" value="<?php print($data['comment']); ?>">
        <input type="submit" value="変更">
        <!-- hiddenで該当idをpost -->
        <input type="hidden" name="id" value="<?php print($data['id']); ?>">
      </form>    
    </td>
    <td><?php print($data['created']); ?></td>
    <td></td>
  </tr>
  <?php } ?>
</tbody>

Controllerの作成

htmlの中にbbs_update.phpを作成(Update処理用)

bbs_update.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'bbs.php';

session_start();

// データベース接続
$db = get_db_connect();

// Postされたものを定義
$id = get_post('id');
$comment = get_post('comment');

// Update
if(update_comment($db, $id, $comment)){
    set_message('コメントを変更しました。');
  } else {
    set_error('コメントの変更に失敗しました。');
  }

  redirect_to(BBS_URL);

Delete機能の実装

Createで生成されたデータを削除できるようにしていきます。

Modelの作成

bbs.phpにDelete機能を追記

bbs.php
// Delete
function delete_comment($db, $id){
  $sql = "
    DELETE FROM
      bbs
    WHERE
      id = ?
  ";
  return execute_query($db, $sql, array($id));
}

Viewの作成

bbs_view.phpにDelete処理へ遷移するよう追記
- tableタグ内のtbodyに追記

bbs.php
<tbody>
  <?php foreach($bbs as $data){ ?>
  <tr>
    <td><?php print($data['name']); ?></td>
    <td>
      <form method="post" action="bbs_update.php">
        <input type="text" name="comment" value="<?php print($data['comment']); ?>">
        <input type="submit" value="変更">
        <input type="hidden" name="id" value="<?php print($data['id']); ?>">
      </form>    
    </td>
    <td><?php print($data['created']); ?></td>
    <td>
      <form method="post" action="bbs_delete.php">
        <input type="submit" value="削除">
        <input type="hidden" name="id" value="<?php print($data['id']); ?>">
      </form>
    </td>
  </tr>
  <?php } ?>
</tbody>

Controllerの作成

htmlの中にbbs_delete.phpを作成(Delete処理用)

bbs.php
<?php
require_once '../conf/const.php';
require_once MODEL_PATH. 'functions.php';
require_once MODEL_PATH. 'bbs.php';

session_start();

// データベース接続
$db = get_db_connect();

// Postされたものを定義
$id = get_post('id');

// Delete
if(delete_comment($db, $id)){
    set_message('コメントを削除しました。');
  } else {
    set_error('コメントの削除に失敗しました。');
  }

  redirect_to(BBS_URL);'

参考

CRUD機能と7つのアクションの関係性
MVCモデルについて

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

PHPでGoogle Geocoding APIを呼び出す

GCPのGeocoding APIをPHP(Guzzle)を使って呼び出してみます。

APIキーを用意する

https://console.cloud.google.com/apis/library/geocoding-backend.googleapis.com
GCPでGeocoding APIを有効化し、APIキーを発行します。

PHPからAPIを呼び出す

Guzzleをインストール

HTTPクライアントのGuzzleをインストールします。

composer require guzzlehttp/guzzle

APIを呼び出す

$api_key には発行したAPIキーの文字列を設定します。

geocode.php
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;

$http_client = new Client();
$url = 'https://maps.googleapis.com/maps/api/geocode/json';
$api_key = '**********************************';

/*
 Geocoding (latitude/longitude lookup)
 住所から緯度経度を取得
 */
$address = '東京都北区赤羽';

try {
    $response = $http_client->request('GET', $url, [
        'headers' => [
            'Accept' => 'application/json',
        ],
        'query' => [
            'key' => $api_key,
            'language' => 'ja',
            'address' => $address,
        ],
        'verify' => false,
    ]);
} catch (ClientException $e) {
    throw $e;
}

$body = $response->getBody();
echo $body . PHP_EOL;

/*
 Reverse geocoding (address lookup)
 緯度経度から住所を取得
 */
$latitude = '35.7799638';
$longitude = '139.723053';
$latlng = $latitude . ',' . $longitude;

try {
    $response = $http_client->request('GET', $url, [
        'headers' => [
            'Accept' => 'application/json',
        ],
        'query' => [
            'key' => $api_key,
            'language' => 'ja',
            'latlng' => $latlng,
        ],
        'verify' => false,
    ]);
} catch (ClientException $e) {
    throw $e;
}
$body = $response->getBody();
$json = json_decode($body);
echo $json->results[0]->formatted_address;

実行結果は次のようになります。

>php geocode.php
{
   "results" : [
      {
         "address_components" : [
            {
               "long_name" : "赤羽",
               "short_name" : "赤羽",
               "types" : [ "political", "sublocality", "sublocality_level_2" ]
            },
            {
               "long_name" : "北区",
               "short_name" : "北区",
               "types" : [ "locality", "political" ]
            },
            {
               "long_name" : "東京都",
               "short_name" : "東京都",
               "types" : [ "administrative_area_level_1", "political" ]
            },
            {
               "long_name" : "日本",
               "short_name" : "JP",
               "types" : [ "country", "political" ]
            },
            {
               "long_name" : "115-0045",
               "short_name" : "115-0045",
               "types" : [ "postal_code" ]
            }
         ],
         "formatted_address" : "日本、〒115-0045 東京都北区赤羽",
         "geometry" : {
            "bounds" : {
               "northeast" : {
                  "lat" : 35.792101,
                  "lng" : 139.7267569
               },
               "southwest" : {
                  "lat" : 35.7753371,
                  "lng" : 139.716646
               }
            },
            "location" : {
               "lat" : 35.7799638,
               "lng" : 139.723053
            },
            "location_type" : "APPROXIMATE",
            "viewport" : {
               "northeast" : {
                  "lat" : 35.792101,
                  "lng" : 139.7267569
               },
               "southwest" : {
                  "lat" : 35.7753371,
                  "lng" : 139.716646
               }
            }
         },
         "place_id" : "ChIJveNASxuTGGAR2OV0KRI3zV4",
         "types" : [ "political", "sublocality", "sublocality_level_2" ]
      }
   ],
   "status" : "OK"
}

日本、〒115-0045 東京都北区赤羽2丁目17−12
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでOption型を利用してnullチェックや例外ハンドラを消す

ScalaやRustを普段使ってる方には当たり前にあるOption型ですが、残念ながらPHPには組み込みされていません。
しかし、php-optionを利用することで同様の体験を得ることが出来ます。
https://github.com/schmittjoh/php-option

こんなコード書いてませんか?

例外を投げる

例えばRepositoryからEntityを取得する処理でこんなコードを書いてませんか?

Repository
public function findById(Id $id): Entity
{
  $result = $this->queryBuilder->...
  if ($result === null) {
    throw New NotFoundException('message');
  }
  return Entity::create($result);
}

データベースからデータを取得して、データがなければ例外、あればEntityとして返却するというコードを書いたことがある人もいるかと思います。
しかし、この関数を利用する際に適切に例外ハンドラを作らないと意図しないエラー画面を出力してしまう可能性があります。

is_nullで値の確認

こんな処理も書いていないでしょうか?

$hoge = $entity->getHoge();
if (is_null($hoge)) {
    return; // デフォルトで返したい値など
}
return $hoge;

とあるEntityから取得できるValueObject等がnullableの場合にis_nullなどを利用してチェックする処理などを書いていないでしょうか?
また、このようなチェックを見逃して意図しない値をViewに渡してしまうなどしてないでしょうか?

値がある型、値がない型に分ける

Some型とNone型という、なにか値のある型と値がない型に分けることで処理をわかりやすくします。

先程のRepositoryでのコードを例に書き換えてみます。

Repository
public function findById(Id $id): Option
{
  $result = $this->queryBuilder->...
  if ($result === null) {
    return None::create();
  }
  return Some::create(Entity::create($result));
}

結果がnullだった場合はNone型、値があればSome型として返却します。
このOption型をどう使うかは次のis_nullチェックしていた例を見てみます。

$hoge = $entity->getHoge();
return $hoge->getOrElse('デフォルトで返したい値など');

getHoge()がRepositoryの例のようにOption型が返ってくると想定しています。Option型にはgetOrElseというメソッドがあり、値があれば取得できて値がなければ引数で指定した値を返すという関数です。

また、値があることだけを確認したい場合も

$hoge = $entity->getHoge();
if ($hoge->isEmpty()) {
  // None型
}
if ($hoge->isDefined()) {
  // Some型
}

このようにメソッドが用意されているので簡単に分岐を作ることが出来ます。

まとめ

Option型を使うことで明示的に値があるかもしれないし、無いかもしれないという型を作ってその振る舞いを扱いやすくできました。
ちなみにphp-optionはphpdotenvで利用されていて知りました。
env('KEY', {default})はまさに値があるかもしれないし、無いかもしれないものですよね。

小言

PhanとかPHPStanを使えばコメントでジェネリクスを表現してIDEでサポートされたりするんでしょうか?(知らない)
ジェネリクスを書きたい、Hackを書くしか無いか・・・

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

Laravel+AWS-SDK-PHPによるDynamoDBのデータ取得

背景

LaravelでDynamoDB使うにあたり、ORMのlaravel-dynamodbなどもあるが、DynamoDBでORM利用するのが少し違和感あった為にSDK使ってやってみた際のメモ(そもそもPHPのWEBフレームワークでやるなという話ではあるが、せっかくSDKが提供されているので一度使ってみようという感じ)

環境

・Windows 10
・HomeStead 9.5
・PHP 7.4
・Laravel 6.0

1.準備

AWS-SDK-PHPのインストール

composer require aws/aws-sdk-php

DynamoDBの環境作成

AWS提供のローカルDynamoDBを利用。Vagrantを使っていたので、Dockerと共存が面倒なことになるのでローカルに直接インストールしました。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DynamoDBLocal.html

※GUIツール(結構便利)
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/workbench.settingup.html

DynamoDB起動

java -Djava.library.path=C:\localsandbox\DynamoDB\DynamoDBLocal_lib -jar C:\localsandbox\DynamoDB\DynamoDBLocal.jar -port 8001 -sharedDb

※HomesteadのVirtualBoxと初期ポート8000が被るので、8001で起動させる

テスト用テーブルの作成

テーブル名:SensorLog
パーティションキー:device_id
ソートキー:receive_dt

aws dynamodb create-table --table-name SensorLog --attribute-definitions AttributeName=device_id,AttributeType=S AttributeName=receive_dt,AttributeType=N --key-schema AttributeName=device_id,KeyType=HASH AttributeName=receive_dt,KeyType=RANGE --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 --endpoint-url http://localhost:8001 

※キーのない項目はテーブル作成時に指定できないので、テストデータ作成時に適当に作る。
※テストデータはGUIで適当に作成

.envに情報設定

DYNAMODB_REGION=ap-northeast-1
#↓vagrantのlocalhost:xxxxだとつながらないのでローカルIPを設定
DYNAMODB_LOCAL_ENDPOINT=http://192.168.11.99:8001
DYNAMODB_DEBUG=true

2.データ取得

・パーティションキーで絞ってデータ検索した結果の最新を取得する。
・パーティションキー+ソートキー(範囲指定)による複合条件検索の結果リストを取得する

use Aws\DynamoDb\Exception\DynamoDbException;
use Aws\DynamoDb\Marshaler;
use Aws\Sdk as AwsSdk;

class SensorLog
{
    /**
     * @var DynamoDbClient
     */
    private $client;

    /**
     * @var Marshaler
     */
    private $marshaler;

    /**
     * DynamoModel constructor.
     */
    public function __construct()
    {
        // ローカルDBであっても"credentials"定義しないとエラーが吐かれるので注意
        $sdk = new AwsSdk([
            'credentials' => [
                'key' => env('DYNAMODB_KEY'),
                'secret' => env('DYNAMODB_SECRET'),
            ],
            'endpoint'   => env("DYNAMODB_LOCAL_ENDPOINT"),
            'region'   => env("DYNAMODB_REGION"),
            'version'  => 'latest'
        ]);

        $this->client = $sdk->createDynamoDb();
        $this->marshaler = new Marshaler();
    }

    /**
     * パーティションキー指定による最新データを取得する
     */
    public function getFirst($id)
    {
        $tableName = 'SensorLog';

        // 検索条件設定:device_idを検索条件にする
        $key_value = $id;
        $jsonStr = '{'.'":device_id":'.'"'.$key_value.'"'.'}';
        $eav = $this->marshaler->marshalJson($jsonStr);

        // Limit:1 取得件数を1件に制限
        // ScanIndexForward:false ソートキー(receive_dt)を降順指定
        $params = [
            'TableName' => $tableName,
            'KeyConditionExpression' => '#id = :device_id',
            'ExpressionAttributeNames'=> [ '#id' => 'device_id' ],
            'Limit' => 1,
            'ScanIndexForward' => false,
            'ExpressionAttributeValues'=> $eav,
        ];

        try {
            $result = $this->client->query($params);
            $data = array();
            foreach ($result['Items'] as $recdData) {
                $data[] =  $this->marshaler->unmarshalItem($recdData);
            }
            return $data;
        } catch (DynamoDbException $e) {
            throw $e;
        }
    }

    /**
     * パーティションキー+対象日付のデータリストを取得する
     */
    public function getDataByDateTime(int $id, string $dt)
    {
        $tableName = 'SensorLog';

        // 対象日付の最小時間と最大時間を設定する
        // receive_dt:YYYYMMDDHHMMSSの値は数値型で格納されている
        $from_dt = $dt."000000";
        $to_dt = $dt."235959";

        // バインド変数を設定(device_id,from_dt,to_dt)
        $key_value = $id;
        $jsonStr = '{' . '":device_id":' . '"' .$key_value.'",":from_dt":'.$from_dt.',":to_dt":'.$to_dt.'}';
        $eav = $this->marshaler->marshalJson($jsonStr);

        // device_idとreceive_dtによる範囲検索を行う
        // ScanIndexForward:false ソートキー(receive_dt)を降順指定
        $params = [
            'TableName' => $tableName,
            'KeyConditionExpression' => '#id = :device_id and receive_dt between :from_dt and :to_dt',
            'ExpressionAttributeNames'=> [ '#id' => 'device_id' ],
            'ScanIndexForward' => false,
            'ExpressionAttributeValues'=> $eav,
        ];

        try {
            $result = $this->client->query($params);
            $data = array();
            foreach ($result['Items'] as $recdData) {
                $data[] =  $this->marshaler->unmarshalItem($recdData);
            }
            return $data;
        } catch (DynamoDbException $e) {
            throw $e;
        }
    }
}

呼び出し

・コントローラーで呼び出し

//1.指定IDの最新データを1件取得
$sensorLog = $sensor->getFirst($id);
//2.2020-08-06のデータを検索して結果リストを取得
$sensorLogList = $sensor->getDataByDateTime($id, "20200806");
dump($sensorLogList);

・結果
image.png

参考

https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.PHP.04.html

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

Adapterパターンを使うメリットとPHP実装例

※この投稿はJava言語で学ぶデザインパターン入門を読んだ感想と復習を兼ねたまとめです

Adapterパターンとは

Adapterパターンはデザインパターンのひとつで、既存のクラスを修正することなく、インタフェースを変更するための手法です。

Java言語で学ぶデザインパターン入門では、電化製品のACアダプタを例に説明されていました。家庭用のコンセントは100ボルトで固定されていますが、ノートパソコンが必要としているのは12ボルトの電源です。この「既に提供されているもの」と「いま必要としているもの」の差を吸収するのがAdapterの役割です。

Adapterパターンに登場する役割は、それぞれ次のように呼ばれるようです。

役割 意味
Target いま必要としているもの(インタフェース) ノートパソコンを動かすための12ボルトの電源
Adapter TargetとAdapteeの差を埋めるもの ノートパソコンと家庭用コンセントをつなぐACアダプタ
Adaptee 既に提供されているもの 家庭用コンセントの100ボルトの電源

この関係をクラス図で表現すると次のようになります。

スクリーンショット 2020-08-06 9.44.45.png

また、AdapterパターンはWrapperパターンと呼ばれることもあるようです。

継承を利用したAdapterパターン実装例

前提として、次のようなBannerクラスが既に実装されているとします。

class Banner
{
    private $string;

    public function __construct(string $string)
    {
        $this->string = $string;
    }

    public function showWithParen(): void
    {
        echo '(' . $this->string . ')' . "\n";
    }

    public function showWithAster(): void
    {
        echo '*' . $this->string . '*' . "\n";
    }
}

Bannerは、文字列を出力する方法を2パターン(showWithParen()showWithAster())持っています。

しかし、いま必要とされているのは次のようなインタフェースの実装です。

interface PrintInterface
{
    public function printWeak(): void;
    public function printStrong(): void;
}

このPrintInterfaceを経由して、Bannerが持っているメソッドを呼び出す方法を考えます。

Adapterパターンには2つの実現方法がありますが、さきに紹介するのは継承を利用した方法です。

次のようなAdapterを作成します。

class PrintBanner extends Banner implements PrintInterface
{
    public function printWeak(): void
    {
        $this->showWithParen();
    }

    public function printStrong(): void
    {
        $this->showWithAster();
    }
}

上記のPrintBannerは、既に実装されているBannerを継承して、いま必要とされているPrintInterfaceを実装しています。このようなAdapterを間にはさむことによって、既存のクラスを変更することなく、呼び出し元はインタフェース経由で目的の関数を呼び出すことが可能になります。

委譲を利用したAdapterパターン実装例

Adapterパターンを実現するもうひとつの方法は、委譲を利用する方法です。

委譲という言葉は知りませんでしたが、利用したいクラスのインスタンスを生成し、そのインスタンスを経由して目的の処理を実行することを委譲というようです。クラス図で表現すると次のようになります。

スクリーンショット 2020-08-06 9.44.24.png

委譲を利用する方法は、Targetがインタフェースではなくクラスである場合に便利です。

// Targetがクラスである場合
abstract class PrintAbstract
{
    abstract public function printWeak(): void;
    abstract public function printStrong(): void;
}

さきほどはAdpterがAdapteeを継承することでAdapterパターンを実現できましたが、複数のクラスを同時に継承することはできないため、Targetがクラスである場合にはそれが使えません。代わりに、委譲を利用して次のように実現します。

class PrintBanner extends PrintAbstract
{
    private $banner; // Bannerをプロパティとして持つ

    public function __construct(string $string)
    {
        $this->banner = new Banner($string);
    }

    public function printWeak(): void
    {
        $this->banner->showWithParen(); // 委譲を利用してBannerの関数を呼び出す
    }

    public function printStrong(): void
    {
        $this->banner->showWithAster();
    }
}

Adapterパターンのメリット

Adapterパターンは、既存のクラスを修正することなく、インタフェースを変更するための手法です。

既存のクラスを使い回すことのメリットには、次のようなものがあるようです。

  • ゼロから実装するよりも早く完成する
  • 既存のクラスがテスト済みである場合は、確実に動作するものを使いまわせる
    • バグが発生した場合は、Adapter側に原因があることが明らかなので修正も容易
    • 既存のクラスは修正しないため、テストを書き直す必要がない
  • 委譲を利用すればTargetがクラスである場合も他のクラスの実装を利用できる

参考までに、Adapterパターン実装例(PHP)を載せておきます。

https://github.com/yuta-o-note/gof-design-pattern

<?php

/**
 * 継承を利用して実現するパターン
 */

interface PrintInterface
{
    public function printWeak(): void;
    public function printStrong(): void;
}

class Banner
{
    private $string;

    public function __construct(string $string)
    {
        $this->string = $string;
    }

    public function showWithParen(): void
    {
        echo '(' . $this->string . ')' . "\n";
    }

    public function showWithAster(): void
    {
        echo '*' . $this->string . '*' . "\n";
    }
}

class PrintBanner extends Banner implements PrintInterface
{
    public function printWeak(): void
    {
        $this->showWithParen();
    }

    public function printStrong(): void
    {
        $this->showWithAster();
    }
}

function main(PrintInterface $print)
{
    $print->printWeak();
    $print->printStrong();
}

$print = new PrintBanner('Hello');
main($print);
<?php

/**
 * 委譲を利用して実現するパターン
 */

abstract class PrintAbstract
{
    abstract public function printWeak(): void;
    abstract public function printStrong(): void;
}

class Banner
{
    private $string;

    public function __construct(string $string)
    {
        $this->string = $string;
    }

    public function showWithParen(): void
    {
        echo '(' . $this->string . ')' . "\n";
    }

    public function showWithAster(): void
    {
        echo '*' . $this->string . '*' . "\n";
    }
}

class PrintBanner extends PrintAbstract
{
    private $banner;

    public function __construct(string $string)
    {
        $this->banner = new Banner($string);
    }

    public function printWeak(): void
    {
        $this->banner->showWithParen();
    }

    public function printStrong(): void
    {
        $this->banner->showWithAster();
    }
}

function main()
{
    $print = new PrintBanner('Hello');
    $print->printWeak();
    $print->printStrong();
}

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

PHP 条件演算子(三項演算子)の省略構文「??」

目的

  • 先輩方が記載したコードを読んでいて初めて知ったものだったのでまとめる

三項演算子??

  • この演算子は別名null合体演算子とも呼ばれる。
  • 下記に例を記載する。

    式1 ?? 式2;
    
  • 式1がnullではない場合には式1が実行される。

  • 式1がnullの時は式2が実行される。

具体例

  • 変数$strに文字列を格納する処理を記載する。
  • 変数$base_strに文字列が格納されている時は変数$base_strの文字列を変数$strに格納する。
  • 変数$base_strがnullの時は文字列「base_str is null」を変数$strに格納する。

    $str = $base_str ?? 'base_str is null';
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む