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

Laravel組み込みの同時実行制御

これはOPENLOGI Advent Calendar 2020の13日目の記事です。

はじめに

Laravelには、Redisを利用して、同じ処理を同時にいくつ実行できるかを制御するためのモジュールがあります。
弊社では、現在laravelの7.xを使っていますが、7.xの情報については、基本的には以下を参照いただければ理解できると思います。
https://readouble.com/laravel/7.x/ja/queues.html

基本的にはドキュメントを読めばだいたいのことを理解できるのですが、やや言葉足らずな部分があります。
今回は、それらのモジュールの同時実行制御が、実際にどのように動くのか、検証していきます。
また、この記事ではlaravel 7.xを前提としています。

説明対象

まず、どこから利用するかですが、Illuminate\Support\Facades\Redisというfacadeが用意されており、そこから利用できます。
また、今回解説する関数については、Illuminate\Redis\Connections\Connectionに関数の定義があります。

で、利用する関数としては、主に2つあります。
funnelthrottleです。
一つ一つ解説していきます。

funnel

funnelは、一度に実行できるジョブの数を制限するというものです。
実際の実装は以下のクラスに定義されています。

  • Illuminate\Redis\Limiters\ConcurrencyLimiter
  • Illuminate\Redis\Limiters\ConcurrencyLimiterBuilder

Redisあんまり詳しく無いのですが、redis操作する際にコマンドを打つのを、一連のまとめた処理として、luaスクリプトで実行できるようです。
RDBで言えば、ストアドプロシージャのようなものでしょうか?
php上にコードとして定義されているので、Redis側に登録済みの状態で実行されるのではなく、都度スクリプト自体をRedis上で実行しているっぽいので、厳密には違うでしょう。

具体的にはevalというコマンドを使っています。
ドキュメントは以下を見るとよさそうです。
https://redis.io/commands/eval

funnelで内部的に呼び出しているluaのscriptは以下の2つです。

lockScript

for index, value in pairs(redis.call('mget', unpack(KEYS))) do
    if not value then
        redis.call('set', KEYS[index], ARGV[3], "EX", ARGV[2])
        return ARGV[1]..index
    end
end

releaseScript

if redis.call('get', KEYS[1]) == ARGV[1]
then
    return redis.call('del', KEYS[1])
else
    return 0
end

と、書いたものの、Luaは大昔にProgramming in Luaを読んだことがある程度なので、まじでわかりません。
察するにlockScriptについては、特定のキーがなければ、特定の期限つきのレコードを登録し、登録した結果を返す。

releaseScriptについては、特定のキーに値があり、それが特定の値と一致するのであれば、それを消すという機能を持つようです。
詳しくは、コード読んでください。

上記のコードから推察するに、実行開始時にRedisにキーを登録し、それが登録されている状態では、同時に同じ処理が走らないように制御できる。
また実行が終わったら、キーを削除し、処理を実行可能な状態に戻すことができる。というもののようです。

では、それを実際に試してみたいと思います。

挙動

funnelはlimit,releaseAfter,blockという関数で、動きを制御することができるのですが、それぞれ見ていきたいと思います。
こんな感じのコードを使っていきます。

<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Redis\LimiterTimeoutException;
use Illuminate\Support\Facades\Redis;

class LaravelRedisTest implements ShouldQueue
{
    /** @var int */
    public $tries = 1;

    /** @var string */
    private $name;

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

    public function handle()
    {
        $redis = Redis::connection();
        $redis->funnel(self::class)
            ->limit(2)
            ->block(0)
            ->then(function () {
                sleep(3);
                logger("{$this->name} is success");
            }, function (LimiterTimeoutException $e) {
                logger("{$this->name} is failed");
            });
    }
}

limit

まずはlimitです。これは同時にいくつ実行できるかを指定します。
同時にblockも使っていますが、これはあとで説明します。

以下の処理をした際の期待値を先に書いておきます。
limitを指定することで、指定数までの処理は実行でき、それを超えると失敗することがわかります。

  1. 処理A(3秒)を実行 -> 成功
  2. 処理B(3秒)を実行 -> 成功
  3. 処理C(3秒)を実行 -> 失敗
  4. 処理Aを終了(3秒まつ)
  5. 処理D(3秒)を実行 -> 成功

実行スクリプト

function run()
{
    dispatch(new LaravelRedisTest('A'));
    dispatch(new LaravelRedisTest('B'));
    dispatch(new LaravelRedisTest('C'));
    sleep(3);
    dispatch(new LaravelRedisTest('D'));
}

実行部分

$redis = LaravelRedis::connection();
$redis->funnel(self::class)
    ->limit(2)
    ->block(0)
    ->then(function () {
        sleep(3);
        echo "{$this->name} is success" . PHP_EOL;
    }, function (LimiterTimeoutException $e) {
        echo "{$this->name} is failed" . PHP_EOL;
    });

結果

[2020-12-12 15:56:19] local.DEBUG: C is failed  
[2020-12-12 15:56:21] local.DEBUG: A is success  
[2020-12-12 15:56:21] local.DEBUG: B is success  
[2020-12-12 15:56:25] local.DEBUG: D is success  

releaseAfter

releaseAfterに値を指定すると、その時間が立つとロックを解除して、他の処理を実行できるようになるようです。
なので、長い処理が動く際は、気をつけて指定する必要がありそうです。
またdefaultでは60秒なので指定しなければ60秒立つと勝手にロックが解除されます。

手順と期待値です。
releaseAfterを指定することで、実行中であってもロックが解除されて、実行可能になっていることがわかります。

  1. 処理A(3秒)を実行
  2. 処理B(3秒)を実行
  3. 1秒待つ
  4. 処理C(3秒)を実行 -> 成功
function run()
{
    dispatch(new LaravelRedisTest('A'));
    dispatch(new LaravelRedisTest('B'));
    sleep(1);
    dispatch(new LaravelRedisTest('C'));
}
$redis = LaravelRedis::connection();
$redis->funnel(self::class)
    ->limit(2)
    ->releaseAfter(1)
    ->block(0)
    ->then(function () {
        sleep(3);
        echo "{$this->name} is success" . PHP_EOL;
    }, function (LimiterTimeoutException $e) {
        echo "{$this->name} is failed" . PHP_EOL;
    });
[2020-12-12 16:10:52] local.DEBUG: A is success  
[2020-12-12 16:10:52] local.DEBUG: B is success  
[2020-12-12 16:10:53] local.DEBUG: C is success  

block

blockは、ロックが取れない場合に、どれくらい待つかという感じです。
これがdefaultが3秒になっているので、何も指定しなければ、3秒待つ形になります。
limitやrelaseAfterの項でblockに0を指定していたのは、挙動が複雑になるためでした。

手順と期待値です。
blockを指定することで、ロックが解除されるのを待っているのがわかります。

  1. 処理A(3秒)を実行
  2. 処理B(3秒)を実行 -> 成功
  3. 処理C(3秒)を実行 -> 成功
function run()
{
    dispatch(new LaravelRedisTest('A'));
    dispatch(new LaravelRedisTest('B'));
    dispatch(new LaravelRedisTest('C'));
}
$redis = LaravelRedis::connection();
$redis->funnel(self::class)
    ->limit(2)
    ->block(3)
    ->then(function () {
        sleep(3);
        echo "{$this->name} is success" . PHP_EOL;
    }, function (LimiterTimeoutException $e) {
        echo "{$this->name} is failed" . PHP_EOL;
    });
[2020-12-12 16:16:03] local.DEBUG: A is success  
[2020-12-12 16:16:03] local.DEBUG: B is success  
[2020-12-12 16:16:06] local.DEBUG: C is success  

throttle

throttleはfunnelと違い、時間の概念が追加されます。
単位時間に、いくつの処理を同時に実行するか制御することができるようです。

実装は以下のクラスに存在します。

  • Illuminate\Redis\Limiters\DurationLimiter
  • Illuminate\Redis\Limiters\DurationLimiterBuilder

redis script

それではRedisに投げているLuaスクリプトを読んで見ましょう。

local function reset()
    redis.call('HMSET', KEYS[1], 'start', ARGV[2], 'end', ARGV[2] + ARGV[3], 'count', 1)
    return redis.call('EXPIRE', KEYS[1], ARGV[3] * 2)
end
if redis.call('EXISTS', KEYS[1]) == 0 then
    return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}
end
if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then
    return {
        tonumber(redis.call('HINCRBY', KEYS[1], 'count', 1)) <= tonumber(ARGV[4]),
        redis.call('HGET', KEYS[1], 'end'),
        ARGV[4] - redis.call('HGET', KEYS[1], 'count')
    }
end
return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1}

funnelは2つスクリプトがありましたが、throttleは1つだけです。
ARGVとか、KEYSとか変数が色々あってもうわからんですね。
それらの変数はphpのコードを読めば分かるのですが、それはコードをご自身で読んでください。

簡単に説明すると、上記のreset関数が、有効期限つきで、登録時と削除時と、同時実行数を1として登録しています。
特定のキーが存在するとき、または存在していても有効期限外であれば、reset関数でキーを登録する。
対象のキーが存在し、その有効期限内であれば、新しくキーを登録したり更新はしない。
返却する値は、新しく実行可能か、現在いくつの処理を並行で処理しようとしているか、また現在の有効期限です。

単位時間に実行する数を制限してくれそうな雰囲気がありますね。
ただ、任意の期間に対して同時に実行している数を制御する、という形ではなさそうです。
あくまで、特定のキーに対して実行しているものが無い際に、最初に実行されたタイミングから指定時間は、同時実行数を制限してくれる。
というものの用に感じます。

throttleの挙動

funnelと違い、指定する部分の違いではなく、時間が立つと自動的に再実行できるという部分から検証していこうと思います。

処理時間が短い場合

まずは、処理時間が3秒と、指定時間に対して短い、あるいは同等の場合の挙動を見ておきます。
これはわかりやすいはず。

以下、手順と期待値です。
3秒間の間に、2つ以上実行されれば、失敗するし、3秒立てば成功するのが確認できます。

  1. 処理A(3秒)を実行
  2. 処理B(3秒)を実行
  3. 処理C(3秒)を実行 -> 失敗
  4. 処理Aを終了(3秒まつ)
  5. 処理D(3秒)を実行 -> 成功
