20200123のPHPに関する記事は13件です。

CakePHPとAjaxでいいねボタンをつくりました

1週間前のAjax何もわかってない自分に向けて書きます。
CakePHP3.8を使っています。

つくりたいもの

Twitterのいいねボタンみたいなやつ。
掲示板の投稿メッセージ一覧に、メッセージに付随して表示する。
自分がボタンをすでに押している場合は、ボタンに色がついている。押していない場合、色がついていない。
他ユーザー含めて、いいねボタンが押された数を表示する。

まず

jQueryを勉強するところから始めました。

テーブルの構成

関係あるところらへんだけ

  • Users
    • id (int)
    • username(関係ない)
    • password(関係ない)
  • Posts
    • id (int)
    • message(関係ない)
  • Favorites
    • id (int)
    • user_id (int)
    • post_id (int)

とりあえずメッセージ一覧画面に、いいねボタンといいね数を表示する

関係ありそうなところだけ抜粋して載せています。
色々と省略しています。

PostsTable.php
public function initialize(array $config)
{
    // PostsとFavoritesは1対多
    $this->hasMany('Favorites', [
        'foreignKey' => 'post_id',
    ]);

}

// 投稿メッセージを一覧表示するのに使います
// メッセージごとのいいねの数も一緒に取得しています
public function findBbsPosts()
{
    $query = $this->find();
    $query
        ->contain(['Users', 'Favorites'])
        ->select(['favorites_count' => $query->func()->count('Favorites.id')])
        ->leftJoinWith('Favorites')
        ->group(['Posts.id'])
        ->enableAutoFields(true);

    return $query;
}
FavoritesTable.php
public function initialize(array $config)
{

    $this->belongsTo('Users', [
        'foreignKey' => 'user_id',
        'joinType' => 'INNER',
    ]);

    $this->belongsTo('Posts', [
        'foreignKey' => 'post_id',
        'joinType' => 'INNER',
    ]);
}

// 投稿メッセージごとのいいねの数をカウントするのに使います
public function countFavorite(int $post_id)
{
    $query = $this->find();
    $query
        ->where(['post_id' => $post_id]);
    return $query;
}

とりあえず投稿メッセージを一覧表示させます。
$authuserにログインしているユーザーについての情報を入れています。

PostsController.php
public function initialize()
{
    // 色々と省略

    $this->loadComponent('Paginator');

    $this->set('authuser', $this->Auth->user());
}

public function index()
{
    $bbsPosts = $this->paginate($this->Posts->findBbsPosts(), [
        'order' => ['Posts.id' => 'DESC'],
    ]);

    $this->set(compact('bbsPosts'));
}

投稿メッセージをelementに切り出しています。

Posts/index.ctp
<?php foreach ($bbsPosts as $bbsPost) : ?>
    <?= $this->element('article', ['bbsPost' => $bbsPost]) ?>
<?php endforeach; ?>

さらにarticle.ctpの中でもいいねボタンに関連する部分だけelementに切り出しています。

article.ctp
<?= $this->element('favorite', ['bbsPost' => $bbsPost]) ?>

すごく格好悪いのですが、array_searchを使って、「ログインしているユーザのID」と、「投稿メッセージごとにくっついているFavoritesのuser_id」で一致するものがあるかどうか探しにいきます。(効率が良くないです。)

  • 一致するものがあれば、favorite-add-buttonを非表示
  • 一致するものがなければ、favorite-delete-buttonを非表示
  • 非表示は、フォームを囲むdivのclassにhideを追加しています。

ハートマークの表示の違いは、Font Awesomeを使っています。
ボタンのclassのfas fa-heartfar fa-heartで変えています。

ついでに、フォームを囲むdivのid属性に、投稿メッセージごとのPostsのidをつけています。

favorite.ctp
<!-- まず、ログインユーザーが投稿ごとにいいねボタンを押しているかどうかを確認します -->
<?php $searchFavorite = array_search($authuser['id'], array_column($bbsPost->favorites, 'user_id'), true) ?>

<!-- 一致するものがなければ、まだ押していないので、「add」のフォームを表示 -->
<?php if ($searchFavorite === false) : ?>
    <div class="favorite" id="addfavorite<?php echo h($bbsPost->id) ?>">
<!-- 一致するものがあれば、すでに押しているので、「add」のフォームを非表示(hide追加) -->
<?php else : ?>
    <div class="favorite hide" id="addfavorite<?php echo h($bbsPost->id) ?>">
<?php endif; ?>

        <?= $this->Form->create('', [
            'url' => [
                'controller' => 'Favorites',
                'action' => 'add',
            ],
            'class' => 'favorite-form'
        ]) ?>
        <?php
            echo $this->Form->hidden('user_id', ['value' => $authuser['id']]);
            echo $this->Form->hidden('post_id', ['value' => $bbsPost->id]);
        ?>
        <?= $this->Form->button('', [
            'class' => 'far fa-heart favorite-add-button',
        ]) ?>
        <?= $this->Form->end() ?>
    </div>

<!-- 一致するものがあれば、すでに押しているので、「delete」のフォームを表示 -->
<?php if ($searchFavorite !== false) : ?>
    <div class="favorite" id="deletefavorite<?php echo h($bbsPost->id) ?>">
<!-- 一致するものがなければ、まだ押していないので、「delete」のフォームを非表示(hide追加) -->
<?php else : ?>
    <div class="favorite hide" id="deletefavorite<?php echo h($bbsPost->id) ?>">
<?php endif; ?>

        <?= $this->Form->create('', [
            'url' => [
                'controller' => 'Favorites',
                'action' => 'delete',
            ],
            'class' => 'favorite-form',
        ]) ?>
        <?php
            echo $this->Form->hidden('user_id', ['value' => $authuser['id']]);
            echo $this->Form->hidden('post_id', ['value' => $bbsPost->id]);
        ?>
        <?= $this->Form->button('', [
            'class' => 'fas fa-heart favorite-delete-button',
        ]) ?>
        <?= $this->Form->end() ?>
    </div>
css
.favorite.hide {
    display: none;
}

カウント部分はPostsTable.phpselect(['favorites_count' => $query->func()->count('Favorites.id')])として取得したものを使います。

favorite.ctp
<p class="favCount" id="favCount<?php echo h($bbsPost->id) ?>">
    <?php if (! isset($bbsPost->favorites_count)) : ?>
        0
    <?php else : ?>
        <?= $this->Number->format($bbsPost->favorites_count) ?>
    <?php endif; ?>
</p>

Ajaxを使ってハートマークとカウント数を変化させます

コントローラでデータベースに保存したり削除したりします。

