- 投稿日:2020-12-12T19:18:27+09:00
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つあります。
funnelとthrottleです。
一つ一つ解説していきます。funnel
funnelは、一度に実行できるジョブの数を制限するというものです。
実際の実装は以下のクラスに定義されています。
Illuminate\Redis\Limiters\ConcurrencyLimiterIlluminate\Redis\Limiters\ConcurrencyLimiterBuilderRedisあんまり詳しく無いのですが、redis操作する際にコマンドを打つのを、一連のまとめた処理として、luaスクリプトで実行できるようです。
RDBで言えば、ストアドプロシージャのようなものでしょうか?
php上にコードとして定義されているので、Redis側に登録済みの状態で実行されるのではなく、都度スクリプト自体をRedis上で実行しているっぽいので、厳密には違うでしょう。具体的にはevalというコマンドを使っています。
ドキュメントは以下を見るとよさそうです。
https://redis.io/commands/evalfunnelで内部的に呼び出している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 endreleaseScript
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を指定することで、指定数までの処理は実行でき、それを超えると失敗することがわかります。
- 処理A(3秒)を実行 -> 成功
- 処理B(3秒)を実行 -> 成功
- 処理C(3秒)を実行 -> 失敗
- 処理Aを終了(3秒まつ)
- 処理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 successreleaseAfter
releaseAfterに値を指定すると、その時間が立つとロックを解除して、他の処理を実行できるようになるようです。
なので、長い処理が動く際は、気をつけて指定する必要がありそうです。
またdefaultでは60秒なので指定しなければ60秒立つと勝手にロックが解除されます。手順と期待値です。
releaseAfterを指定することで、実行中であってもロックが解除されて、実行可能になっていることがわかります。
- 処理A(3秒)を実行
- 処理B(3秒)を実行
- 1秒待つ
- 処理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 successblock
blockは、ロックが取れない場合に、どれくらい待つかという感じです。
これがdefaultが3秒になっているので、何も指定しなければ、3秒待つ形になります。
limitやrelaseAfterの項でblockに0を指定していたのは、挙動が複雑になるためでした。手順と期待値です。
blockを指定することで、ロックが解除されるのを待っているのがわかります。
- 処理A(3秒)を実行
- 処理B(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 successthrottle
throttleはfunnelと違い、時間の概念が追加されます。
単位時間に、いくつの処理を同時に実行するか制御することができるようです。実装は以下のクラスに存在します。
Illuminate\Redis\Limiters\DurationLimiterIlluminate\Redis\Limiters\DurationLimiterBuilderredis 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秒立てば成功するのが確認できます。
- 処理A(3秒)を実行
- 処理B(3秒)を実行
- 処理C(3秒)を実行 -> 失敗
- 処理Aを終了(3秒まつ)
- 処理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処理時間が長い場合
次は、指定時間に対して、処理の時間が長い場合の挙動を見てみます。
以下が手順と期待値です。
短い場合と結果は変わりませんが、最初の処理がおわっていなくても、指定時間が経過すれば、実行可能になっていることがわかります。
- 処理A(10秒)を実行
- 処理B(10秒)を実行
- 処理C(10秒)を実行 -> 失敗
- 3秒まつがAもBも終わっていない
- 処理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と比べて仕掛けや挙動がわかりづらく迷いました。
よかったら使ってみてください。
- 投稿日:2020-12-12T19:07:33+09:00
Allowed memory size of 134217728 bytes exhausted (tried to allocate 32768 bytes)
原因などは他の記事が分かりやすいので説明しません。
https://deep-blog.jp/engineer/1887/
とかただ、今一度立ち止まって、、
呼んでいる中で同じように呼んでいないかみる必要ありそうです!
このメソッド呼ぶとこのエラー出るのなら、きっとそのメソッド内で変な処理している可能性あります。
変な箇所を探してください!
メモリー使用エンドレスでメモリ上限超えている場合とんでもないことになるかと、、
=================================
最初の記事の方にでてこなかったので書きました!
ぜひこのエラーを記事で紹介している方がいる場合、
上記内容も私みたいなおっちょこちょいを助けるべき追記お願いします!?♀️
- 投稿日:2020-12-12T18:50:14+09:00
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」によって
selectがui-selectに変換されていたからだった。
今回は対処法としてDOMの段階ではmy-selectみたいな「jQuery Mobile」に勝手に変換されないようにして解決させた。さすがに「Java」と「JavaScript」のように全く違う言語とまではいかないけど、それでも結構違った。
最後に、今回作成した成果物をご紹介
jQuery
jQuery Mobile
- 投稿日:2020-12-12T18:39:05+09:00
LINE Messaging APIとGoutteを使用して最新記事を送信してもらう
はじめに
2020年度XTechグループアドベントカレンダーの13日目を担当します、iXIT株式会社の21卒内定者、小長谷です!
LINE Messaging API と Goutte を使用したボットについて書きます。作成したもの
私が普段閲覧しているTechableの最新記事を、Goutte(PHPのライブラリ)を用いてスクレイピングし、LINEで送信してもらうボットを作成しました。
最新記事 と送信すると、その時の最新記事の一覧を返してくれます。
記事の詳細を見たい場合、 サイトへ をタップすることでサイトへ移動します。
実行時のページ画像
環境
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
- 投稿日:2020-12-12T17:55:24+09:00
Mcrypt で暗号化し OpenSSL で複合する
なぜ
古いバージョンの Laravel で実装され、放置気味だったシステムをバージョンアップしたら、
Mcryptが使えなくて困った、という話です。
Mcryptは長期間メンテナンスされておらず、 PHP 7.2 で削除されました。
これに変わるものがOpenSSLです。稼働中システムで、
Mcryptで暗号化したデータが大量にデータベースに眠っている、という状態ですが、 どうやらMcryptとOpenSSLでは、暗号化の結果に差異があるようで困ってしまいました。
結論としては、互換性があるので 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をオプション指定しないと失敗します。
一瞬血の気が引いたのですが、互換性があって問題なくやり取りできるよ、ということのログでした。
- 投稿日:2020-12-12T17:23:13+09:00
[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" } } } } */
- 投稿日:2020-12-12T16:04:42+09:00
ReactとPHP 初めての連携
はじめに
今回はReactで簡単なフォームを作って、PHPの方で受け取るプログラムを書いてみました。Laravelではやったことがあるのですが、フレームワーク無しのPHPでやったことがなくて、フレームワーク無しのPHPをだいぶ忘れてしまったのでやってみました。
テキストを入力して送信したら、そのテキストが表示されて、何も入力しなければエラーが表示される感じのシンプルなフォームです。
以下を参考にさせていただきました。
参考:Create a Contact Form With PHP and React in 3 MinReact側
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.tsximport 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をできるようにしたいなと思います。文章力あまりないのでわかりにくいかもしれませんが、少しずつ続けながら上げていきたいと思います。
- 投稿日:2020-12-12T15:00:37+09:00
【初心者】PHPでの配列の並び替え
- 投稿日:2020-12-12T14:38:32+09:00
自社サービスのインタラクションデザインを改善するためにPHPで非同期処理をしてみた
はじめまして、こんにちは。
今年の10月に福岡で起業して、なんちゃってCTOをしている赤と黒が好きな若造です。
今回は開発中の自社サービスのインタラクションデザインを改善するためにPHPで非同期処理をしてみたという話を書きます。PHPで処理を並行したい。
ユーザーがあるアクションを起こした時に、PHPでの処理に10秒掛かるとします。
その間ユーザーは10秒待つ羽目になるわけですが、この時間が貴重な現代社会に置いて、ユーザーは10秒待つと当然イライラします。
なのでなるべく早くリクエストからレスポンスの時間を短くしたいわけですが、いくらPHP8で爆速になったとは言え、限界もあります。
そこで「重要な処理」と「重要では無い処理」で処理自体を分割して
「重要な処理」が終わった時点でレスポンスを返し、「重要では無い処理」は裏で並列で処理するアイディアを思いつきました。
これなら実質3秒でユーザーにレスポンスを返す事ができます!PHPはシングルスレッドだよ
ですが残念なことにPHPは基本的にシングルスレッドで、並列処理は出来ません。1
だから無理です。
今日のアドベントカレンダー記事は以上です。あざした。
…というわけにはいきません。
PHPはシングルスレッドですが、マルチプロセスは可能です。
「並列処理」ではなくて「分割処理」という言葉のほうがイメージしやすいですね
シングルスレッドとマルチプロセスは結果は同じ様に見えますが、プロセスが分割されているので、変数などの情報を共有できないのは注意しなくてはいけません。
(正確にはメモリが共有出来ない)ではどうやってプロセスを分けるのか、を解説していきます。
exec関数
詳しい解説はPHPの公式のマニュアルに任せます。
PHPマニュアル:https://www.php.net/manual/ja/function.exec.phpPHP 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本来であれば全ての処理をシングルプロセスで処理すると完了に
5+1+10+0=16秒掛かるところを
マルチプロセスにしたおかげで、全ての処理の完了に10秒で済んでしまいました。しかしexec関数はlinuxのコマンドを叩く処理で、マルチプロセスのためのものではありません。
linuxのコマンドを叩くという性質上、うっかりユーザーの入力をそのままexec関数の引数に当てるなんてことは絶対にしないようにしましょう。exec関数でのマルチプロセスの欠点
一見簡単にマルチプロセスで処理できるexec関数ですが、実は弱点があります。
処理を分割するためにプロセスを作るという性質上、処理が立て込んだ時には処理が横に広がりすぎて、サーバーに影響を及ぼすからです。
最悪サーバーが落ちると思います。
Laravelでの非同期処理
素のPHPでマルチプロセスをしてみましたが、実際の業務やサービスではフレームワークを使うことでしょう。
株式会社ナインステクノロジーズはLaravelが得意なので、Laravelでも非同期処理をやってみましょう。Laravelではexec関数でのマルチプロセスとは少し違って「ジョブとキュー」で並列処理を実現します。
ジョブとキュー
厳密に言えば「ジョブとキュー」もマルチプロセス処理の一種で
「ジョブ」と言われる処理の塊を、「キュー」と呼ばれるプロセスが変わって処理するイメージです。
最初ジョブは全く無く、キューもジョブが無いので待機状態になっています。
なんらかのアクションでジョブが記録されると、キューはそれに気づきます。
キューは記録されているジョブを1つ取り出して処理を始めます。こういう流れで処理を非同期的に処理します。
php artisan queue
詳しい解説はLaravelのドキュメントを翻訳しているサイトに任せます。
Laravel 6.x キュー:https://readouble.com/laravel/6.x/ja/queues.htmlLaravel 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"; } }そして
.envのQUEUE_CONNECTIONを編集します。.envQUEUE_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秒掛かったのです。
ジョブ1を処理して、
ジョブ1が終わったらジョブ2を処理して、
ジョブ2が終わったらジョブ3を処理して、
ジョブ3が終わったらジョブ4を処理して、
ジョブ4が終わる。キューは1個なので、処理できるジョブも当然1個です。だから並列的に処理が出来ないんですね。
なので、並列的に処理したい場合はキューの数を増やすと良いでしょう。
キューを増やすことで同時に処理できるジョブの数が増え、効率的に処理をこなすことが出来ます。キューの数だけジョブを捌けるのですが、ここにも落とし穴があって
例えばジョブの増加速度よりもキューがジョブを捌く速度が遅くなってしまうと
いつまで立っても処理が開始されないジョブが出てきてしまう可能性があります。
まさに炎上状態。
一応Laravelではジョブの優先度やキューへの振り分けを操作出来ますが、万が一こういう自体が起こらない訳ではありません。まとめ
PHPはシングルスレッドで並列処理は難しいですが
exec関数やLaravelのqueueで非同期的に処理を実行できます。活用することでユーザーへのレスポンスが早くなることでしょう。
本当はこの処理と合わせてプログレスバーを実装する予定でしたが、以外と長くなったので今度にします。もし間違っているところがあればマサカリお願いします。(言葉尻を捉えてイジメるのはやめてくださぃ。
明日は奇しくも同じ福岡の企業に所属するせいけしろーさんです。
ココまで読んでくださってありがとうございました。
LGTMとTwitterをフォローしてくれると嬉しいです!よろしくおねがいします!!!
@9th_tech_Ryoあと別のアドベントカレンダーになりますが、弊社長も記事を明日書くので、ぜひ見てみてください!!!!!
https://qiita.com/akira_9th
- 投稿日:2020-12-12T13:45:43+09:00
PHP Conference Japan 2020 スライドまとめ
PHP Conference Japan 2020 Re:born
- 公式サイト: https://phpcon.php.gr.jp/2020
- 公式YouTubeチャンネル: https://www.youtube.com/user/PHPConferenceJP
- 公式ツイッター: https://twitter.com/phpcon
- 公式Discord: https://twitter.com/phpcon/status/1337547720806989824?s=20
- ハッシュタグ: #phpcon #phpcon2020
- 日時: 2020.12.12 SAT
YouTube Live
- PHP Conference Japan 2020 - Track 1
- PHP Conference Japan 2020 - Track 2
- PHP Conference Japan 2020 - Track 3
- PHP Conference Japan 2020 - Track 4
- PHP Conference Japan 2020 - Track 5 - PHP8 Special Track
- PHP Conference Japan 2020 - Track 6 - International Track
セッショントーク一覧
資料はTrack順に並べていきます。※資料公開され次第随時更新していきます。
前夜祭
Laravelの黒魔術 | localdisk
- https://www2.slideshare.net/devworks/laravel-php2020-239980059/devworks/laravel-php2020-239980059
- https://twitter.com/localdisk
SPAのAPI開発の「やりづらさ」をDDDとオブジェクト指向の発想で解決する | 菱田裕美
Track1
PHPの今とこれから2020 | 廣川類
PHP WEBアプリケーション設計入門――10年先を見据えて作る | GMOインターネット | 成瀬 允宣
長期運用を目指す『Shadowverse』におけるリファクタ事例の紹介 〜テストの導入とメンバーへの普及法〜 | 株式会社Cygames | 髙野 祐輝
事業のスケールアウトを支えるPHPで作る分散アーキテクチャ | 竹澤有貴
- https://speakerdeck.com/ytake/shi-ye-falsesukeruautowozhi-eru-phpdezuo-rufen-san-akitekutiya
- https://twitter.com/ex_takezawa
パネルセッション「ひさてるさんに聞け」 | たなかひさてる, イアン・ブライソン, 小山哲志
ウェブセキュリティのありがちな誤解を解説する | 徳丸浩
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
- https://speakerdeck.com/cwozaki/php-on-kubernetes-php-conference-2020-re-born-number-phpcon
- https://twitter.com/k_kinzal
Webサービスをセキュアに保つために必要な視点 | ariaki
DNS改ざん検知ツールの実装とDNSパケットの世界 | 市川@cakepher
- https://docs.google.com/presentation/d/1DvrP9tOLvgSeoGynkNt0U8hRqvfZxBtxa0a6wRA8XuE/edit#slide=id.p
- https://twitter.com/cakephper
CakePHPで学ぶDIコンテナ | itosho
- https://speakerdeck.com/itosho525/learn-a-di-container-through-cakephp
- https://github.com/itosho/x-cakephp-di-container
- https://twitter.com/itosho
PhpStormを使えばほとんどコード補完されるんだってばさ | マキ
PHPで作るオンラインカンファレンス向け録画システム | 長谷川智希
- https://speakerdeck.com/tomzoh/building-talk-pre-recording-system-with-php
- https://twitter.com/tomzoh
Track4
Laravelで運用しているサービスをNuxt.jsにリプレイスする | 久保田賢二朗
- https://speakerdeck.com/kubotak/laraveldeyun-yong-siteirusabisuwo-nuxt-dot-jsniripureisusuru
- https://twitter.com/kubotak_public
LaravelDB.comを使ってDB設計「Migration生成」の基本操作を学ぶ | Daisuke Yamazaki
ゼロベースから Laravel を用いた API 実装オートメーション | めもり〜
- https://speakerdeck.com/memory1994/zerobesukara-laravel-woyong-ita-api-shi-zhuang-otomesiyon
- https://twitter.com/m3m0r7
Laravel × オニオンアーキテクチャで始めるテスト駆動開発 | 村田主磨
- https://speakerdeck.com/canon1ky/laravel-x-onionakitekutiyadeshi-merutesutoqu-dong-kai-fa
- https://github.com/kmurata08/laravel-sample
- https://twitter.com/canon1ky
テストピラミッドを意識したテストコード実装戦略 | 02
- https://speakerdeck.com/cocoeyes02/tesutopiramitudowoyi-shi-sitatesutokodoshi-zhuang-zhan-lue
- https://twitter.com/cocoeyes02
本番でしか起きない問題に早く気が付けるように、僕は Laravel Dusk で CI する | sogaoh
- https://speakerdeck.com/sogaoh/ben-fan-desikaqi-kinaiwen-ti-nizao-kuqi-gafu-keruyouni-pu-ha-laravel-dusk-de-ci-suru
- https://github.com/sogaoh/snipe-it/tree/dusk/tests/Browser
Laravel + Lighthouseで始める低コストなGraphQL入門 | あきの/akkino
- https://speakerdeck.com/d_endo/laravel-plus-lighthousedeshi-merudi-kosutonagraphqlru-men
- https://twitter.com/DddEndow
Track5
PHP 8 で作る JSON パーサ | 新原雅司
- https://speakerdeck.com/shin1x1/php8-json-parser
- https://github.com/shin1x1/php8-toy-json-parser/tree/phpcon2020
- https://twitter.com/shin1x1
PHPのソースコードから理解するPreloadとJIT | 富所 亮
PHP8はISUCONへの扉を開く鍵となるか | 清家史郎
- https://docs.google.com/presentation/d/1BgaSPWa5NJTAIC__8kRRx3lzxximKNvTcxuNWnA0Td0/edit
- https://twitter.com/seike460
PHP 8 の新機能を PHP内部コードのテスト phpt から読む | 東口和暉
PHP8時代のWebアプリケーションフレームワークの話をしよう | 中榮健二
- https://speakerdeck.com/n1215/php8shi-dai-falsewebapurikesiyonhuremuwakufalsehua-wosiyou
- https://twitter.com/n_1215
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 | もも
チームメンバーをエンパワーメントしよう!レガシープロジェクト改善事始め | ぬさし
- https://www2.slideshare.net/nukisashineko/ss-240043842/nukisashineko/ss-240043842
- https://twitter.com/nukisashineko
静的解析から始める負債コード解消 | 藤岡大樹
レガシーシステムに自動テストを導入する第一歩 | 荒巻拓哉
PHPer のための Vim 実践入門 | 濱田 晃輔
- https://speakerdeck.com/hamakou108/phper-falsetamefalse-vim-shi-jian-ru-men
- https://twitter.com/hamakou108
8年運用しているCakePHPのECサイトをLaravelにリプレイスした一年後の話 | 近藤心平
Symfony公式の『日本語』入門書ができたよって話 | 角田 一平
再コンパイル不要! core dump さえ吐ければ gdb デバッグできます | 鈴木 智也
- https://speakerdeck.com/yamotuki/zai-konhairubu-yao-dot-core-dump-saetu-kereha-gdb-tehatukutekimasu
- https://twitter.com/yamotuki
PHPerのためのCVEデータベースの紹介 | ナガノ
- https://speakerdeck.com/nagano/phperfalsetamefalse-cvedetabesufalseshao-jie
- https://twitter.com/glassmonekey
めざせブレークポイントマスター | つざき
PHPConのElePHPantができるまで | nauleyco
- 投稿日:2020-12-12T13:01:57+09:00
Laravel 5.5 Storageファサードで画像ファイルを保存
URLから取得した画像ファイルをStorageファサードを使い
storage/app/public/下に保存した時に、少し詰まったのでメモ。Storageファザードを使ってファイルを保存する方法
Storageファザードを使ってファイルを保存する方法は以下
公式リファレンスより抜粋
https://readouble.com/laravel/5.5/ja/filesystem.htmlreadouble.com/laravel/5.5/ja/filesystem.htmluse Illuminate\Support\Facades\Storage; Storage::put('file.jpg', $contents);Storage::put()を使うと、第一引数にファイル名、第二引数に保存したいファイルを入れます。
そうすると通常はstorage/app/public/内に画像が保存されます。でも、保存先フォルダも指定したい!
という場合はこちらreadouble.com/laravel/5.5/ja/filesystem.htmluse 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などのクラウドディスクにファイルを保存する場合もこの方法で大丈夫なのではないかとおもいます!
- 投稿日:2020-12-12T11:29:09+09:00
きっと正しくないレンタルサーバーの作り方 Vol.3 - サーバーをセットアップする
はじめに
前回の記事からだいぶ時間が経ちましたが、第2回めです(・∀・)。
今回は全体的なアーキテクトについて記述していこうと思います(・∀・)。※注意!
この一連の記事で紹介するコードは動作の概念を説明するものでありセキュリティーなどは意識していません(・∀・)。実際に運用するシステムなどに使用しないでください(・∀・)。
(そのまま使うひともいないと思いますが)また、私も記事を書きながら開発をしていくので「後になってみたら最初の方の記事間違えてたー」なんて事は起きそうです(・∀・)。
ご了承ください(・∀・)。
目次
- きっと正しくないレンタルサーバーの作り方 Vol.1 -プロジェクト事始め
- きっと正しくないレンタルサーバーの作り方 Vol.2 - ざっくりアーキテクト
- きっと正しくないレンタルサーバーの作り方 Vol.3 - サーバーをセットアップする(この記事)
サーバー設定前のDNSまわり下準備
ドメインを取る
今回は お名前.com を使用します(・∀・)。
他のレジストラサービスでも良いと思います(・∀・)。レンタルサーバーサービスの運用には独自ドメインが必要になりますので、適当に取得してみてください(・∀・)。
.xxx とか .xyz とかの適当なドメインなら初年度はけっこう安いです(・∀・)。この記事では rentaserve.com という独自ドメインを取得したように記事を書きます(・∀・)。
ドメイン取得方法については割愛します(・∀・)。
(ちゃんと書いて欲しい人が居たらコメントください)VPSを借りる
サーバーOSには Debian/GNU Linux 10 を採用します(・∀・)。
ホスティングサービスには [ConoHa VPS] を使用します(・∀・)。Debian ベースの Raspberry Pi でもほぼ同等の手順を取れると思いますので、自宅サーバーでレンタルサーバーサービスなどにチャレンジしてみても面白いかも知れません(・∀・)。
(グローバルIPアドレスが必要になります)VPS や Linux の基本的な手順については割愛します(・∀・)。
(ちゃんと書いて欲しい人が居たらコメントください)私は以下の感じで VPS を作成しました(・∀・)。
独自ドメインでDNSサーバーを動かす下準備
ドメインをネームサーバーとして動作させる設定をお名前.com で行います(・∀・)。
取得したドメイン(今回の場合は rentaserve.com )をネームサーバー(権威DNSサーバー)として動作させるための設定です(・∀・)。
まず、作成した ConoHa VPS のグローバルIPアドレスが必要になります(・∀・)。
ConoHa の VPS 詳細画面から確認できます(・∀・)。rentaserve.com の場合は 150.95.216.242 になりますね(・∀・)。
次に、お名前.con の NAVI 画面からDNS関連の設定を行っていきます(・∀・)。
rentaserve.com を選択した状態で設定画面の「ネームサーバー名としてホスト登録を行う」を選択します(・∀・)。
これは、取得した独自ドメインにアクセスした時にどのIPアドレスのDNSサーバーの情報を見に行くかの設定で、独自ドメインでDNSサーバーを立ち上げるのに必須の設定になります(・∀・)。
ここに
- ns1.rentaserve.com
- ns2.rentaserve.com
のふたつのDNSホストを作成します(・∀・)。
ホストには ConoHa VPS インスタンスのグローバルIPアドレスを設定してください(・∀・)。
※注意!
本来は ns1 と ns2 は別々のDNSサーバーを立ててそれぞれのIPアドレスを設定して障害耐性を高めます(・∀・)。
今回は全てを単一のサーバーで実現するためにインチキをしています(・∀・)。最後に rentaserve.com のネームサーバーを ns1.rentaserve.com / ns2.rentaserve.com に設定します(・∀・)。
ここに最低ふたつの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 以外の名前をつけておくのも良いかもですね(・∀・)。
rebootActive Directory ドメインコントローラーの構築
概説
rentaserve では、Linux のアカウント管理は Active Directory という機能に任せます(・∀・)。
Active Directory について説明しだすとそれだけで記事がかけてしまいますが、すっごく簡単に言うと
- LDAPというデータベースを使用して、ここにユーザーデータを書き込むと勝手にアカウントが作成される
- データベースを連携させれば複数マシンでアカウント情報を共有できる
という素敵システムです(・∀・)。
これは、レンタルサーバーが複数台に分かれた時でもサーバー間で Linux アカウントを共有したいなーと思って導入しました(・∀・)。
巷のレンタルサーバーでこのようなアカウント管理はあまり見ない気もする(・∀・)?
(しらんけど)インストールと設定
とりあえず必要なものをインストール(・∀・)。
Debian/GNU Linux 10 は何でも標準的にパッケージがあって楽ですね(・∀・)wapt install samba krb5-config winbind smbclient libpam-winbind libnss-winbind krb5-config resolvconfインストール中、なにか訊かれても適当に進めて大丈夫です(・∀・)。
この設定は一度消して書き直しますので(・∀・)。次に、Active Directory 構築のために、既存の標準動作を止めます(・∀・)。
systemctl stop smbd nmbd winbind systemctl disable smbd nmbd winbindActive 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/interfaceseth0 の設定の末尾に
- 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-dcmariadb
インストール
ざっくり mariadb をインストールします(・∀・)。
基本的にデフォルトで問題ないと思います(・∀・)。apt install mariadb-server systemctl restart mariadb systemctl enable mariadb mysql_secure_installationrentaserve.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.confZZZ-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 apache2DNSサーバーを構築する
インストール他
まずは PowerDNS をインストール(・∀・)。
apt install pdns-server pdns-backend-mysql systemctl stop pdnsDNSサーバーの設定を行う(・∀・)。
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 プロジェクトにマイグレーションファイルとシーダーを用意指定あります(・∀・)。
GitHubCREATE 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 するだけです(・∀・)。
最後に
必要なサーバー設定はある程度終わりましたので、次回以降はレンタルサーバーのシステム開発に入れたら良いなーと思っています(・∀・)。
- 投稿日:2020-12-12T10:50:19+09:00
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にアップロードして確認してみて下さい。
- 投稿日:2020-12-12T10:27:00+09:00
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に出力されている文字がおかしいのは一旦置いておきます。
PHPecho '[今日のメモ]' . 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 | +----+--------------------+----無事完了!
参考にさせていただきました!ありがとうございます!
爆速でターミナルを使うためのショートカット集間違えている所や、ここもっと勉強するといいよ!などコメントいただけましたら、かなり有り難いのでよろしくお願いいたします笑
- 投稿日:2020-12-12T00:01:37+09:00
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での開発中に意図せず出てきたので、
使っている変数を確かめてみると、メッセージが解消できました。