function run()
{
    dispatch(new LaravelRedisTest('A'));
    dispatch(new LaravelRedisTest('B'));
    dispatch(new LaravelRedisTest('C'));
    sleep(3);
    dispatch(new LaravelRedisTest('D'));
}
$redis = LaravelRedis::connection();
$redis->throttle(self::class)
    ->allow(2)
    ->every(3)
    ->block(0)
    ->then(function () {
        sleep(3);
        echo "{$this->name} is success" . PHP_EOL;
    }, function (LimiterTimeoutException $e) {
        echo "{$this->name} is failed" . PHP_EOL;
    });
[2020-12-12 16:32:56] local.DEBUG: C is failed  
[2020-12-12 16:32:59] local.DEBUG: A is success  
[2020-12-12 16:32:59] local.DEBUG: B is success  
[2020-12-12 16:33:01] local.DEBUG: D is success  

処理時間が長い場合

次は、指定時間に対して、処理の時間が長い場合の挙動を見てみます。

以下が手順と期待値です。
短い場合と結果は変わりませんが、最初の処理がおわっていなくても、指定時間が経過すれば、実行可能になっていることがわかります。

  1. 処理A(10秒)を実行
  2. 処理B(10秒)を実行
  3. 処理C(10秒)を実行 -> 失敗
  4. 3秒まつがAもBも終わっていない
  5. 処理D(10秒)を実行 -> 成功
function run()
{
    dispatch(new LaravelRedisTest('A'));
    dispatch(new LaravelRedisTest('B'));
    dispatch(new LaravelRedisTest('C'));
    sleep(3);
    dispatch(new LaravelRedisTest('D'));
}
$redis = LaravelRedis::connection();
$redis->throttle(self::class)
    ->allow(2)
    ->every(3)
    ->block(0)
    ->then(function () {
        sleep(10);
        echo "{$this->name} is success" . PHP_EOL;
    }, function (LimiterTimeoutException $e) {
        echo "{$this->name} is failed" . PHP_EOL;
    });
[2020-12-12 16:35:57] local.DEBUG: C is failed  
[2020-12-12 16:36:06] local.DEBUG: A is success  
[2020-12-12 16:36:06] local.DEBUG: B is success  
[2020-12-12 16:36:10] local.DEBUG: D is success  

まとめ

わたしが実際に仕事で使ったのはthrottleの方ですが、funnelと比べて仕掛けや挙動がわかりづらく迷いました。
よかったら使ってみてください。

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

Allowed memory size of 134217728 bytes exhausted (tried to allocate 32768 bytes)

原因などは他の記事が分かりやすいので説明しません。
https://deep-blog.jp/engineer/1887/
とか

ただ、今一度立ち止まって、、

呼んでいる中で同じように呼んでいないかみる必要ありそうです!

このメソッド呼ぶとこのエラー出るのなら、きっとそのメソッド内で変な処理している可能性あります。

変な箇所を探してください!

メモリー使用エンドレスでメモリ上限超えている場合とんでもないことになるかと、、

=================================

最初の記事の方にでてこなかったので書きました!

ぜひこのエラーを記事で紹介している方がいる場合、

上記内容も私みたいなおっちょこちょいを助けるべき追記お願いします!?‍♀️

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

jQueryとjQuery Mobileでcloneしてみた

はじめに

この記事の筆者はひよっこWebエンジニア(2年目)で、jQueryとJavaScriptはほぼ未経験です。
温かい目で見ていただけると幸いですm(_ _)m
コード例はPHPで動的に書いたものを説明しやすくするためにHTMLに書き直し、なおかつ読みやすいように調整したものですので、間違っているかもしれません。

背景

自社サービスにおいて、cloneを使う機会があったので、
また使う事になったときのためにまとめておくことにしました。

使った技術

  • PHP
  • JS
    • jQuery
    • jQuery Mobile
  • CSS
    • jQuery
    • jQuery Mobile

jQuery編

コード例

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="1">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').prop('disabled', true);
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').prop('disabled', false);

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1)
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 現要素の一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times);
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 項目の一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初の要素は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').prop('disabled', true);
        }
    });
</script>

で定義して、「追加」を押すと、

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="2">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="like_fruits_2">
        <th></th>
        <td>
            <select name="fruits[2]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').prop('disabled', true);
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').prop('disabled', false);

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1)
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times);
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').prop('disabled', true);
        }
    });
</script>

みたいな感じに展開されるように作った。
jQueryは初心者でも結構あっさり実装できた。

jQuery Mobile編

コード例

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="1">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <my-select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </my-select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link');

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換。my-selectを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1).replace(/my-select/g, 'select')
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times).trigger('create');
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 一番最初の要素しか無いときは何も処理をさせない
        if (creation_times === 1) {
            return;
        }

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
            return;
        }
    });
</script>

で定義して、「追加」を押すと、

<table>
    <tr id="like_fruits_1">
        <th>
            <label>好きなフルーツ</label>
            <input type="hidden" id="like_fruits_count" value="2">
        </th>
        <td>
            <select name="fruits[1]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
            <button type="button" id="add_like_fruits_button" class="button button-pill button-primary" style="width: 100px;"><i class="fa fa-plus-square"></i> 追加</button>
            <button type="button" id="delete_like_fruits_button" class="button button-pill button-caution" style="width: 100px;"><i class="fa fa-trash"></i> 削除</button>
        </td>
    </tr>

    <tr id="like_fruits_2">
        <th></th>
        <td>
            <select name="fruits[2]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </select>
        </td>
    </tr>

    <tr id="clone_like_fruits_template" style="display: none">
        <th></th>
        <td>
            <my-select name="fruits[xxx]" class="select">
                <option value="">--</option>
                <option value="apple">りんご</option>
                <option value="orange">オレンジ</option>
                <option value="water_melon">スイカ</option>
                <option value="melon">メロン</option>
            </my-select>
        </td>
    </tr>
</table>

<script type=text/javascript>
    $(document).ready(function() {
        // 削除ボタンを無効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
    });

    // 追加ボタンをクリックしたときの処理
    $(document).on('click', '#add_like_fruits_button', function() {
        // 追加ボタンが押下されたら削除ボタンを有効化する
        $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link');

        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 追加したあとの要素数を保存
        $('#like_fruits_count').val(creation_times + 1);

        // clone_like_fruits_templateのクローンを作成
        let copy = $('#clone_like_fruits_template').clone(true);

        // xxxを置換。my-selectを置換
        copy.html(function(i, oldHTML) {
            return oldHTML.replace(/xxx/g, creation_times+1).replace(/my-select/g, 'select')
        });

        // styleを外す。
        copy.prop('style', false);

        // 属性を付加する
        copy.attr('id', 'like_fruits_'+(creation_times+1));

        // 一番最後に格納
        copy.insertAfter('#like_fruits_'+creation_times).trigger('create');
    });

    // 削除ボタンをクリックしたときの処理
    $(document).on('click', '#delete_like_fruits_button', function() {
        // 今ある要素数を取得
        let creation_times = +$('#like_fruits_count').val();

        // 一番最初の要素しか無いときは何も処理をさせない
        if (creation_times === 1) {
            return;
        }

        // 削除した後の個数を保存する
        $('#like_fruits_count').val(creation_times - 1);

        // 一番最後の行を削除する
        $('#like_fruits_'+creation_times).remove();

        // 一番最初は消せないように削除ボタンを無効化する
        if (creation_times <= 2) {
            $('#delete_like_fruits_button').attr('class', 'button button-pill button-caution button-large ui-btn-inline ui-shadow ui-link disabled');
            return;
        }
    });
</script>

これでほぼjQueryと同じ感じに展開された。

名前は似ているが、実は結構違った「jQuery」と「jQuery Mobile」

「jQuery Mobile」は「Mobile」とつくだけだから、「jQuery」で書いたコードがそのまま動くでしょ。なんて軽く思っていたら違う部分が結構あった。
jQuery Mobileでは、cloneで追加されたセレクトボックスを選択しても、画面上に反映されなかった。
これの解消に2日くらい時間を要した。

理由は、DOMとしてレンダリングされたとき、すでに「jQuery Mobile」によってselectui-selectに変換されていたからだった。
今回は対処法としてDOMの段階ではmy-selectみたいな「jQuery Mobile」に勝手に変換されないようにして解決させた。

さすがに「Java」と「JavaScript」のように全く違う言語とまではいかないけど、それでも結構違った。

最後に、今回作成した成果物をご紹介

jQuery

追加ボタンを押す前
スクリーンショット 2020-12-12 20.38.32.png

追加ボタンを押した後
スクリーンショット 2020-12-12 20.38.49.png

jQuery Mobile

追加ボタンを押す前
スクリーンショット 2020-12-12 20.39.26.png

追加ボタンを押した後
スクリーンショット 2020-12-12 20.39.40.png

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

LINE Messaging APIとGoutteを使用して最新記事を送信してもらう

はじめに

2020年度XTechグループアドベントカレンダーの13日目を担当します、iXIT株式会社の21卒内定者、小長谷です!
LINE Messaging API と Goutte を使用したボットについて書きます。

作成したもの

私が普段閲覧しているTechableの最新記事を、Goutte(PHPのライブラリ)を用いてスクレイピングし、LINEで送信してもらうボットを作成しました。

Techable(テッカブル) -海外・国内のネットベンチャー系ニュースサイト

fabpot/goutte - Packagist

最新記事 と送信すると、その時の最新記事の一覧を返してくれます。

 

記事の詳細を見たい場合、 サイトへ をタップすることでサイトへ移動します。

 

実行時のページ画像

環境

macOS BigSur 11.0.1
PHP 7.3.22

テンプレートメッセージを送信するボットを作成する

LINE Developersのガイドに、サンプルのボットを作成するまでのチュートリアルがあるので、スムーズに始めることができました!
https://developers.line.biz/ja/docs/messaging-api/

カルーセルテンプレートの作成

送信するメッセージには様々なタイプがあります。私は今回カルーセルテンプレートを使用しました。
https://developers.line.biz/ja/docs/messaging-api/message-types/#carousel-template

カルーセルテンプレート

複数のカラムを表示するテンプレートです。カラムは横にスクロールして順番に表示できます。

私が作成したものは、それぞれのカラムに画像タイトルテキストアクションを配置しました。

1つのカラムだけ配置するコードを以下のように書きました。

<?php
require_once dirname(__FILE__) . '/vendor/autoload.php';

use LINE\LINEBot\Constant\HTTPHeader;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;
use LINE\LINEBot;
use LINE\LINEBot\TemplateActionBuilder\UriTemplateActionBuilder;
use LINE\LINEBot\MessageBuilder\TemplateBuilder\CarouselColumnTemplateBuilder;
use LINE\LINEBot\MessageBuilder\TemplateBuilder\CarouselTemplateBuilder;
use LINE\LINEBot\MessageBuilder\TemplateMessageBuilder;