FavoritesController.php
public function add()
{
    $this->autoRender = false;

    $favorite = $this->Favorites->newEntity();

    if ($this->request->is('ajax')) {
        $received_data = $this->request->getData();

        $favorite->user_id = $this->request->getData('user_id');
        $favorite->post_id = $this->request->getData('post_id');

        if ($this->Favorites->save($favorite)) {
            // FavoritesTable.phpに書いたcountFavorite()を使っていいねの数を取得します
            $count = $this->Favorites->countFavorite($favorite->post_id)->count();

            // favorite-button-change.jsに'received_data'と'count'を渡します
            $this->response->body(json_encode(['received_data' => $received_data, 'count' => $count]));
            return;
        }
        $this->Flash->error(__('The favorite could not be saved. Please, try again.'));
    }
}

public function delete()
{
    $this->autoRender = false;

    if ($this->request->is('ajax')) {
        $received_data = $this->request->getData();

        $param = [
            'user_id' => $this->request->getData('user_id'),
            'post_id' => $this->request->getData('post_id'),
        ];

        if ($this->Favorites->deleteAll($param)) {
            $count = $this->Favorites->countFavorite($this->request->getData('post_id'))->count();

            $this->response->body(json_encode(['received_data' => $received_data, 'count' => $count]));
            return;
        } else {
            $this->Flash->error(__('The favorite could not be deleted. Please, try again.'));
        }
    }
}
favorite-button-change.js
$(function () {
    // 「add」のボタンをクリックしたとき
    $('.favorite-add-button').click(function (event) {
        event.preventDefault();

        var param = $(this).parent('.favorite-form').serializeArray();

        $.ajax({
            url: '/bbs/Favorites/add', // FavoritesControllerのaddアクションに送ります
            type: 'POST',
            dataType: 'json',
            data: param,
            timeout: 10000,
        }).done(function (result) { // 成功の場合
            // FavoriteControllerのaddアクションのreturnの結果がresultに入っています
            // divのclass属性のhideを逆にします
            $('#addfavorite' + result['received_data']['post_id']).addClass('hide');
            $('#deletefavorite' + result['received_data']['post_id']).removeClass('hide');

            // いいね数を取得した数に書き換えます
            $('#favCount' + result['received_data']['post_id']).text(result['count']);
        }).fail(function (XMLHttpRequest, textStatus, errorThrown) { // 失敗の場合
            alert("失敗");
        });
    });

    // 「delete」のボタンをクリックしたとき
    $('.favorite-delete-button').click(function (event) {
        event.preventDefault();

        var param = $(this).parent('.favorite-form').serializeArray();

        $.ajax({
            url: '/bbs/Favorites/delete',
            type: 'POST',
            dataType: 'json',
            data: param,
            timeout: 10000,
        }).done(function (result) {
            $('#addfavorite' + result['received_data']['post_id']).removeClass('hide');
            $('#deletefavorite' + result['received_data']['post_id']).addClass('hide');
            $('#favCount' + result['received_data']['post_id']).text(result['count']);
        }).fail(function (XMLHttpRequest, textStatus, errorThrown) {
            alert("失敗");
        });
    });
});

拙い出来なので、ご指摘いただけるととてもありがたいです。

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

【初学者向け】セキュリティ対策入門[番外編①]〜OSコマンドインジェクション編〜

前提

確認環境

以下と同様です。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

本シリーズの目的

以下と同様です。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

本記事の目標

OSコマンドインジェクションの概要、原因、対策について理解することです。

本記事を読み進める上での必要事項

以下の内容を終えていることです。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

どうでもいいお話

あまり主要ではない脆弱性についてはこちらの番外編で扱っていきます。主要ではないというのは被害ケースをあまり耳にしないという意味で、脆弱性をつかれたときの被害が少ないというわけではないので悪しからず。

概要

OSコマンドについて不正な命令を入れ込むことです。こちらのSQLインジェクションがSQL文について不正な命令を入れ込むことならば、こちらはOSコマンドというわけです。

実際に見てみましょう。とはいえ、番外編なのでGitHubにソースはないので本記事上で紹介します。

実際に見てみよう

<?php

$file_list = shell_exec("ls {$_GET['param']}");
echo $file_list;

PHPファイルでOSコマンドを実行したい場合、shell_execという関数を使う必要があります。

では、実際にどういった場合に被害が発生するかというと、| {任意のOSコマンド}のような文字列をクエリストリングスのparamに与えられたときです。OSコマンドについて学んだことがある方なら、これのやばさが伝わると思います。自由なOSコマンドが使えれば任意のファイルを書き換えたり削除したり様々な被害をもたらすことが可能です。

めちゃくちゃ恐ろしいですね。

原因

入力値をそのままOSコマンドの一部に渡していることです。

対策

そもそも使わない

OSコマンドをユーザの入力で行わなければいけないケースというのはなかなかないはずです。あったとしてもライブラリを使用するなどの選択肢を検討する必要があります。

そもそも使わない観点では、php.iniで特定の関数を実行不可にできるので本対策を行う場合はそれも合わせて行った方がいいでしょう。

エスケープする

上述の通り、こういった処理は実装しないのが一番です。とはいえ、どうしても実装したい場合はエスケープする関数もあります。escapeshellargという関数です。具体的にすると以下のような感じです。

<?php

$param = escapeshellarg($_GET['param']);

$file_list = shell_exec("ls {$param}");
echo $file_list;

参考文献

独習PHP 第3版

今回の内容は以上です。最後までご覧いただきありがとうございました。

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

【初学者向け】セキュリティ対策入門[番外編]①〜OSコマンドインジェクション編〜

前提

確認環境

以下と同様です。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

本シリーズの目的

以下と同様です。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

本記事の目標

OSコマンドインジェクションの概要、原因、対策について理解することです。

本記事を読み進める上での必要事項

以下の内容を終えていることです。
【初学者向け】セキュリティ対策入門⓪〜環境構築編〜

どうでもいいお話

あまり主要ではない脆弱性についてはこちらの番外編で扱っていきます。主要ではないというのは被害ケースをあまり耳にしないという意味で、脆弱性をつかれたときの被害が少ないというわけではないので悪しからず。

概要

OSコマンドについて不正な命令を入れ込むことです。こちらのSQLインジェクションがSQL文について不正な命令を入れ込むことならば、こちらはOSコマンドというわけです。

実際に見てみましょう。とはいえ、番外編なのでGitHubにソースはないので本記事上で紹介します。

実際に見てみよう

<?php

$file_list = shell_exec("ls {$_GET['param']}");
echo $file_list;

PHPファイルでOSコマンドを実行したい場合、shell_execという関数を使う必要があります。

では、実際にどういった場合に被害が発生するかというと、| {任意のOSコマンド}のような文字列をクエリストリングスのparamに与えられたときです。OSコマンドについて学んだことがある方なら、これのやばさが伝わると思います。自由なOSコマンドが使えれば任意のファイルを書き換えたり削除したり様々な被害をもたらすことが可能です。

めちゃくちゃ恐ろしいですね。

原因

入力値をそのままOSコマンドの一部に渡していることです。

対策

そもそも使わない

OSコマンドをユーザの入力で行わなければいけないケースというのはなかなかないはずです。あったとしてもライブラリを使用するなどの選択肢を検討する必要があります。

そもそも使わない観点では、php.iniで特定の関数を実行不可にできるので本対策を行う場合はそれも合わせて行った方がいいでしょう。

エスケープする

上述の通り、こういった処理は実装しないのが一番です。とはいえ、どうしても実装したい場合はエスケープする関数もあります。escapeshellargという関数です。具体的にすると以下のような感じです。

<?php

$param = escapeshellarg($_GET['param']);

$file_list = shell_exec("ls {$param}");
echo $file_list;

参考文献

独習PHP 第3版

今回の内容は以上です。最後までご覧いただきありがとうございました。

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

videoタグがipadでうまく再生できない考察

はじめに

動画ファイルがたくさん溜まってきたのでwebで検索し再生するソフトを作成した。html5からvideoタグが使えるようになり、新しいwebの作り方を勉強しつつ作成したものです。しかし、パソコン・Androidスマホでwebを操作し再生することはできたのだが、ipadで再生することができなかった。はじめてのvideoでもあり、使い方に間違いがあるのかもしれませんが、結局原因がわかりませんでした。

今回、調査した内容をまとめるとともに、当面の運用として対処法を示す。

使用したプログラム

原因調査をするため、プログラムをシンプルにした。プログラムはhtmlファイルとビデオを提供するphpファイルです。
対象動画ファイル(003.mp4):サイズは1280x720、長さは2:04:33、ファイルサイズは1.33GiB
大きいファイルサイズの動画が対象のため、一度に全てを送信するにはサーバの負担が大きすぎるため、最大1MBに制限しクライアントから逐次複数回要求により全体を提供する方法とした。

index.html
<html lang='ja'>
    <head>
    <meta http-equiv='content-type' content='application/html; charset=EUC-JP' >
    </head>
    <body>
    <div id='video'>
        <video controls width="640" autoplay playsinline>
            <source src="movie_mp4t.php">
        </video>
    </div>
    </body>
</html>
movie_mp4t.php
<?php
    function write_log($text) {
        umask(0);
        $log = date("Y-m-d H:i:s") . " {$_SERVER['REQUEST_URI']}    {$text}\n";
        error_log($log, 3, "access.log");
    }

    $file = "003.mp4";                                                      // 動画ファイルへのパス
    $size = filesize($file);
    $fp = fopen($file, "rb");
    $step = 1000000;                                                        // 最大伝送サイズ
    if (@$_SERVER["HTTP_RANGE"]){                                           // ブラウザがHTTP_RANGEを要求してきた場合
        list($start, $end) = sscanf($_SERVER["HTTP_RANGE"], "bytes=%d-%d"); // 要求された開始位置と終了位置を取得
        $s = $end - $start + 1;
        write_log('$_SERVER["HTTP_RANGE"]:'.$_SERVER["HTTP_RANGE"]);
        if (empty($end)) $end = $start + $step - 1;                         // 終了位置が指定されていない場合$step bytes出す
        fseek($fp, $start);
    } else {
        $start = 0;
        $end = $step - 1;
    }
    if ($end - $start >= $step) $end = $start + $step - 1;                  // 要求が$stepより多い場合$stepに制限
    if ($end >= $size - 1) $end = $size - 1;                                // 要求が動画の終了を超えている場合制限
    $c_size = $end - $start + 1;                                            // 提供サイズ
    $etag = md5($_SERVER["REQUEST_URI"]).$size;                             // コンテンツの識別子
    $header_http = "HTTP/1.1 206 Partial Content";
    $header_content = "Content-Type: video/mp4";
    $header_accept = "Accept-Ranges: bytes";
    $header_range = "Content-Range: bytes {$start}-{$end}/{$size}";
    $header_length = "Content-Length: {$c_size}";
    $header_etag = "Etag: \"{$etag}\"";
    header($header_http);
    write_log($header_http);
    header($header_content);
    write_log($header_content);
    header($header_accept);                                                 // HTTP_RANGE(部分リクエスト)に対応
    write_log($header_accept);
    header($header_range);
    write_log($header_range);
    header($header_length);
    write_log($header_length);
    header($header_etag);
    write_log($header_etag);
    if ($c_size) echo fread($fp, $c_size);                                  // ファイルポインタの開始位置からコンテンツ長だけ出力
    fclose($fp);
?>

状況

以下のログのとおり、複数回のやり取り後止まってしまいます。
ログが長いので固定メッセージを省略し示す。

access.log
2020-01-15 08:22:06 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=0-1
2020-01-15 08:22:06 /movie4/movie_mp4t.php  HTTP/1.1 206 Partial Content
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Type: video/mp4
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Accept-Ranges: bytes
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Range: bytes 0-1/1432153772
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Length: 2
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Etag: "c7ad0632263096871e3aaf236ccf7e301432153772"
2020-01-15 08:22:06 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=0-1432153771
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Range: bytes 0-999999/1432153772
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:06 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1423966208-1432153771
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Range: bytes 1423966208-1424966207/1432153772
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:06 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1427505152-1432153771
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Range: bytes 1427505152-1428505151/1432153772
2020-01-15 08:22:06 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1432092672-1432153771
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Range: bytes 1432092672-1432153771/1432153772
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 61100
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1424228320-1427505151
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Range: bytes 1424228320-1425228319/1432153772
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1427800032-1432092671
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Range: bytes 1427800032-1428800031/1432153772
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1425228320-1427505151
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Range: bytes 1425228320-1426228319/1432153772
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1426228320-1427505151
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Range: bytes 1426228320-1427228319/1432153772
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1427228320-1427505151
2020-01-15 08:22:07 /movie4/movie_mp4t.php  HTTP/1.1 206 Partial Content
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 276832
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1428800032-1432092671
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Range: bytes 1428800032-1429800031/1432153772
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:07 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1429800032-1432092671
2020-01-15 08:22:07 /movie4/movie_mp4t.php  HTTP/1.1 206 Partial Content
2020-01-15 08:22:07 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:08 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1430800032-1432092671
2020-01-15 08:22:08 /movie4/movie_mp4t.php  Content-Range: bytes 1430800032-1431800031/1432153772
2020-01-15 08:22:08 /movie4/movie_mp4t.php  Content-Length: 1000000
2020-01-15 08:22:08 /movie4/movie_mp4t.php  $_SERVER["HTTP_RANGE"]:bytes=1431800032-1432092671
2020-01-15 08:22:08 /movie4/movie_mp4t.php  Content-Range: bytes 1431800032-1432092671/1432153772
2020-01-15 08:22:08 /movie4/movie_mp4t.php  Content-Length: 292640