$channel_access_token = 'xxxxx';
$channel_secret = 'xxxxx';

$httpClient = new CurlHTTPClient($channel_access_token);
$bot = new LINEBot($httpClient, ['channelSecret' => $channel_secret]);

$events = $bot->parseEventRequest(file_get_contents('php://input'), $_SERVER['HTTP_' . HTTPHeader::LINE_SIGNATURE]);
$event = $events[0];

$columns = [];

$action = new UriTemplateActionBuilder("サイトへ", "クリックしたとき、開かれるURI");
$column = new CarouselColumnTemplateBuilder("タイトル(40字以内)", "テキスト(画像、タイトルを指定する場合60字以内)", "画像URL", [$action]);

$columns[] = $column;

$carousel = new CarouselTemplateBuilder($columns);
$templateMessageBuilder = new TemplateMessageBuilder("メッセージのタイトル", $carousel);
$response = $bot->replyMessage($event->getReplyToken(), $templateMessageBuilder);

Goutteで記事をスクレイピングする

次に、カラムに必要な画像タイトルテキストアクション(に使用する、記事へのURL)を、Goutteを用いてTechableからスクレイピングしました。
1つの記事を取得するコードを以下のように書きました。

<?php
require_once dirname(__FILE__) . '/vendor/autoload.php';

$goutteClient = new Goutte\Client();
$crawler = $goutteClient->request("GET", "https://techable.jp/");

$articleUrl = $crawler->filter("#panel-whatsnew")->filter(".te-articles__list__item")->filter("a")->extract(["href"])[0];
echo "記事のURL: " . $articleUrl . PHP_EOL;

$title = $crawler->filter("#panel-whatsnew")->filter(".te-articles__list__item")->filter(".te-articles__list__item__content__title")->text();
echo "記事のタイトル: " . $title . PHP_EOL;

$text = $crawler->filter("#panel-whatsnew")->filter(".te-articles__list__item")->filter(".te-articles__list__item__content__summary")->text();
echo "記事のテキスト: " . $text . PHP_EOL;

$img = $crawler->filter("#panel-whatsnew")->filter(".te-articles__list__item")->filter(".te-articles__list__item__thumb__img")->extract(["style"])[0];
$img = getUrl($img);
echo "画像URL: " . $img. PHP_EOL;

/*
 * CSSプロパティで設定された背景画像のURLを取り出す
 * " background-image: url( https://xxx.png ); "
 *    ↓
 * " https://xxx.png "
 */
function getUrl($img)
{
    preg_match("/(https).*\.(png|jpg|jpeg)/i", $img, $match);
    return $match[0];
}
$ php index.php

記事のリンク: https://techable.jp/archives/143963
記事のタイトル: 楽天、UGVによるスーパーからの商品配送サービス実現に向け横須賀市で実証実験
記事のテキスト: 楽天株式会社と神奈川県横須賀市は、2019年に「西友 リヴィンよこすか店」から港湾緑地「うみかぜ公園1年前半には、同市...
画像URL: https://techable.jp/wp-content/uploads/2020/12/32156cdde08f9a24d1d321145576baed.png

上記コード実行時のページ画像

以上の処理を取得したい記事に対して行い、カラムを作成しました。

最後に

LINE Messaging API と Goutte を使用したボットについて書かせていただきました。
今後は人気記事を送信してくれる機能などを追加して、多機能なボットにしようと思います!

XTechグループのアドベントカレンダーはまだまだ続きます。お楽しみに!

以下の情報を参考にしました

Messaging APIリファレンス
line-bot-sdk-php
[PHP] Messaging APIを使ったLINEbotで色々試してみる
Packagist

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

Mcrypt で暗号化し OpenSSL で複合する

なぜ

古いバージョンの Laravel で実装され、放置気味だったシステムをバージョンアップしたら、 Mcrypt が使えなくて困った、という話です。


Mcrypt は長期間メンテナンスされておらず、 PHP 7.2 で削除されました。
これに変わるものが OpenSSL です。

稼働中システムで、 Mcrypt で暗号化したデータが大量にデータベースに眠っている、という状態ですが、 どうやら McryptOpenSSL では、暗号化の結果に差異があるようで困ってしまいました。


結論としては、互換性があるので OK です。

暗号化結果の比較

Mcrypt で暗号化

リプレイス前に動作していたものと同等のコードです。

// 暗号化するデータ
$plaintext = 'dummy_text';
// シークレットキー
$key = 'dummy_secret_key';

$ciphertext = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $plaintext, MCRYPT_MODE_ECB);
$ciphertext_base64 = base64_encode($ciphertext);
echo $ciphertext_base64; // dD2hL+qY2lG60wM7vQW0fQ==

OpenSSL で暗号化

リプレイス後のコードです。

// 暗号化するデータ
$plaintext = 'dummy_text';
// シークレットキー
$key = 'dummy_secret_key';
// 同等の暗号メソッド
$cipher = "aes-128-ecb";

$ciphertext = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA);
$ciphertext_base64 = base64_encode($ciphertext);
echo $ciphertext_base64; // srIFQhnoDAIgRJOefeES4Q==

うん、結果が同じではありませんね ?

復号する

すでに述べている通り、結果は違いますが復号は可能です。

Mcrypt で暗号化し、 Mcrypt で復号する

mcrypt_encrypt() で暗号化したものは、当然 mcrypt_decrypt() で復号ができます。

// 暗号化済み文字列
$ciphertext_base64 = 'dD2hL+qY2lG60wM7vQW0fQ==';
// シークレットキー
$key = 'dummy_secret_key';

$ciphertext_dec = base64_decode($ciphertext_base64);
$plaintext_dec = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $ciphertext_dec, MCRYPT_MODE_ECB);
echo $plaintext_dec; // dummy_text

Mcrypt で暗号化し、 OpenSSL で復号する

// 暗号化済み文字列
$ciphertext_base64 = 'dD2hL+qY2lG60wM7vQW0fQ==';
// シークレットキー
$key = 'dummy_secret_key';
// 同等の暗号メソッド
$cipher = "aes-128-ecb";