考察

ログを以下のように整理する。
14回の要求で動画のindexを読み終えた段階で停止している。

最大サイズ1MB
            要 求                  応 答          転 送 
    ---------- ---------- ---------- ---------- ------- ----------------------------------
 1:          0-         1          0-         1       2 ファイルの識別子?2バイト読み込み
 2:          0-1432153771          0-    999999 1000000 ファイル全体を要求されるが1MB送信
 3: 1423966208-1432153771 1423966208-1424966207 1000000 動画のindex全体を要求されるが1MB送信
 4: 1427505152-1432153771 1427505152-1428505151 1000000 動画のindex
 5: 1432092672-1432153771 1432092672-1432153771 1000000 動画のindex
 6: 1424228320-1427505151 1424228320-1425228319 1000000 動画のindex
 7: 1427800032-1432092671 1427800032-1428800031 1000000 動画のindex
 8: 1425228320-1427505151 1425228320-1426228319 1000000 動画のindex
 9: 1426228320-1427505151 1426228320-1427228319 1000000 動画のindex
10: 1427228320-1427505151 1427228320-1427505151  276832 動画のindex
11: 1428800032-1432092671 1428800032-1429800031 1000000 動画のindex
12: 1429800032-1432092671 1429800032-1430800031 1000000 動画のindex
13: 1430800032-1432092671 1430800032-1431800031 1000000 動画のindex
14: 1431800032-1432092671 1431800032-1432092671  292640 動画のindex

転送最大サイズを変更して実施してみる。

最大サイズ2MB
            要 求                 応 答          転 送 
    ---------- ---------- ---------- ---------- ------- ----------------------------------
 1:          0-         1          0-         1       2 ファイルの識別子?2バイト読み込み
 2:          0-1432153771          0-   1999999 2000000 ファイル全体を要求されるが2MB送信
 3: 1423966208-1432153771 1423966208-1425966207 2000000 動画のindex全体を要求されるが2MB送信
 4: 1427505152-1432153771 1427505152-1429505151 2000000 動画のindex
 5: 1432092672-1432153771 1432092672-1432153771   61100 動画のindex
 6: 1424203752-1427505151 1424203752-1426203751 2000000 動画のindex
 7: 1427726312-1432092671 1427726312-1429726311 2000000 動画のindex
 8: 1426203752-1427505151 1426203752-1427505151 1301400 動画のindex
 9: 1429726312-1432092671 1429726312-1431726311 2000000 動画のindex
10: 1431726312-1432092671 1431726312-1432092671  366360 動画のindex
最大サイズ3MB
            要 求                 応 答          転 送 
    ---------- ---------- ---------- ---------- ------- ----------------------------------
 1:          0-         1          0-         1       2 ファイルの識別子?2バイト読み込み
 2:          0-1432153771          0-   2999999 3000000 ファイル全体を要求されるが3MB送信
 3: 1423966208-1432153771 1423966208-1426966207 3000000 動画のindex全体を要求されるが3MB送信
 4: 1427505152-1432153771 1427505152-1430505151 3000000 動画のindex
 5: 1432092672-1432153771 1432092672-1432153771   61100 動画のindex
 6: 1424179176-1427505151 1424179176-1427179175 3000000 動画のindex
 7: 1427677168-1432092671 1427677168-1430677167 3000000 動画のindex
 8: 1427179176-1427505151 1427179176-1427505151  325976 動画のindex
 9: 1430677168-1432092671 1430677168-1432092671 1415504 動画のindex
最大サイズ4MB
            要 求                 応 答          転 送 
    ---------- ---------- ---------- ---------- ------- ----------------------------------
 1:          0-         1          0-         1       2 ファイルの識別子?2バイト読み込み
 2:          0-1432153771          0-   3999999 4000000 ファイル全体を要求されるが4MB送信
 3: 1423966208-1432153771 1423966208-1427966207 4000000 動画のindex全体を要求されるが4MB送信
 4: 1427505152-1432153771 1427505152-1431505151 4000000 動画のindex
 5: 1432092672-1432153771 1432092672-1432153771   61100 動画のindex
 6: 1424277472-1427505151 1424277472-1427505151 3227680 動画のindex
 7: 1427767264-1432092671 1427767264-1431767263 4000000 動画のindex
 8: 1431767264-1432092671 1431767264-1432092671  325408 動画のindex
 9:     196584-1423966207     196584-   4196583 4000000 動画
10:    4196584-1423966207    4196584-   8196583 4000000 動画

最後の最大サイズ4MBにすることで正常に動画が再生された。
何れの最大サイズにおいても3:~5:の要求が全く同じで応答レンジとは全く関係がない。
正常に再生されない最大サイズでは、3:と4:の隙間を6:以降で補完しているが、4:と5:の補完が完全に行えていなく再生に失敗していると思われる。最大サイズ4MBの4:と5:には偶然(?)隙間がなく、再生できた。更に、サイズの大きな動画ファイルでは最大サイズ4MBでも再生に失敗することもわかっている。
ここでは、紹介しなかったがプログレッシブダウンロード対応のMP4ファイル(indexをファイルの前方に配置する)を作成し実験したが変化はなかった。

結論

原因については、ipad-osのバグと考えたほうが自然では有るが、これまでに対応されていないところを見るとなにか理由がありそうである。結局原因は全く分からず対処法として以下で対応している。
動画サイズによって、変動することから最大サイズを動画サイズの1/300とすることで取り敢えず再生できている。

-   $step = 1000000;                                                        // 最大伝送サイズ
+   $step = round($size / 300);                                             // 最大伝送サイズ

因みに、PCで再生した場合を以下に示す。若干クセがありますが、順次読み取っていくシンプルな読み方となっています。

最大サイズ1MB(PC)
            要 求                 応 答          転 送 
    ---------- ---------- ---------- ---------- ------- ----------------------------------
 1:          0-                    0-    999999 1000000 全体要求
 2: 1423966208-1432153771 1423966208-1424966207 1000000 動画のindex全体を要求されるが1MB送信
 3: 1424966208-           1424966208-1425966207 1000000 動画のindex
 4: 1425966208-1432153771 1425966208-1426966207 1000000 動画のindex
 5: 1426966208-           1426966208-1427966207 1000000 動画のindex
 6: 1427966208-1432153771 1427966208-1428966207 1000000 動画のindex
 7: 1428966208-           1428966208-1429966207 1000000 動画のindex
 8: 1429966208-1432153771 1429966208-1430966207 1000000 動画のindex
 9: 1430966208-           1430966208-1431966207 1000000 動画のindex