$ciphertext_dec = base64_decode($ciphertext_base64);
$plaintext_dec = openssl_decrypt($ciphertext_dec, $cipher, $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
echo $plaintext_dec; // dummy_text

はい、復号できました。
問題ないです。

もちろん、 OpenSSL で暗号化したものを、このコードで復号することができます。

なお、 OPENSSL_ZERO_PADDING をオプション指定しないと失敗します。


一瞬血の気が引いたのですが、互換性があって問題なくやり取りできるよ、ということのログでした。

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

[PHP]多次元配列から要素をフィルタリングする

PHPには、array_filter関数というものがあります。
配列に入っている要素を特定の条件でフィルタリングするための関数で、空の要素を排除したい時や偶数を取り出したい時などに使ったりします。

例えば以下のコードを実行した場合、2番目(空文字)、4番目(null)、6番目(false)の要素が削除され、値が入っているものだけを取り出すことができます。

$array = [
    'aaa',
    '',
    'bbb',
    null,
    'ccc',
    false,
];

var_dump(array_filter($array));

/* 実行結果
array(3) {
  [0]=>
  string(3) "aaa" 
  [2]=>
  string(3) "bbb" 
  [4]=>
  string(3) "ccc" 
}
*/

しかし、array_filter関数は多次元配列には対応していないので、
多次元配列に対応した関数を作りました。

/**
 * コールバック関数を使用して、配列の要素をフィルタリングする(多次元配列対応)
 *
 * @param array $array 処理する配列
 * @param callable $callback 使用するコールバック関数
 * @param bool $unset_empty_array 空の配列を削除するか否か
 * @return array $array フィルタリングされた配列
 */
function array_filter_recursive($array, $callback=null, $unset_empty_array=false)
{
    if (is_array($array)) {
        // コールバック関数が未指定の場合
        if (!$callback) {
            // コールバック関数を指定する
            $callback = function($value) {
                return !empty($value); 
            };
        }
        foreach (array_keys($array) as $key) {
            if (is_array($array[$key])) {
                // 多次元配列の場合は再帰処理を行う
                $array[$key] = array_filter_recursive($array[$key], $callback, $unset_empty_array);
                // $unset_empty_arrayがtrueかつ配列が空なら削除
                if ($unset_empty_array && empty($array[$key])) {
                    unset($array[$key]);
                }
            // 指定したコールバック関数をコールし、戻り値がfalseなら削除
            } else if (!call_user_func($callback, $array[$key])) {
                unset($array[$key]);
            }
        }
    }
    return $array;
}

ではさっそく、先ほど作った関数を使ってみたいと思います。

実行結果は以下の通りで、ちょっと歪な多次元配列でも空の要素が全て削除されることが確認できました。

$array = [
    [],
    [
        'aaa',
        '',
        'bbb',
    ],
    [
        [
            null,
            'ccc',
            'ddd',
        ],
        [],
        [
            [],
            [
                'eee',
                'fff',
                false,
            ],
        ],
    ],
];

var_dump(array_filter_recursive($array, 'strlen', true));

/* 実行結果
array(2) {
  [1]=>
  array(2) {
    [0]=>
    string(3) "aaa"
    [2]=>
    string(3) "bbb"
  }
  [2]=>
  array(2) {
    [0]=>
    array(2) {
      [1]=>
      string(3) "ccc"
      [2]=>
      string(3) "ddd"
    }
    [2]=>
    array(1) {
      [1]=>
      array(2) {
        [0]=>
        string(3) "eee"
        [1]=>
        string(3) "fff"
      }
    }
  }
}
*/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactとPHP 初めての連携

はじめに

今回はReactで簡単なフォームを作って、PHPの方で受け取るプログラムを書いてみました。Laravelではやったことがあるのですが、フレームワーク無しのPHPでやったことがなくて、フレームワーク無しのPHPをだいぶ忘れてしまったのでやってみました。

テキストを入力して送信したら、そのテキストが表示されて、何も入力しなければエラーが表示される感じのシンプルなフォームです。
image.pngimage.png

以下を参考にさせていただきました。
参考:Create a Contact Form With PHP and React in 3 Min

React側

package.json
{
  "name": "simple-form",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@material-ui/core": "^4.11.2",
    "@types/axios": "^0.14.0",
    "@types/node": "^14.14.12",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "axios": "^0.21.0",
    "react": "^16.11.0",
    "react-dom": "^16.11.0",
    "react-hook-form": "^6.13.0",
    "react-scripts": "^4.0.1",
    "styled-components": "^5.2.1",
    "ts-node": "^9.1.1",
    "typescript": "^4.1.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

↑使用したpackageです。react-hook-formとmaterial-uiを使用していますが、これらの説明は割愛します。


↓react-hook-formの記事も書いているので良ければ是非!
react-hook-formの使い方を解説!V6.12.0追加

index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

index.tsxでApp.tsxからインポートしています。見慣れた形だと思います。

import React, { useState } from "react";
import axios from "axios";
import { Button, CircularProgress, styled, TextField } from "@material-ui/core";
import { useForm } from "react-hook-form";

type FormData = {
  text: string;
};

export const App: React.FC = () => {
  const [message, setMessage] = useState("");
  const [error, setError] = useState(false);

  const {
    register,
    handleSubmit,
    formState: { isSubmitting },
  } = useForm<FormData>();

  const onSubmit = async (data: FormData) => {
    try {
      const res = await axios.post("http://localhost:8080/index.php", data);
      setError(res.data.error);
      setMessage(res.data.message);
    } catch {
      setError(true);
      setMessage("通信に失敗しました。");
    }
  };

  return (
    <>
      <Form onSubmit={handleSubmit(onSubmit)}>
        <TextField
          defaultValue=""
          margin="normal"
          variant="outlined"
          name="text"
          error={error}
          inputRef={register}
          helperText={message}
        />
        <Button
          type="submit"
          variant="contained"
          color="primary"
          disabled={isSubmitting}>
          {isSubmitting ? <CircularProgress size={24} /> : "送信"}
        </Button>
      </Form>
    </>
  );
};

const Form = styled("form")({
  display: "flex",
  flexDirection: "column",
  width: 300,
  margin: "0 auto",
});

axiosでhttp://localhost:8080/index.phpにフォームの値を送信し、レスポンスとして、errorとmessageが入った連想配列を受け取ります。PHPの方のコードを見ればイメージがしやすいと思います。

PHP側

PHPはDockerでnginxを使い、http://localhost:8080で立ち上げました。MAMPとかを使っても簡単にできると思います。

index.php
<?php
header("Access-Control-Allow-Origin: *");
header('Access-Control-Allow-Headers: Content-Type');
$rest_json = file_get_contents("php://input"); // JSONでPOSTされたデータを取り出す
$_POST = json_decode($rest_json, true); // JSON文字列をデコード

if(empty($_POST['text'])) {
    echo json_encode(
        [
           "error" => true,
           "message" => "Error: 入力してください。",
        ]
    ); 
} else {
    echo json_encode(
        [
           "error" => false,
           "message" => 'Success: 入力されたテキスト→'.$_POST['text'],
        ]
    ); 
}
  • Access-Control-Allow-Originで異なるオリジンからのアクセスを可能に
  • Access-Control-Allow-Headersで使用可能なHTTPヘッダーを設定
  • file_get_contentsでJSONでPOSTされたデータを取り出す
  • POSTされたデータがなければ、errorがtrueでエラーメッセージを返し、POSTされたデータがあれば、errorがfalseでPOSTされたデータを返す感じです

終わりに

ここまで読んでいただきありがとうございます!細かい説明があまりできていませんが、初めてフロントエンドとサーバーサイドで連携する時のサンプルとしてみていただければと思います。次はデータベースを使って簡単なCRUDをできるようにしたいなと思います。文章力あまりないのでわかりにくいかもしれませんが、少しずつ続けながら上げていきたいと思います。

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

【初心者】PHPでの配列の並び替え

配列の並び替えについて

今日は配列の並び替え(ソート)について覚えておきたい。
結論としては…

sort(配列名);

だった。
これを利用すると、アルファベット順、数字は小さい順に並び替えが可能だった。
平仮名の場合も五十音順になるようだ。

以前、バブルソートのコードの書き方を覚えておかなければ。と思っていたのだけれど…

きっとどこかで役に立つかもしれない。

今ある知識でもなんとかできるのか考えることは面白い。
それに加えて色々なことを知ることで、できることの幅が広がっていくことは本当に面白い。

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

自社サービスのインタラクションデザインを改善するためにPHPで非同期処理をしてみた

はじめまして、こんにちは。
今年の10月に福岡で起業して、なんちゃってCTOをしている赤と黒が好きな若造です。
今回は開発中の自社サービスのインタラクションデザインを改善するためにPHPで非同期処理をしてみたという話を書きます。

PHPで処理を並行したい。

ユーザーがあるアクションを起こした時に、PHPでの処理に10秒掛かるとします。
その間ユーザーは10秒待つ羽目になるわけですが、この時間が貴重な現代社会に置いて、ユーザーは10秒待つと当然イライラします。
image.png
なのでなるべく早くリクエストからレスポンスの時間を短くしたいわけですが、いくらPHP8で爆速になったとは言え、限界もあります。
そこで「重要な処理」と「重要では無い処理」で処理自体を分割して
「重要な処理」が終わった時点でレスポンスを返し、「重要では無い処理」は裏で並列で処理するアイディアを思いつきました。
image.png
これなら実質3秒でユーザーにレスポンスを返す事ができます!

PHPはシングルスレッドだよ

ですが残念なことにPHPは基本的にシングルスレッドで、並列処理は出来ません。1
だから無理です。
今日のアドベントカレンダー記事は以上です。あざした。

image.png
 

 

…というわけにはいきません。
PHPはシングルスレッドですが、マルチプロセスは可能です。
「並列処理」ではなくて「分割処理」という言葉のほうがイメージしやすいですね
image.png
シングルスレッドとマルチプロセスは結果は同じ様に見えますが、プロセスが分割されているので、変数などの情報を共有できないのは注意しなくてはいけません。
(正確にはメモリが共有出来ない)

ではどうやってプロセスを分けるのか、を解説していきます。

exec関数

詳しい解説はPHPの公式のマニュアルに任せます。
PHPマニュアル:https://www.php.net/manual/ja/function.exec.php

PHP 7.4.10の環境で以下のコードを書いてみました。
exec関数で「返り値を/dev/nullに捨てて、バックグラウンド処理する、phpのプロセス」を立ち上げるコマンドを叩くコードです。

index.php
<?php
    echo "START";
    error_log("\n\n".date('Y-m-d H:i:s')."  START", 3, __DIR__.'/log.txt');

    exec('php '.__DIR__.'/num1_wait5.php > /dev/null &');
    exec('php '.__DIR__.'/num2_wait1.php > /dev/null &');
    exec('php '.__DIR__.'/num3_wait10.php > /dev/null &');
    exec('php '.__DIR__.'/num4_wait0.php > /dev/null &');

    error_log("\n".date('Y-m-d H:i:s')."  END", 3, __DIR__.'/log.txt');
    echo "END";
num1_wait5.php
<?php
    sleep(5);
    error_log("\n".date('Y-m-d H:i:s')."  NUM1_WAIT5", 3, __DIR__.'/log.txt');
num2_wait1.php
<?php
    sleep(1);
    error_log("\n".date('Y-m-d H:i:s')."  NUM2_WAIT1", 3, __DIR__.'/log.txt');
num3_wait10.php
<?php
    sleep(10);
    error_log("\n".date('Y-m-d H:i:s')."  NUM3_WAIT10", 3, __DIR__.'/log.txt');
num4_wait0.php
<?php
    error_log("\n".date('Y-m-d H:i:s')."  NUM4_WAIT0", 3, __DIR__.'/log.txt');

index.phpで4つのプロセスを立ち上げて処理するコードです。
実行結果は以下になります。

2020-12-12 10:28:00  START
2020-12-12 10:28:00  END
2020-12-12 10:28:00  NUM4_WAIT0
2020-12-12 10:28:01  NUM2_WAIT1
2020-12-12 10:28:05  NUM1_WAIT5
2020-12-12 10:28:10  NUM3_WAIT10

図にするとこんな感じです。
image.png

本来であれば全ての処理をシングルプロセスで処理すると完了に5+1+10+0=16秒掛かるところを
マルチプロセスにしたおかげで、全ての処理の完了に10秒で済んでしまいました。

しかしexec関数はlinuxのコマンドを叩く処理で、マルチプロセスのためのものではありません。
linuxのコマンドを叩くという性質上、うっかりユーザーの入力をそのままexec関数の引数に当てるなんてことは絶対にしないようにしましょう。

exec関数でのマルチプロセスの欠点

一見簡単にマルチプロセスで処理できるexec関数ですが、実は弱点があります。
処理を分割するためにプロセスを作るという性質上、処理が立て込んだ時には処理が横に広がりすぎて、サーバーに影響を及ぼすからです。
最悪サーバーが落ちると思います。
image.png

Laravelでの非同期処理

素のPHPでマルチプロセスをしてみましたが、実際の業務やサービスではフレームワークを使うことでしょう。
株式会社ナインステクノロジーズはLaravelが得意なので、Laravelでも非同期処理をやってみましょう。

Laravelではexec関数でのマルチプロセスとは少し違って「ジョブとキュー」で並列処理を実現します。

ジョブとキュー

厳密に言えば「ジョブとキュー」もマルチプロセス処理の一種で
「ジョブ」と言われる処理の塊を、「キュー」と呼ばれるプロセスが変わって処理するイメージです。

image.png
最初ジョブは全く無く、キューもジョブが無いので待機状態になっています。

image.png
なんらかのアクションでジョブが記録されると、キューはそれに気づきます。

image.png
キューは記録されているジョブを1つ取り出して処理を始めます。

こういう流れで処理を非同期的に処理します。

php artisan queue

詳しい解説はLaravelのドキュメントを翻訳しているサイトに任せます。
Laravel 6.x キュー:https://readouble.com/laravel/6.x/ja/queues.html

Laravel Framework 6.20.6の環境でジョブとキューの設定をやっていきます。
まずジョブをデータベースに記録したいので、migrationを行います。

php artisan queue:table
php artisan queue:failed-table    #初期インストール状態では既にmigrationファイルは存在しているのでエラーが出る。
php artisan migrate

これでjobsテーブルfailed_jobsテーブルが作られます。

そしてjobファイルを生成して、書いていきます。

php artisan make:job Num1Wait5.php
php artisan make:job Num2Wait1.php
php artisan make:job Num3Wait10.php
php artisan make:job Num4Wait0.php
/app/Jobs/Num1Wait5.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num1Wait5 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(5);
        error_log("\n".date('Y-m-d H:i:s')."  NUM1_WAIT5", 3, '/log.txt');
    }
}
/app/Jobs/Num2Wait1.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num2Wait1 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(1);
        error_log("\n".date('Y-m-d H:i:s')."  NUM2_WAIT1", 3, '/log.txt');
    }
}
/app/Jobs/Num3Wait10.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num3Wait10 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(10);
        error_log("\n".date('Y-m-d H:i:s')."  NUM3_WAIT10", 3, '/log.txt');
    }
}
/app/Jobs/Num4Wait0.php
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class Num4Wait0 implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        sleep(0);
        error_log("\n".date('Y-m-d H:i:s')."  NUM4_WAIT0", 3, '/log.txt');
    }
}