10: 1431966208-1432153771 1431966208-1432153771  187564 動画のindex
11:          0-1430966271          0-    999999 1000000 動画(?)
12:          0-1432153771          0-    999999 1000000 動画
13:    1000000-              1000000-   1999999 1000000 動画
14:    2000000-1432153771    2000000-   2999999 1000000 動画
15:    3000000-              3000000-   3999999 1000000 動画

参考

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

LaravelでUncaught domexception: failed to execute '***' on 'element': ',' is not a valid attribute name.を吐き出したときに確認したこと

Laravelにてアプリを作成していたところコンソールに下記のようなエラーが発生

Uncaught domexception: failed to execute 'setattribute' on 'element': ',' is not a valid attribute name.

簡単に訳すと「属性名に','(カンマ)は使えないから'setattribute'は実行できないよ!」
と怒られてしまってます。

考えたこと

コンソールのエラーだからJavaScriptのファイルに不備があるのか?
スクリーンショット 2020-01-23 午後5.36.59.png

右側のapp.js:41954から該当エラー個所をみてみるが何もおかしいところはなさそう。
そもそも","(カンマ)がその行にない...

「属性名」に異常?つまりviewファイル側におかしいところがあるのか?

index.blade.php
      <li>
        <a href="{{ action('WordsController@show', $word )}}">{{ $word->en }}</a>
        <a href="">{{ $word->ja }}</a>
        <a href="#", data-id="{{ $word->id }}">[×]</a>
        <form method="post", action=" {{ url('/words', $word->id) }} " id="form_{{ $word->id }}" >
        {{ csrf_field() }}
        {{ method_field('delete') }}
        </form>
      </li>

よーくみると

4行目のhref="#"の後ろのカンマ...
5行目のmethod="post"の後ろのカンマ...

いりません!!!

スクリーンショット 2020-01-23 午後5.36.17.png

これを消したらエラーも消えました。

おわりに

認証機能を追加するべく、Laravel6のuiパッケージを入れ込んでからこのエラーがで始めたので、色々と考え込んでしまったけれど、結局は小さなミスでした。
直前までUIも問題なく動いていたのも時間を食ってしまいました。
しかしながら探しても日本語の記事が全然ない...
同じようなことが起こった方と自分への忘備録として残しておきます。

参考リンク

https://github.com/OnsenUI/OnsenUI/issues/169

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

【初心者向け】関数について

意味      
関数 一つの処理をまとめたもの
引数 関数に渡せる値
返り値 関数から戻ってくる値
初期値 引数がない時の値

関数とは

処理をひとまとめにすることで使いまわせる
似たような処理をまとめられる

//定義
function sayHello() {

//処理
  echo "Hello!";
}

//呼び出し
sayHello();// Hello!
sayHello();// Hello! 何度も呼び出し可能

引数とは

関数に渡す値

//渡された引数が$nameに入る
function sayHello($name) {

//$nameは関数の中で使用できる
  echo "Hello!" . $name;
}

//関数に引数を渡す
sayHello("Tom");// Hello!Tom
sayHello("Bob");// Hello!Bob


//引数は複数指定できる
function sayHello($name,$ago) {

  echo "Hello!" . $name . $ago;
}

sayHello("Tom",19);// Hello!Tom19
sayHello("Bob",20);// Hello!Bob20

返り値

処理を終了させ、関数から戻ってくる値

function sayHello($name) {

//値を返す
  return "Hello!" . $name;

  echo "ここは処理をされない。";
}

$test = "jon";
//sayHello関数の呼び出し
echo sayHello($test);// Hello!jon

初期値

引数を指定しなった時の値

//初期値指定
function sayHello($name = "Jon") {

  echo "Hello!" . $name;
}

sayHello("TOM");// Hello!Tom
sayHello("Bob");// Hello!Bob
sayHello();     // Hello!Jon 初期値
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AmazonLinuxでPHP7.1から7.2にアップデート

概要

AmazonLinuxでPHP7.1から7.2にあげたときのメモです。
試行錯誤しながらやったので、これ通りに動かないことがあると思います。

  • yumからインストール
$ sudo yum -y install php72 php72-mbstring php72-pdo php72-php-fpm.x86_64

実行結果

: (中略)
Error
Requires: scl-utils
  • scl-utilsが必要らしいので、インストール
  • yumでは用意されていないので、rpmから
$ sudo rpm -Uvh ftp://ftp.scientificlinux.org/linux/scientific/6.9/x86_64/updates/fastbugs/scl-utils-20120927-29.el6_9.x86_64.rpm
  • 再度php72をインストール
$ sudo yum -y install php72 php72-mbstring php72-pdo php72-php-fpm.x86_64
  • 新しいバージョンに切替
$ sudo unlink /usr/bin/php
$ sudo ln -s /usr/bin/php72 /usr/bin/php
$ sudo /etc/init.d/php-fpm stop && sudo /etc/init.d/php72-php-fpm start
  • (Option) composerで必要なpackageを追加でインストール
$ sudo yum install php72-php-mbstring.x86_64 php72-php-pdo.x86_64 php72-php-xml.x86_64 php72-php-mysqlnd

困ったときに実行すると良いコマンド

  • インストールできるpackageを確認
$ yum list | grep php
  • インストール済みpackage確認
$ yum list installed | grep php
  • php-fpmリスタート
$ sudo service php72-php-fpm restart
  • nginxリスタート
$ sudo service nginx restart
  • php.iniの場所確認
$ php -r "echo phpinfo();" | grep "php.ini"

あとがき

Dockerにしたい、、

参考

https://qiita.com/kidatti/items/2d6a4a24f89dc71eb66e

https://skworkspace.net/archives/457

https://www.pgen.info/archives/506

https://qiita.com/ritukiii/items/624eb475b85e28613a70

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

Laravelでブログの本文の続きを「...」にする方法

{!! nl2br(e(str_limit(変数, 制限文字数))) !!}

で制限文字数から先の文章は「...」になります。

解説

nl2br