そしてジョブを入れるcontrollerファイルを生成して、書いていきます。

php artisan make:controller TestController
/app/Http/Controllers/TestController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TestController extends Controller
{
    public function test(){
        echo "START";
        error_log("\n\n".date('Y-m-d H:i:s')."  START", 3, '/log.txt');

        $this->dispatch(new \App\Jobs\Num1Wait5() );
        $this->dispatch(new \App\Jobs\Num2Wait1() );
        $this->dispatch(new \App\Jobs\Num3Wait10() );
        $this->dispatch(new \App\Jobs\Num4Wait0() );

        error_log("\n".date('Y-m-d H:i:s')."  END", 3, '/log.txt');
        echo "END";
    }
}

そして.envQUEUE_CONNECTIONを編集します。

.env
QUEUE_CONNECTION=database

最後にキューのプロセスを立ち上げます。

php artisan queue:work > /dev/null &

これで準備完了です。
実際にTestController@testを実行した結果が以下です。

2020-12-12 12:58:00  START
2020-12-12 12:58:00  END
2020-12-12 12:58:05  NUM1_WAIT5
2020-12-12 12:58:06  NUM2_WAIT1
2020-12-12 12:58:16  NUM3_WAIT10
2020-12-12 12:58:16  NUM4_WAIT0

…あれ?

php artisan queueでのジョブとキューでの欠点

先程の実行結果は、16秒掛かってしまいました。
exec関数では10秒だったのになぜでしょうか?

実は先程の実行は「キューが1個だったから」16秒掛かったのです。

image.png
キューが1個なので以下の流れで処理をします。

ジョブ1を処理して、
ジョブ1が終わったらジョブ2を処理して、
ジョブ2が終わったらジョブ3を処理して、
ジョブ3が終わったらジョブ4を処理して、
ジョブ4が終わる。

キューは1個なので、処理できるジョブも当然1個です。だから並列的に処理が出来ないんですね。
なので、並列的に処理したい場合はキューの数を増やすと良いでしょう。

image.png
キューを増やすことで同時に処理できるジョブの数が増え、効率的に処理をこなすことが出来ます。

キューの数だけジョブを捌けるのですが、ここにも落とし穴があって
例えばジョブの増加速度よりもキューがジョブを捌く速度が遅くなってしまうと
いつまで立っても処理が開始されないジョブが出てきてしまう可能性があります。
image.png

まさに炎上状態。
一応Laravelではジョブの優先度やキューへの振り分けを操作出来ますが、万が一こういう自体が起こらない訳ではありません。

まとめ

PHPはシングルスレッドで並列処理は難しいですが
exec関数やLaravelのqueueで非同期的に処理を実行できます。

活用することでユーザーへのレスポンスが早くなることでしょう。
本当はこの処理と合わせてプログレスバーを実装する予定でしたが、以外と長くなったので今度にします。

もし間違っているところがあればマサカリお願いします。(言葉尻を捉えてイジメるのはやめてくださぃ。

明日は奇しくも同じ福岡の企業に所属するせいけしろーさんです。

ココまで読んでくださってありがとうございました。
LGTMとTwitterをフォローしてくれると嬉しいです!よろしくおねがいします!!!
@9th_tech_Ryo

あと別のアドベントカレンダーになりますが、弊社長も記事を明日書くので、ぜひ見てみてください!!!!!
https://qiita.com/akira_9th


  1. ptreadsというモジュールを使えば可能らしい 

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

PHP Conference Japan 2020 スライドまとめ

PHP Conference Japan 2020 Re:born

YouTube Live

セッショントーク一覧

資料はTrack順に並べていきます。※資料公開され次第随時更新していきます。

前夜祭

Laravelの黒魔術 | localdisk

SPAのAPI開発の「やりづらさ」をDDDとオブジェクト指向の発想で解決する | 菱田裕美

Track1

PHPの今とこれから2020 | 廣川類

PHP WEBアプリケーション設計入門――10年先を見据えて作る | GMOインターネット | 成瀬 允宣

長期運用を目指す『Shadowverse』におけるリファクタ事例の紹介 〜テストの導入とメンバーへの普及法〜 | 株式会社Cygames | 髙野 祐輝

事業のスケールアウトを支えるPHPで作る分散アーキテクチャ | 竹澤有貴

パネルセッション「ひさてるさんに聞け」 | たなかひさてる, イアン・ブライソン, 小山哲志

ウェブセキュリティのありがちな誤解を解説する | 徳丸浩

Track2

NewRelicプラットフォームを使ったオブザーバビリティ入門 | BASE株式会社 | 川口将貴

レガシープロジェクトで、メタプログラミングを使ったPHPStan静的解析レベル上げ | 弁護士ドットコム株式会社 | 小宮山 太樹

徳丸皆伝を狙いませんか?徳丸実務試験とPHP8上級試験の解説 | PHP技術者認定機構 | 吉政忠志 ゲストスピーカー 徳丸浩先生

自分のやりたいことやって超簡単にチームのコミュニケーションを活性化させた | Hamee株式会社 | あすみ

PSRで学ぶHTTP Webアプリケーションの実践 | うさみけんた

Composer 2.0って何?どう変わるの?読んでみました! | きんじょうひでき

今こそ理解する、PHPの日時計算 | Sho Yamada

効果的な静的解析のCI導入パターンを求めて | 杉山 祐一

微妙な違いも見逃すな!ビジュアルリグレッションテスト! | 大橋佑太

Track3

初心者セッション | 柏岡秀男

GCPとPHP | サイトウ

PHP on Kubernetes | Kouta Ozaki

Webサービスをセキュアに保つために必要な視点 | ariaki

DNS改ざん検知ツールの実装とDNSパケットの世界 | 市川@cakepher

CakePHPで学ぶDIコンテナ | itosho

PhpStormを使えばほとんどコード補完されるんだってばさ | マキ

PHPで作るオンラインカンファレンス向け録画システム | 長谷川智希

Track4

Laravelで運用しているサービスをNuxt.jsにリプレイスする | 久保田賢二朗

LaravelDB.comを使ってDB設計「Migration生成」の基本操作を学ぶ | Daisuke Yamazaki

ゼロベースから Laravel を用いた API 実装オートメーション | めもり〜

Laravel × オニオンアーキテクチャで始めるテスト駆動開発 | 村田主磨

テストピラミッドを意識したテストコード実装戦略 | 02

本番でしか起きない問題に早く気が付けるように、僕は Laravel Dusk で CI する | sogaoh

Laravel + Lighthouseで始める低コストなGraphQL入門 | あきの/akkino

Track5

PHP 8 で作る JSON パーサ | 新原雅司

PHPのソースコードから理解するPreloadとJIT | 富所 亮

PHP8はISUCONへの扉を開く鍵となるか | 清家史郎

PHP 8 の新機能を PHP内部コードのテスト phpt から読む | 東口和暉

PHP8時代のWebアプリケーションフレームワークの話をしよう | 中榮健二

PHP 8 で Web 以外の世界の扉を叩く | sji

Track6

PHP 8.0: A new version, a new era | Gabriel Caruso

Service communication re:Born | Nick Chiu

How good are my tests? | Stephan Hochdoerfer

Functional Programming in PHP | Lochemem Michael

Lightning talk

玩具サブスクリプション・レンタルサービスの代表の子どもたちにPHPについて語ってもらった | 株式会社トラーナ | 志田典道

3分で分かるConnehito Tech Vision | コネヒト株式会社 | 伊藤 翔

Lenet の開発環境の紹介 | 株式会社ホワイトプラス | 古賀 敦士

福岡市のお墨付き!エンジニアフレンドリー企業に選ばれたFusicの実態に迫る! | 株式会社Fusic | 槇原 竜之輔

PHP8 on AWS lambda | 有限会社アリウープ | 柏岡秀男

  • None

エンジニアでもできる簡単親切エラーUI | もも

チームメンバーをエンパワーメントしよう!レガシープロジェクト改善事始め | ぬさし

静的解析から始める負債コード解消 | 藤岡大樹

レガシーシステムに自動テストを導入する第一歩 | 荒巻拓哉

PHPer のための Vim 実践入門 | 濱田 晃輔

8年運用しているCakePHPのECサイトをLaravelにリプレイスした一年後の話 | 近藤心平

Symfony公式の『日本語』入門書ができたよって話 | 角田 一平

再コンパイル不要! core dump さえ吐ければ gdb デバッグできます | 鈴木 智也

PHPerのためのCVEデータベースの紹介 | ナガノ

めざせブレークポイントマスター | つざき

PHPConのElePHPantができるまで | nauleyco

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

Laravel 5.5 Storageファサードで画像ファイルを保存

URLから取得した画像ファイルをStorageファサードを使い
storage/app/public/下に保存した時に、少し詰まったのでメモ。

Storageファザードを使ってファイルを保存する方法

Storageファザードを使ってファイルを保存する方法は以下
公式リファレンスより抜粋
https://readouble.com/laravel/5.5/ja/filesystem.html

readouble.com/laravel/5.5/ja/filesystem.html
use Illuminate\Support\Facades\Storage;

Storage::put('file.jpg', $contents);

Storage::put()を使うと、第一引数にファイル名、第二引数に保存したいファイルを入れます。
そうすると通常はstorage/app/public/内に画像が保存されます。

でも、保存先フォルダも指定したい!
という場合はこちら

readouble.com/laravel/5.5/ja/filesystem.html
use Illuminate\Http\File;
use Illuminate\Support\Facades\Storage;

// 自動的に一意のIDがファイル名として指定される
Storage::putFile('photos', new File('/path/to/photo'));

// ファイル名を指定する
Storage::putFileAs('photos', new File('/path/to/photo'), 'photo.jpg');

引数はそれぞれ以下の通りです。
Storage::putFile(保存先, new File('ファイルのパス'));
Storage::putFileAs('保存先', new File('ファイルのパス'), ファイルの名前);

今回はファイル名も指定したい、保存先も指定したい、とのことで、
Storage::putFileAs()を使うことにしました。

詰まったところ。 ファイルのパスって?

Illuminate\Http\Fileのインスタンスの引数にファイルのパス指定しますが、
ファイルのパスの取り方が思いつかなくて少し考えました。

が、簡単にできました。

一時ファイルを使います。

//画像のURL
$url = 'https::example.com/img.jpg';
//URLからファイル名を取得 ここはお好きな方法でファイル名を決めてください。
$file_name = substr(strrchr($url,"/"),1);
//URLからファイル取得
$img_downloaded = file_get_contents($url);
//一時ファイル作成
$tmp = tmpfile();
//一時ファイルに画像を書き込み
fwrite($tmp, $img_downloaded);
//一時ファイルのパスを取得
$tmp_path = stream_get_meta_data($tmp)['uri'];
//storageに保存。
Storage::putFileAs('images', new File($tmp_path), $file_name);
//一時ファイル削除
fclose($file_handle);

ちゃんとstorage/app/public/images/下に保存できました!
publict直下に保存したい時は

Storage::putFileAs('', new File($tmp_path), $file_name);

フォルダの指定を空にすればOKです!

S3などのクラウドディスクにファイルを保存する場合もこの方法で大丈夫なのではないかとおもいます!

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

きっと正しくないレンタルサーバーの作り方 Vol.3 - サーバーをセットアップする

はじめに

前回の記事からだいぶ時間が経ちましたが、第2回めです(・∀・)。
今回は全体的なアーキテクトについて記述していこうと思います(・∀・)。

※注意!
この一連の記事で紹介するコードは動作の概念を説明するものでありセキュリティーなどは意識していません(・∀・)。

実際に運用するシステムなどに使用しないでください(・∀・)。
(そのまま使うひともいないと思いますが)

また、私も記事を書きながら開発をしていくので「後になってみたら最初の方の記事間違えてたー」なんて事は起きそうです(・∀・)。

ご了承ください(・∀・)。

目次

サーバー設定前のDNSまわり下準備

ドメインを取る

今回は お名前.com を使用します(・∀・)。
他のレジストラサービスでも良いと思います(・∀・)。

レンタルサーバーサービスの運用には独自ドメインが必要になりますので、適当に取得してみてください(・∀・)。
.xxx とか .xyz とかの適当なドメインなら初年度はけっこう安いです(・∀・)。

この記事では rentaserve.com という独自ドメインを取得したように記事を書きます(・∀・)。

ドメイン取得方法については割愛します(・∀・)。
(ちゃんと書いて欲しい人が居たらコメントください)

VPSを借りる

サーバーOSには Debian/GNU Linux 10 を採用します(・∀・)。
ホスティングサービスには [ConoHa VPS] を使用します(・∀・)。

Debian ベースの Raspberry Pi でもほぼ同等の手順を取れると思いますので、自宅サーバーでレンタルサーバーサービスなどにチャレンジしてみても面白いかも知れません(・∀・)。
(グローバルIPアドレスが必要になります)

VPS や Linux の基本的な手順については割愛します(・∀・)。
(ちゃんと書いて欲しい人が居たらコメントください)

私は以下の感じで VPS を作成しました(・∀・)。

image.png

独自ドメインでDNSサーバーを動かす下準備

ドメインをネームサーバーとして動作させる設定をお名前.com で行います(・∀・)。

取得したドメイン(今回の場合は rentaserve.com )をネームサーバー(権威DNSサーバー)として動作させるための設定です(・∀・)。

まず、作成した ConoHa VPS のグローバルIPアドレスが必要になります(・∀・)。
ConoHa の VPS 詳細画面から確認できます(・∀・)。

image.png

rentaserve.com の場合は 150.95.216.242 になりますね(・∀・)。

次に、お名前.con の NAVI 画面からDNS関連の設定を行っていきます(・∀・)。

rentaserve.com を選択した状態で設定画面の「ネームサーバー名としてホスト登録を行う」を選択します(・∀・)。

image.png

これは、取得した独自ドメインにアクセスした時にどのIPアドレスのDNSサーバーの情報を見に行くかの設定で、独自ドメインでDNSサーバーを立ち上げるのに必須の設定になります(・∀・)。

ここに

  • ns1.rentaserve.com
  • ns2.rentaserve.com

のふたつのDNSホストを作成します(・∀・)。

ホストには ConoHa VPS インスタンスのグローバルIPアドレスを設定してください(・∀・)。

image.png

※注意!

本来は ns1 と ns2 は別々のDNSサーバーを立ててそれぞれのIPアドレスを設定して障害耐性を高めます(・∀・)。
今回は全てを単一のサーバーで実現するためにインチキをしています(・∀・)。

最後に rentaserve.com のネームサーバーを ns1.rentaserve.com / ns2.rentaserve.com に設定します(・∀・)。

image.png

ここに最低ふたつのDNSサーバーを指定しないと行けないので、ns1 と ns2 を作成しました(・∀・)。

これでDNS関連の下準備が完了です(・∀・)。

サーバーの設定をしていく

下準備

まずはパッケージを更新します(・∀・)。
私は基本 root で作業してるので、気になるひとは sudo でも使ってください(・∀・)。

$ apt update
$ apt upgrade

ホームディレクトリのテンプレートを作成します(・∀・)。
アカウントが追加された時に自動生成されるモノをここに入れておきます(・∀・)。

mkdir -p /etc/skel/{Maildir,public_html,mysqld}
chmod -R 700 /etc/skel
chmod -R 755 /etc/skel/public_html

ファイヤーウォールとポート転送

とりあえず、必要そうなネットワーク関連のパッケージをインストールします(・∀・)。

apt install dnsutils whois iptables-persistent nmap

手順では使わないものもありますが、とりあえず入れちゃう感じ(・∀・)。
(最低限必要なのは iptables-persistent です)

さて、今回作るレンタルサーバーサービスでは

  • ActiveDirectory ドメインコントローラーとして Samba 4 AD のDNS
  • 外部向け権威DNSサーバーとしての PowerDNS

と、ふたつのDNSが動作します(・∀・)。
どちらもポート53を使用するため、そのまま動かすと片方が動作しません(・∀・)。

なので、今回は PowerDNS の方をポート 10053 で動作させ、グローバルからのアクセスが合った場合、ポート53 から ポート10053に転送します(・∀・)。
Samba 4 AD のDNS はグローバルからのアクセスは必要ないため、そのままローカルからのアクセスのみポート53で受け付ける感じにします(・∀・)。

では、実際にファイヤーウォールを設定していきます(・∀・)。

まずは基本

iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -j ACCEPT

意味は

  • 自分のサーバーからのアクセスは無条件に許可
  • 外部からの接続は確立されていれば許可
  • 内部から外部へのアクセスは無条件で許可

のような感じです(・∀・)。

次に、アクセスできるポートを指定します(・∀・)。

iptables -A INPUT -p tcp --dport 21 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 53 -j ACCEPT
iptables -A INPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 465 -j ACCEPT
iptables -A INPUT -p tcp --dport 993 -j ACCEPT
iptables -A INPUT -p tcp --dport 10053 -j ACCEPT
iptables -A INPUT -p udp --dport 10053 -j ACCEPT
iptables -A INPUT -p tcp --dport 30100:30500 -j ACCEPT

開けたポートは

  • FTP/FTPS
  • SSH
  • DNS(Samba 4 AD)
  • HTTP
  • HTTPS
  • SMTPS
  • IMAPS
  • DNS(PowerDNS)
  • FTP(パッシブ用のポート、FTPサーバー構築の際に説明します)

になります(・∀・)。

次に、転送設定を行います(・∀・)。

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 53 -j REDIRECT --to-port 10053
iptables -t nat -A PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-port 10053

これは
「ネットワークカード eth0 のポート 53 にアクセスしてきたものを自分自信のポート 10053 に転送する」
という意味になります(・∀・)。

ネットワークカードの eth0 はグローバル(インターネット側)のネットワークカードになるので、そこからのDNSアクセスを PowerDNS に転送する設定になります(・∀・)。

自分自身から、また今回は使用しませんが複数サーバー構成にした歳のローカルのネットワークカードにはこの設定は反映されません(・∀・)。
なので、自分自身やローカルネットからのDNS(ポート53)アクセスは Samba 4 AD のDNSアクセスになります(・∀・)。

設定を確認する場合は

iptables -L

を打つと分かります(・∀・)。

Debian/GNU Linux 10 は初期状態ではIPパケットの転送が許可されていませんので、それを許可する設定にします(・∀・)。

echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf

/etc/sysctl.conf を vi などで開いて、末尾に net.ipv4.ip_forward = 1 を記述していただいても構いません(・∀・)。

最後に、上記の設定を保存して反映します(・∀・)。

netfilter-persistent save
netfilter-persistent reload
systemctl enable netfilter-persistent

クオータ対応

今回作成する共有型のレンタルサーバーは、ひとつのサーバーを何人かで共有します(・∀・)。
その時、無制限にファイルをアップロードされてしまうとサーバーのストレージ容量がパンクしてしまいます(・∀・)。

なので

  • このアカウントは合計1GBまで
  • あのアカウントは合計5GBまで

のような制御が必要になります(・∀・)。
そのために quota というものを使用します(・∀・)。

まずはインストール(・∀・)。

apt install quotatool

次に、ちょっと小細工(・∀・)。

今回、/home 以下に quota を設定するのですが、基本的に quota は ストレージ(パーティション)単位 で設定します(・∀・)。
しかし、ConoHa の場合、/ 以下がひとつストレージになっていて / 直下と /home が別れていないため、このままでは使用できません(・∀・)。

そのため、/root/home.img を言う 80GBくらいあるでっかいファイル を作成、そのファイルを まるでストレージ(HDD や SSD)のように見せかけて /home にマウントする という手法を取ります(・∀・)。

まずは 80GB の巨大なファイル(/root/home.img)を作成します(・∀・)。

dd if=/dev/zero of=/root/home.img bs=80MiB count=1KiB

ホスティングサービスに負荷がかかりそうなので、あんまり多用すると怒られるかも(・∀・)?

次にループデバイスに巨大なファイルを指定、普通のストレージのようにフォーマットします(・∀・)。

losetup /dev/loop0 /root/home.img
mkfs.ext4 /dev/loop0

再起動時に自動的に /root/home.img を /home にマウントするように設定します(・∀・)。

echo "/root/home.img /home ext4 defaults,loop,usrquota 1 2" >> /etc/fstab

/etc/fstab を vi で開いて手で追記しても構いません(・∀・)。

最後にマウントしてチェックします(・∀・)

mount -a

quotacheck -vaug

この方法は Raspberry Pi のようにパーティション構造を自由にできない環境でも有効ですね(・∀・)。

自宅サーバーで行う場合、/home はパーティションかストレージ自体を分けたほうが良いでしょう(・∀・)。
(その場合、この小細工は不要です)

この辺でいっしょ再起動でもしておきますか(・∀・)

気になるひとはホスト名に rentaserve 以外の名前をつけておくのも良いかもですね(・∀・)。

reboot

Active Directory ドメインコントローラーの構築

概説

rentaserve では、Linux のアカウント管理は Active Directory という機能に任せます(・∀・)。

Active Directory について説明しだすとそれだけで記事がかけてしまいますが、すっごく簡単に言うと

  • LDAPというデータベースを使用して、ここにユーザーデータを書き込むと勝手にアカウントが作成される
  • データベースを連携させれば複数マシンでアカウント情報を共有できる

という素敵システムです(・∀・)。

これは、レンタルサーバーが複数台に分かれた時でもサーバー間で Linux アカウントを共有したいなーと思って導入しました(・∀・)。
巷のレンタルサーバーでこのようなアカウント管理はあまり見ない気もする(・∀・)?
(しらんけど)

インストールと設定

とりあえず必要なものをインストール(・∀・)。
Debian/GNU Linux 10 は何でも標準的にパッケージがあって楽ですね(・∀・)w

apt install samba krb5-config winbind smbclient libpam-winbind libnss-winbind krb5-config resolvconf

インストール中、なにか訊かれても適当に進めて大丈夫です(・∀・)。
この設定は一度消して書き直しますので(・∀・)。

次に、Active Directory 構築のために、既存の標準動作を止めます(・∀・)。

systemctl stop smbd nmbd winbind
systemctl disable smbd nmbd winbind

Active Directory を構築します(・∀・)。

rm /etc/samba/smb.conf
samba-tool domain provision --use-rfc2307 --interactive
  • Realm の rentaserve.com(構築したいレンタルサーバーの独自ドメイン)
  • Administrator のパスワード

以外の項目は空エンターで問題ありません(・∀・)。

Administrator は Windows の用語で、Linux で言う root に相当する管理者アカウントです(・∀・)。

この Administrator のパスワードですが、「Active Directory パスワード複雑性ポリシー」というものを満たしていないとエラーになります(・∀・)。
具体的には、半角英数字記号全てを含むパスワードでないと弾かれますので、設定時は注意してください(・∀・)。

構築が終わったら設定ファイルのバックアップを取っておきましょう(・∀・)。

cp /etc/samba/smb.conf /etc/samba/smb.conf.bak

パスワード複雑性

既存の動作では、これから作成するアカウントは全て複雑なパスワードを要求されます(・∀・)。
また、アカウントの有効期限が42日に設定されています(・∀・)。

これらの設定を変更していきます(・∀・)。

samba-tool domain passwordsettings set --complexity=off
samba-tool domain passwordsettings set --min-pwd-length=8
samba-tool domain passwordsettings set --min-pwd-age=0
samba-tool domain passwordsettings set --max-pwd-age=0

設定の意味は

  • パスワードの複雑性ポリシーを無効にする(例えば pass1234 とかでも設定できる)
  • パスワードの最短文字数を8文字にする
  • ユーザーの有効期限を無限にする

となります(・∀・)。

続いて、Linux のログインを Samba 4 AD で行えるように設定します

vi /etc/nsswitch.conf

以下のふたつの後ろに winbind を追記します(・∀・)。

  • passwd: files systemd winbind
  • group: files systemd winbind

DNSアクセス先を自分自身にする(・∀・)。

vi /etc/network/interfaces

eth0 の設定の末尾に

  • dns-nameservers 127.0.0.1

を記述する(・∀・)。

ネットワークを再起動して Active Directory のユーザーが確認できればオッケーです(・∀・)・

systemctl restart networking

wbinfo -u

最後に自動起動の設定をして完了です(・∀・)。

systemctl unmask samba-ad-dc
systemctl restart samba-ad-dc
systemctl enable samba-ad-dc

mariadb

インストール

ざっくり mariadb をインストールします(・∀・)。
基本的にデフォルトで問題ないと思います(・∀・)。

apt install mariadb-server

systemctl restart mariadb
systemctl enable mariadb

mysql_secure_installation

rentaserve.com ようのデータベースを作成

mysql -u root -p

以下のSQLを実行します(・∀・)。

CREATE USER 'rentaserve'@'localhost' IDENTIFIED BY 'ここにパスワードを入れる';
CREATE DATABASE IF NOT EXISTS `rentaserve`;
GRANT ALL PRIVILEGES ON `rentaserve`.* TO 'rentaserve'@'localhost';
QUIT;

ウェブサーバーをインストール

Apache2 と PHP7.3

インストールして設定します(・∀・)。

apt install apache2 sqlite3 php7.3 php7.3-sqlite3 php7.3-pdo php7.3-fpm php7.3-mysql php7.3-zip php7.3-gd php7.3-mbstring php7.3-curl php7.3-xml php7.3-bcmath php7.3-ldap php-xdebug php-pear php-dev composer

a2enmod proxy_fcgi
a2enmod userdir
a2enmod rewrite
a2enmod ssl
a2enconf php7.3-fpm

標準のページを削除して作り直す(・∀・)。

a2dissite default-ssl
a2dissite 000-default

vi /etc/apache2/sites-available/ZZZ-default.conf

ZZZ-default.conf の内容は以下の感じです(・∀・)。

<VirtualHost *:80>
    ServerName rentaserve.com
    ServerAdmin mao.lembryo@gmail.com
    DocumentRoot /var/www/html/public

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

    <Directory /var/www/html>
        Options FollowSymLinks
        AllowOverride All
    </Directory>

    <FilesMatch "\.(php)$">
        SetHandler "proxy:unix:/run/php/php7.3-fpm.sock|fcgi://localhost"
    </FilesMatch>
</VirtualHost>

php-fpm の設定を修正(・∀・)。
コネクションタイムを Apache2 の設定に合わせておかないと、サーバー再起動時に PHP が停止してエラーになってします(・∀・)。

vi /etc/php/7.3/fpm/php-fpm.conf

以下の process_control_timeout = 0 がコメントアウトされているので、コメントを削除して

  • process_control_timeout = 300

とします(・∀・)。

不要になったファイルを削除してサーバーを再起動(・∀・)。

rm /etc/apache2/sites-available/default-ssl.conf
rm /etc/apache2/sites-available/000-default.conf
a2ensite ZZZ-default

systemctl restart php7.3-fpm apache2

DNSサーバーを構築する

インストール他

まずは PowerDNS をインストール(・∀・)。

apt install pdns-server pdns-backend-mysql

systemctl stop pdns

DNSサーバーの設定を行う(・∀・)。

vi /etc/powerdns/pdns.conf

以下の追記(270行目付近)

  • local-port=10053
  • launch=gmysql
  • gmysql-host=127.0.0.1
  • gmysql-user=rentaserve
  • gmysql-password=データベースに設定したパスワード
  • gmysql-dbname=rentaserve

意味は

  • DNSサーバーのポートを 53 ではなく 10053 で動作させる
  • バックエンドに MySQL(mariadb)を使用およびそのアクセス情報の設定

になります(・∀・)。

設定を反映(・∀・)。

systemctl restart pdns
systemctl enable pdns

バックエンド mariadb にテーブルを作成(・∀・)。

PowerDNS バックエンドの mariadb には以下のテーブルが必要です(・∀・)。
ここは 良く分からんけどそういうもの と思って、以下のテーブルを作成しましょう(・∀・)。

既に Apache2 と PHP7.3 が動作しているので phpMyAdmin などから行っても良いでしょう(・∀・)。

SQL直打ちもアレなので、Laravel プロジェクトにマイグレーションファイルとシーダーを用意指定あります(・∀・)。
GitHub

CREATE TABLE `comments`(
    `id` INT(11) NOT NULL,
    `domain_id` INT(11) NOT NULL,
    `name` VARCHAR(255) NOT NULL,
    `type` VARCHAR(10) NOT NULL,
    `modified_at` INT(11) NOT NULL,
    `account` VARCHAR(40) NOT NULL,
    `comment` TEXT NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
;
CREATE TABLE `cryptokeys`(
    `id` INT(11) NOT NULL,
    `domain_id` INT(11) NOT NULL,
    `flags` INT(11) NOT NULL,
    `active` TINYINT(1) DEFAULT NULL,
    `content` TEXT DEFAULT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
;
CREATE TABLE `domainmetadata`(
    `id` INT(11) NOT NULL,
    `domain_id` INT(11) NOT NULL,
    `kind` VARCHAR(32) DEFAULT NULL,
    `content` TEXT DEFAULT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
;
CREATE TABLE `domains`(
    `id` INT(11) NOT NULL,
    `name` VARCHAR(255) NOT NULL,
    `master` VARCHAR(128) DEFAULT NULL,
    `last_check` INT(11) DEFAULT NULL,
    `type` VARCHAR(6) NOT NULL,
    `notified_serial` INT(11) DEFAULT NULL,
    `account` VARCHAR(40) DEFAULT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
;
CREATE TABLE `records`(
    `id` INT(11) NOT NULL,
    `domain_id` INT(11) DEFAULT NULL,
    `name` VARCHAR(255) DEFAULT NULL,
    `type` VARCHAR(10) DEFAULT NULL,
    `content` TEXT DEFAULT NULL,
    `ttl` INT(11) DEFAULT NULL,
    `prio` INT(11) DEFAULT NULL,
    `change_date` INT(11) DEFAULT NULL,
    `disabled` TINYINT(1) DEFAULT 0,
    `ordername` VARCHAR(255) CHARACTER
SET
    utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
    `auth` TINYINT(1) DEFAULT 1
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
;
CREATE TABLE `supermasters`(
    `ip` VARCHAR(64) NOT NULL,
    `nameserver` VARCHAR(255) NOT NULL,
    `account` VARCHAR(40) NOT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
;
CREATE TABLE `tsigkeys`(
    `id` INT(11) NOT NULL,
    `name` VARCHAR(255) DEFAULT NULL,
    `algorithm` VARCHAR(50) DEFAULT NULL,
    `secret` VARCHAR(255) DEFAULT NULL
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4
;

ALTER TABLE `comments` ADD PRIMARY KEY(
    `id`
),
ADD KEY `comments_domain_id_idx`(
    `domain_id`
),
ADD KEY `comments_name_type_idx`(
    `name`,
    `type`
),
ADD KEY `comments_order_idx`(
    `domain_id`,
    `modified_at`
)
;
ALTER TABLE `cryptokeys` ADD PRIMARY KEY(
    `id`
),
ADD KEY `domainidindex`(
    `domain_id`
)
;
ALTER TABLE `domainmetadata` ADD PRIMARY KEY(
    `id`
),
ADD KEY `domainmetadata_idx`(
    `domain_id`,
    `kind`
)
;
ALTER TABLE `domains` ADD PRIMARY KEY(
    `id`
),
ADD UNIQUE KEY `name_index`(
    `name`
)
;
ALTER TABLE `records` ADD PRIMARY KEY(
    `id`
),
ADD KEY `nametype_index`(
    `name`,
    `type`
),
ADD KEY `domain_id`(
    `domain_id`
),
ADD KEY `recordorder`(
    `domain_id`,
    `ordername`
)
;
ALTER TABLE `supermasters` ADD PRIMARY KEY(
    `ip`,
    `nameserver`
)
;
ALTER TABLE `tsigkeys` ADD PRIMARY KEY(
    `id`
),
ADD UNIQUE KEY `namealgoindex`(
    `name`,
    `algorithm`
)
;
ALTER TABLE `comments` MODIFY `id` INT(
    11
) NOT NULL AUTO_INCREMENT
;
ALTER TABLE `cryptokeys` MODIFY `id` INT(
    11
) NOT NULL AUTO_INCREMENT
;
ALTER TABLE `domainmetadata` MODIFY `id` INT(
    11
) NOT NULL AUTO_INCREMENT
;
ALTER TABLE `domains` MODIFY `id` INT(
    11
) NOT NULL AUTO_INCREMENT
;
ALTER TABLE `records` MODIFY `id` INT(
    11
) NOT NULL AUTO_INCREMENT
;
ALTER TABLE `tsigkeys` MODIFY `id` INT(
    11
) NOT NULL AUTO_INCREMENT
;
ALTER TABLE `records` ADD CONSTRAINT `records_ibfk_1` FOREIGN KEY(
    `domain_id`
) REFERENCES `domains`(
    `id`
)
ON  DELETE CASCADE
;

ドメインとレコードを登録

ドメインとレコードを登録していきます(・∀・)。

まずはドメイン、コレはかんたんです(・∀・)。
ドメイン名と type に NATIVE を入れるだけです(・∀・)。

INSERT INTO `domains`(
    `name`,
    `type`
)
VALUES(
    'rentaserve.com',
    'NATIVE'
)
;

続いて rentaserve.com のDNSレコード(・∀・)。

  • レコードが所属するドメインIDを指定(domains の id)
  • レコード名
  • レコード形式(SOA / NS / A / AAAA / MX / TXT など)
  • レコードの内容

などを入れていきます(・∀・)。
まずは SOA と NS レコード、これが無いと nslookup などでエラーになります(・∀・)。

SOA のメールアドレスは連絡の取れる自分のメールアドレスを設定してください(・∀・)。

INSERT INTO `records`(
    `domain_id`,
    `name`,
    `type`,
    `content`,
    `ttl`
)
VALUES(
    1,
    'rentaserve.com',
    'SOA',
    'rentaserve.com mao.lembryo@gmail.com 1',
    '86400'
)
INSERT INTO `records`(
    1,
    `domain_id`,
    `name`,
    `type`,
    `content`
)
VALUES(
    '1',
    'rentaserve.com',
    'NS',
    'ns1.rentaserve.com',
    '86400'
)
;
INSERT INTO `records`(
    `domain_id`,
    `name`,
    `type`,
    `content`,
    `ttl`
)
VALUES(
    '1',
    'rentaserve.com',
    'NS',
    'ns2.rentaserve.com',
    '86400'
)
;

最後に逆引きIPアドレスの情報を入れていきます(・∀・)。

INSERT INTO `records`(
    `domain_id`,
    `name`,
    `type`,
    `content`,
    `ttl`
)
VALUES(
    '1',
    'rentaserve.com',
    'A',
    '150.95.216.242',
    '600'
)
;
INSERT INTO `records`(
    `domain_id`,
    `name`,
    `type`,
    `content`,
    `ttl`
)
VALUES(
    '1',
    'ns1.rentaserve.com',
    'A',
    '150.95.216.242',
    '600'
)
;
INSERT INTO `records`(
    `domain_id`,
    `name`,
    `type`,
    `content`,
    `ttl`
)
VALUES(
    '1',
    'ns2.rentaserve.com',
    'A',
    '150.95.216.242',
    '600'
)
;

これで、rentaserve.com / ns1.rentaserve.com / ns2.rentaserve.com にアクセスした時にIPアドレス 150.95.216.242 を返すようになります(・∀・)。

records に「mao.rentaserve.com」を入れればサブドメインにも出来ます(・∀・)。
(設定した Aレコードの content ないのIPアドレスを返すようになる)

レンタルサーバー利用者の独自ドメインを使用する場合も、同期の domains と records に相応のデータを INSERT するだけです(・∀・)。

最後に

必要なサーバー設定はある程度終わりましたので、次回以降はレンタルサーバーのシステム開発に入れたら良いなーと思っています(・∀・)。

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

WordPressテーマ開発で最低限必要なファイル は2つ!

はじめに

下記の方向けで書きます。

  • 取りあえずテーマを表示させたい
  • WordPressテーマ開発したい
  • php初心者

WordPressテーマ開発で最低限必要なファイル

では、
WordPressでテーマをアップロードしたとき最低限必要なファイル(構成)についてです。

今回、紹介するのは
WordPressの設定でテーマとして認識されるための構成です。

次のファイルを同じ階層にまとめて、アップロードすれば
とりあえずテーマをWordPressが認識して動くようになります。

index.php

トップページ用テンプレートになります。

中身は何でも可能!!
ひとまず、「Hello World」的な文言でも書いておきます。

HTMLのベタ書きでもOKです。

index.php
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link href="<?php echo get_template_directory_uri(); ?>/css/style.css" rel="stylesheet">
    <title>サイト名</title>
</head>
<body>
    <p>Hello World</p>
</body>
</html>

style.css

WordPressテーマ作成で
陥りがちなのが、下記のstyle.cssを用意していないことです。
これ忘れると、テーマはうごかないです。

style.css
/*
Theme Name: サイトorブログ名等々
*/

他に、記載しとくと良いものも合わせると
下のようになります。

style.css
/*
Theme Name: サイトorブログ名等々
Description: テーマの説明
Version: 1.0.0
Author:  テーマの作成者
*/

まとめ

以上です。
あとは上の2つのファイルを1つのファイルにまとめてWordPressにアップロードして確認してみて下さい。

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

MySQL PHPからデータ登録できずエラーになってしまう&ターミナルから文字を入力した際に削除した文字も表示されてしまう

環境

mac 11.0.1
docker-compose.ymlappコンテナ 
php:7.4-apachedbコンテナ mysql:5.5

事象1 ターミナルでプログラムの実行確認をしている際に、MySQLにデータが登録されない

ターミナル
[今日のメモ]
登録しました
string(54) "INSERT INTO memo (memo) VALUES (登登録しました)"
Error: データの追加に失敗しました
Debugging: Unknown column '登登録しました' in 'field list'

VALUESに出力されている文字がおかしいのは一旦置いておきます。

PHP
     echo '[今日のメモ]' . PHP_EOL;
        $memo = trim(fgets(STDIN));
        $sql = "INSERT INTO memo (memo) VALUES ($memo)"; 
        $result = mysqli_query($link, $sql);
        var_dump($sql);

memoにシングルクォーテーション追加
$sql = "INSERT INTO memo (memo) VALUES ('$memo')";
$memoをシングルクォーテーション''で囲まないと文字としてではなく、カラムで出力されてしまいエラーになる

事象2 ターミナルから文字を入力した際に削除した文字もテーブルに登録されてしまう

ターミナル
% docker-compose exec app php tmp/memo.php
Success: データベースに接続できました
[今日のメモ]
明日
string(9) "明日明"
登録が完了しました

実行確認の際に、BackSpaceで誤入力を削除し、上手く削除されてテーブルに、登録されている時もありましたが、上記のようになる事が多く、初めはINSERT文がおかしいのかと思ったのですが、最終的にターミナルでの操作にいきつきました。

var_dump(変数名)で出力すると消えていない文字を発見!!

コマンドのショートカットで削除する事にしました。

削除
コマンド 意味
Ctrl + h (BackSpace) カーソルの後方を1文字削除
Ctrl + d (Delete) カーソル上の1文字削除
Ctrl + w 単語1個文削除
Ctrl + d は何も入力していない状態で入力するとログアウトしてしまうので注意が必要。

カット,ヤンク
コマンド 意味
Ctrl + k 行末まで削除
Ctrl + u 行頭まで削除
Ctrl + y 最後に削除した内容を挿入
カットが切り取りで、ヤンクが貼り付けと覚えておけば大丈夫です。

私の現在の設定では、Ctrl + h (BackSpace)は使用できませんでした。

ターミナル
[今日のメモ]
明日
string(6) "明日"
登録が完了しました

SELECT * FROM memo;

MySQL:テーブル
| 71 | 明日明              | 2020-12-12 04:26:04 |
| 72 | 明日                | 2020-12-12 04:26:29 |
+----+--------------------+----

無事完了!

参考にさせていただきました!ありがとうございます!
爆速でターミナルを使うためのショートカット集

間違えている所や、ここもっと勉強するといいよ!などコメントいただけましたら、かなり有り難いのでよろしくお願いいたします笑

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

PHP(laravel)でUndefined property: stdClassのメッセージが出たとき

PHPのstdClassは簡単に言うと、プロパティやメソッドが無いオブジェクトです。

その特徴を活かして使われることもありますが、
意図せず「Undefined property: stdClass」のメッセージが表示された場合は
定義していないオブジェクトを使っている可能性があります。

//クラス
class Aaa {
 public $word;
}
//オブジェクト
$a = new Aaa;//定義
$a->word = 'こんにちは';
echo $a->word; //出力:こんにちは

//定義していない変数$bを使うと
$b->word = 'こんにちは';
echo $a->word; //出力:Undefined property: stdClass

私の場合はlaravelでの開発中に意図せず出てきたので、
使っている変数を確かめてみると、メッセージが解消できました。

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