PHPの改行コードになります。(htmlで言う<br>
https://www.php.net/manual/ja/function.nl2br.php

e()

()内をエスケープさせます。

e関数は、PHPのhtmlspecialchars関数をdouble_encodeオプションにデフォルトでtrueを指定し、実行します。

https://readouble.com/laravel/5.7/ja/helpers.html (「e()」で文字検索してください。)

htmlspecialchars — 特殊文字を HTML エンティティに変換する

https://www.php.net/manual/ja/function.htmlspecialchars.php

エスケープ処理に関してはこちらを参照
https://qiita.com/n_hirai/items/df0a21d2409ee47973e5

まあ要は記号を特殊文字に変換させることです。そうすることによってユーザーが意図せず内部のプログラミングをいじってしまうことを防ぐことができます。
例えば、phpで $this->hensuu の値を取得する処理が書かれている場合、フォームでユーザーが「あああ<<<超えられない壁<<<いいい」とか打ってしまったら内部のプログラミングが<に反応してバグったらやばいからこの処理を使うのです。(もっと良い例えありませんか・・・?)

str_limit(変数、制限文字)

Str::limitでも行けます。繰り返しになってしまいますが、制限文字数から先の文章は「...」になります。
なんか最初からこうなってるみたいです。

str_limit関数は、指定した長さへ文字列を切り詰めます。

https://readouble.com/laravel/5.7/ja/helpers.html (「str_limit」で文字検索してください。)

{!! !!}

これでエスケープから逃れることができます。
通常Laravelで使われるblade構文の {} はhtmlからエスケープしてphp処理を行う際に使われるものでした。
それを「!!」にすることで否定系にしているのでhtmlとして処理されます。
中の処理が終了したらhtmlとして出力させようとしています。

個人的に

なにこれ解説むっず。本当に合ってんのかこれ。
でも使ってみると結構便利なので使ってみてください。何かあればコメントお願いします><

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

Googleスプレッドシートの内容を読み込む

はじめに

こちらの記事は、

を使用してGoogleスプレッドシートの内容を読み込むサンプルです。

参考にした記事は、PHPでSheetsAPIを使用してGoogleスプレッドシートにデータを追記していくサンプル。です。

↑こちらの記事は、スプレッドシートの書き込みですが、この記事では読み込み方法を説明します。

ですので、上記の記事の

  • プロジェクトを作る。
  • スプレッドシート作成・共有設定
  • PHPからスプレッドシート追記 > 0. githubからcloneしてライブラリ導入
  • PHPからスプレッドシート追記 > 1. プロジェクト直下にダウンロードしたJsonファイルを設置

まで同じです!

Googleスプレッドシートの値を読み込む

サンプルソース

動作確認はしていないので、エラーになったらごめんなさい。

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

class GoogleSpreadSheetClass {
    protected $service;

    protected $serviceKeyJson = 'サービスアカウントキーが書いてあるファイル.json';

    function __construct() {
        putenv('GOOGLE_APPLICATION_CREDENTIALS=' . dirname(__FILE__) . '/' . $this->serviceKeyJson);

        $client = new Google_Client();
        $client->useApplicationDefaultCredentials();
        $client->addScope(Google_Service_Sheets::SPREADSHEETS);
        $client->setApplicationName('test');
        $this->service = new Google_Service_Sheets($client);
    }

    /**
     * @param string スプレッドシートID
     * @param string 最終列アルファベット
     * @param string 最終行番号
     * @return boolean 成功 or 失敗
     */
    function main($spreadSheetId, $lastColumnNumber, $lastRowNumber) {
        $lastRange = $lastColumnNumber . $lastRowNumber;

        try {
            $data = $this->service->spreadsheets_values->get($spreadSheetId, 'A1:' . $lastRange);
            var_dump($data->values);

        } catch (Exception $e) {
            return false;
        }

        return true;
    }

}

サンプルスプレッドシート

spread_sheet_sample.png

解説

spreadsheets_valuesの関数は、

  • 第一引数にスプレッドシートのID
  • 第二引数にスプレッドシートの範囲

を指定します。

スプレッドシートの範囲は、stringで「A1:D6」などと指定します。
開始から終了まで斜めで囲む感じですね。

返ってくる値

Array
(
    [0] => Array
        (
            [0] => メーカー名
            [1] => 弦の名前
            [2] => おすすめジャンル
            [3] => 価格
        )

    [1] => Array
        (
            [0] => Thomastik Infeld
            [1] => Spirocore Midium
            [2] => ジャズポップス
            [3] => 18,900+tax
        )

    [2] => Array
        (
            [0] => Thomastik Infeld
            [1] => Spirocore Light
            [2] => ジャズポップス
            [3] => 18,900+tax
        )

    [3] => Array
        (
            [0] => Pirastro
            [1] => Flexocor
            [2] => なんでも
            [3] => 28,980+tax
        )

    [4] => Array
        (
            [0] => Pirastro
            [1] => Original Flexocor
            [2] => オーケストラ
            [3] => 38,040+tax
        )

    [5] => Array
        (
            [0] => Pirastro
            [1] => The Jazzer
            [2] => ジャズ
            [3] => 28,440+tax
        )

)

これを作った経緯

あるデータを突っ込んで、WordPressで固定ページ作りたいということがあったので、スプレッドシートの一行ずつで固定ページを作成するような処理を書いたときにやったやつです。

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

Laravel で バリデーションエラー時にJSで動的に増やしたinputの値を保持する

やりたいこと

jsで下記のようなinputをボタンを押したら追加していくというような可変な入力欄のとき
バリデーションエラーが起きたら、行が増えた状態で、それぞれに値が保持されていてほしい

test.blade.php
<div>
    <input type="text" name="hoge[]">
</div>

やりかた

Laravel 6.6で確認しています

バリデーションエラーが起こった時 old('hoge.0')とやれば、一番初めの入力値がとれます。
つまり、indexを指定してあげればその値が、指定しなければ配列そのものが取れます。

old('hoge.0');
old('hoge.1');
// hoge
// hoge2
old('hoge');
// [0 => hoge, 1 => hoge2]

なので、blade側でエラー時にループしてあげれば増えた状態で
入力値を保持できます

test.blade.php
@empty(old('hoge'))
<div>
    <input type="text" name="hoge[]">
</div>
@else
@foreach(old('hoge') as $value)
<div>
    <input type="text" name="hoge[]" value="{{$value}}">
</div>
@endforeach
// ↑↓ どっちでも($loop->indexでも)
@foreach(old('hoge') as $key => $value)
<div>
    <input type="text" name="hoge[]" value="{{old('hoge.{$key}')}}">
</div>
@endforeach
@endempty

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

Laravelをデザインパターンで考察する ~ Builder Pattern ~

デザインパターン

デザインパターンは、いろんな文脈で使われるが、ここでのデザインパターンはGoFのデザインパターン。
その中で今回は、Builder Patternでの実装箇所を取り上げる。
Builder Patternは、デザインパターンの分類、生成・構成・振る舞いのうちの生成に分類される。
生成過程を抽象化したり、コンストラクタの引数が多いとか複雑なときに使うデザインパターン。

基本的な説明は以下のリンクに任せる

[デザインパターン]
https://www.techscore.com/tech/DesignPattern/index.html/

[Builderパターン | PHPデザインパターン]
https://www.ritolab.com/entry/125

Illuminate\Support\Managerクラス

Illuminate\Support\Managerクラスは、Laravelに用意されているBuilder Patternを簡単に利用するための抽象クラス。
インスタンス生成過程を隠蔽し、シンプルに再利用するために継承して使用することができる。

Session, Cache, Auth等で使用されていて、Authの認証方式やSessionのストレージをLaravelで初期状態で使用できるもの以外を使いたい場合は、自前ですべて作成するのではなく、Manageクラスに従って拡張すれば、Laravelの機能をできるだけ残したまま拡張できる。
また、configの変更等最小のステップでほぼコードを書くことなく、設定を変更してSessionを別プロジェクトでも再利用が簡単にできる。

なので機能拡張の際、Managerクラスを継承あるいは引用しているクラスをLaravelが使用している場合、これに乗っかって拡張するのが楽。
新規機能を作る時も、Managerクラスを使えば、Driverを使ったBuilderパターンを容易に使用できる。

最近はLaravelのManagerコンポーネント(AuthManager等)は、継承せずに実装されるように変更されている(処理自体は踏襲されている)。
現在継承して使用されているのはSessionManagerなのでSessionManagerを使用して説明する。

SessionManager

Sessionで使用する機能(Illuminate\Session\Store)を再利用しつつ、使用者は保存先のストレージをconfigを使用して簡単に変更し、Session用のオブジェクト(Illuminate\Session\Store)を生成できるようにというのが、SessionManagerの趣旨
Sessionのストレージの選択肢は、ほぼ確定しているのでそのストレージごとに生成過程を見やすく管理できる。

SessionServiceProviderh

Illuminate\Session\SessionServiceProvider
protected function registerSessionManager()
{
    $this->app->singleton('session', function ($app) {
        return new SessionManager($app);
    });
}

SessionServiceProviderでSessionManagerがコンテナに登録されている
SessionManagerは、あくまで生成過程を抽象化してくれているもので、最終的にはIlluminate\Session\Storeのインスタンスが帰ってくる。

SessionManager

Illuminate\Session\SessionManager
protected function callCustomCreator($driver) //たぶん独自にDriverを登録できるやつ
{
    return $this->buildSession(parent::callCustomCreator($driver));
}

// 既存のDriverメソッドの抜粋
protected function createFileDriver()
{
    return $this->createNativeDriver();
}

protected function createMemcachedDriver()
{
    return $this->createCacheBased('memcached');
}

protected function createDynamodbDriver()
{
    return $this->createCacheBased('dynamodb');
}

// ここで/config/session.phpのdriverをDriverとしてセットしている。
public function getDefaultDriver()
{
    return $this->app['config']['session.driver'];
}

生成過程で必要になる要素を、Driverとして登録する。
ここではSessionを保存するためのStorageをDriverとして登録しておく。
既にFile、memcached、Redis、Dynamoとかいくつか用意されている。
callCustomCreatorで独自のDriverを登録できるはず(試したことない)
各Driverメソッド内で生成過程の差異を吸収する。
Sessionに別のストレージを使いたい場合この仕組みを利用するとLaravelの仕様に乗っかれて便利。

また、configでSessionを設定できるように、$this->app['config']を使用してStoreの生成を抽象化している。

Manager

Illuminate\Support\Manager
protected function createDriver($driver)
{
    if (isset($this->customCreators[$driver])) {
        return $this->callCustomCreator($driver);
    } else {
        $method = 'create'.Str::studly($driver).'Driver';
        if (method_exists($this, $method)) {
            return $this->$method();
        }
    }
    throw new InvalidArgumentException("Driver [$driver] not supported.");
}

// これでdriverメソッドで生成したStoreのメソッドをSessionManagerから直接使用できる
// Session::get('hoge')て感じで(SessionはSessionManagerのファサード)
public function __call($method, $parameters)
{
    return $this->driver()->$method(...$parameters);
}

createDriverにはDefaultDriverが渡されて、
\$method = 'create'.Str::studly($driver).'Driver';
のところでDriverメソッドを確定している。

SessionManagerの効果

SessionManagerにより、SessionのストレージをDATABASEにしたい場合、下記を設定するだけで、SessionファサードでDatabaseストレージに対応したStoreインスタンスが取得できるようになる。

.env
SESSION_DRIVER=database
Artisanコマンド実行
php artisan session:table

php artisan migrate
Session処理
Session::all();

Session::get('hoge');

SMS用のManagerクラスを作ってみる

Smsは外部サービスに処理を投げるので、Driverとして使用するSmsサービスを定義しておく。
今回は、NexmoとTwilioのDriverメソッドを定義する。このメソッドからSms送信用のDriverクラスを定義しておく。なのでSmsManagerが返すインスタンスは、Driverクラス。

App\Components\Sms\SmsManager

SmsManager
<?php

namespace App\Components\Sms;

use Illuminate\Support\Manager;
use Nexmo\Client as NexmoClient; // NexmoのPHPクライアント
use Twilio\Rest\Client as TwilioClient; // TwilioののPHPクライアント
use App\Components\Sms\Drivers\NullDriver; // configに設定がない時用のDriver
use App\Components\Sms\Drivers\NexmoDriver;
use App\Components\Sms\Drivers\TwilioDriver;
use Nexmo\Client\Credentials\Basic as NexmoBasicCredentials; // Nexmoの認証に使うやつ

class SmsManager extends Manager
{
    public function channel($name = null)
    {
        return $this->driver($name);
    }

    public function createNexmoDriver()
    {
        return new NexmoDriver(
            $this->createNexmoClient(),
            $this->app['config']['sms.nexmo.from']
        );
    }

    public function createTwilioDriver()
    {
        return new TwilioDriver(
            $this->createTwilioClient(),
            $this->app['config']['sms.twilio.from']
        );
    }

    protected function createNexmoClient()
    {
        return new NexmoClient(
            new NexmoBasicCredentials(
                $this->app['config']['sms.nexmo.key'],
                $this->app['config']['sms.nexmo.secret']
            )
        );
    }

    protected function createTwilioClient()
    {
        return new TwilioClient(
            $this->app['config']['sms.twilio.key'],
            $this->app['config']['sms.twilio.secret']
        );
    }

    public function createNullDriver()
    {
        return new NullDriver;
    }

    public function getDefaultDriver()
    {
        return $this->app['config']['sms.default'] ?? 'null';
    }
}

createClientで、SMSのapiの初期化をして各DriverClassに渡す。

App\Components\Sms\Drivers\Driver

各外部サービスのDriverクラスの抽象クラス、共通の設定処理を抽象化しておく。

App\Components\Sms\Drivers\Driver
<?php

namespace App\Components\Sms\Drivers;

use Illuminate\Support\Arr;
use App\Components\Sms\Exceptions\SmsException;

abstract class Driver implements
{
    protected $recipient;

    protected $message;

    abstract public function send();

    public function to(string $recipient) // 宛先の設定
    {
        $this->recipient = $recipient;

        return $this; // メソッドチェーンしたいので$thisを返しておく。
    }

    public function content(string $message) // メッセージの設定
    {
        $this->message = $message;

        return $this;
    }
}

App\Components\Sms\Drivers\TwilioDriver

Twilio用のDriverクラス

App\Components\Sms\Drivers\TwilioDriver
<?php

namespace App\Components\Sms\Drivers;

use Twilio\Rest\Client as TwilioClient;

class TwilioDriver extends Driver
{
    /**
     * The Twilio client.
     * 
     * @var \Twilio\Rest\Client
    */
    protected $client;

    /**
     * The phone number this sms should be sent from.
     *
     * @var string
     */
    protected $from;

    /**
     * Create a new Twilio driver instance.
     *
     * @param  \Twilio\Rest\Client  $twilio
     * @param  string  $from
     * @return void
    */
    public function __construct(TwilioClient $twilio, $from)
    {
        $this->client = $twilio;
        $this->from = $from;
    }

    public function send() // 送信処理
    {
        return $this->client->messages->create(
            $this->recipient, [
                'from' => $this->from,
                'body' => trim($this->message)
            ]
        );
    }
}

使ってみる

SMS::channel('twilio')
    ->to($phoneNumber)
    ->content('Using twilio driver to send SMS')
    ->send();

channelメソッドでDriver(TwilioかNexmo)を指定して、各種設定をしてメッセージを送信できて便利。

参考サイト

https://www.slideshare.net/BobbyBouwmann/laravel-design-patterns-86729254
https://itnext.io/building-driver-based-components-in-laravel-5b390dc25bd9
https://github.com/orobogenius/building-driver-based-components

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

Dockerで入れたPostgreSQLへのPDO接続でエラーが出て悩んだ。

出たエラー

こんにちは、Docker初心者です。
なんならMySQLばっかりだったのでPostgreSQLも初です(汗)

タイトルのままです。
docker-compose.ymlで入れたPostgreSQLへのPDO接続をしようとしたら、
下のようなエラーが出てしまったンゴです。

error.
 SQLSTATE[08006] [7] could not translate host name "postgres" to address: Name or service not known 

ちなみに書いていたPDOはこんな感じです。

property.php
try {
    $pdo_connect = new PDO( 'pgsql:host=postgres; dbname={name};', '{user}', '{dbname}' );  
} catch(PDOException $e) {
    var_dump($e->getMessage());
}

解決

問題はdocker-compose.ymlとPDOの両方にありました。ymlファイルはこちら。
コメントアウトで間違ってる点書いてます。

docker-compose.yml
  postgres-db:
  #なんと、この postgres-db がHOST名になるらしい。知らなかった。PDOほhostを変える必要あり。
   image: postgres:8.4
   environment:
    POSTGRES_ROOT_PASSWORD: root
    POSTGRES_USER: user
    POSTGRES_PASSWORD: dbname
    POSTGRES_DATABASE: db #これ、本当は POSTGRES_DB: db と書かないといけない。
    TZ: Asia/Tokyo
   hostname: postgres #HOSTがおかしいのかと思ってあとから付け加えたもの。いらない。
   ports:
    - 6001:5432

修正後

property.php
try {
    $pdo_connect = new PDO( 'pgsql:host=postgres-db; dbname={name};', '{user}', '{dbname}' );   
} catch(PDOException $e) {
    var_dump($e->getMessage());
}
docker-compose.yml
  postgres-db:
   image: postgres:8.4
   environment:
    POSTGRES_ROOT_PASSWORD: root
    POSTGRES_USER: user
    POSTGRES_PASSWORD: dbname
    POSTGRES_DB: db
    TZ: Asia/Tokyo
   ports:
    - 6001:5432

接続完了!
パイセンに感謝です。

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

グラフ描画のためのデータベース操作

グラフ描画のためのデータベース操作

どうやってデータを間引くか

剰余系を使って間引く

データベースに記録したデータをグラフにしたい場合、あまりにもデータが多いので間引く必要がある。
そもそも、データベースはスキップという操作があまり得意ではない。
それでもテーブルのIDを単純に剰余系で間引くSQLは、こんな感じ。

SELECT id,`CreateAt`,`機器ID`,`温度` 
      FROM data_table 
      WHERE MOD(id,100)=0

これだと、複数の機器がデータベースに記録されていた時に、データの最初と最後が欠けてしまう機器が出てくる。
そこで機器ごとの連続した番号が必要と思い、データベースに記録する時に連番を振っておくseq列を追加。

SELECT id,`CreateAt`,`機器ID`,`温度`
      FROM data_table 
      WHERE `機器ID`='XXXX-XXXX' AND MOD(seq,100)=0

一応もれなく計算出来るが、機器毎にSQLを発行して読み出すので、機種が増える毎に計算時間が遅くなって行く。

データベースの得意なのは集計

ある時、別件で1時間毎にデータを集計する機能を追加して気づいた。
スキップするのではなく、1時間毎の平均を取るSQLを書いてみた。

SELECT DATE_FORMAT(`CreateAt`, '%Y-%m-%d %H:00:00') AS hour,
      `機種ID`,AVG(`温度`) AS tempr 
      FROM data_table 
      GROUP BY `機種ID`,hour 
      HAVING tempr 

別の書き方で、こう書ける事も分かった。

SELECT FROM_UNIXTIME(TRUNCATE(UNIX_TIMESTAMP(`CreateAt`)/3600,0)*3600) as sec,
      `機器ID`,AVG(`温度`) AS tempr
      FROM data_table 
      GROUP BY `機種ID`,sec 
      HAVING tempr

この書き方だと任意の秒数で集計ができるのに気づいた。
グラフの横軸は時間なので、横軸に表示する範囲の日時を描画点数で割って、あらかじめ描画点の間隔の秒数を求めておく。

$start = strtotime(最初のdatetime); 
$end   = strtotime(最後のdatetime); 
$diff  = $end - $start;
$secPerPoint = intval($diff/描画点数);
SELECT FROM_UNIXTIME(TRUNCATE(UNIX_TIMESTAMP(`CreateAt`)/{$secPerPoint},0)*{$secPerPoint}) as sec,
      `機器ID`,AVG(`温度`) AS tempr
      FROM data_table
      GROUP BY `機種ID`,sec 
      HAVING tempr

間引くよりも平均を取る。そうすればGROUP BYが使えるので、一度のSQLで機器単位の平均を求めることができる。プログラムも単純になる。
肝心のスピードだが、多少早くなったが、グラフに送るデータの量が多いところがボトルネックになってた。例えばX軸の時間とかは、全機種共通なので1つだけ送るとかデータ転送量減らす予定。

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