20211126のPHPに関する記事は7件です。

PHP で .txt ファイルから文字列を読み取り、配列に変換し表示させる

プログラミング初心者の Qiita 初投稿なのでヘタクソな文面、ご了承ください。 PHP を習いたてで、下記のような流れで制作していて少しつまずいた点を書いていきます。 1. 「post.php」というファイルで form を作成し「write.php」へ送信 2. 「write.php」から「data.txt」へデータを書き込み 3. 「data.txt」のデータを「read.php」というデータで読み取り 今回詰まったところは上記 3 の手順です! ということで自分が詰まって、解決まで数時間かかった超しょぼいオチをご覧ください。 まず、「data.txt」は以下のデータが入っております。 2021-11-25 06:37:41,たけし,a@g.com,29 2024-6-09 12:08:26,まさる,aghf@aggaag.com,40 左から順に「日付, 名前, メールアドレス, 年齢」です。 今回は 2 人がフォームを送ってくれたので、 2 行です。 はい、そして僕が最初に書いたのはこちら ↓ <?php $filename = 'data/data.txt'; $fp = fopen($filename, 'r'); while (!feof($fp)) { $txt = fgets($fp); $datas = explode(",", $txt); $lines = file($filename); print_r($lines); } fclose($fp); echo __FILE__; ?> <tr> <td><?= $datas[0]; ?></td> <td><?= $datas[1]; ?></td> <td><?= $datas[2]; ?></td> <td><?= $datas[3]; ?></td> </tr> うん、意味わからんコードだ。 ちなみに出力結果としては、こんなんでした ↓ 2024-6-09 12:08:26 まさる aghf@aggaag.com 40 まさるしか受け取れねーよ!!!ってキレてました。 なぜか最後の行のデータしか取得ができませんでした。 その後はこんな解決法を模索 ↓ * include() の値を取得して編集して配列にできねえかなあ * 一旦行ごとに配列にしないといけないのでは? ちなみに一行ごとに配列にはこちらでできました ↓ // 行ごとに配列にする処理 $file = dirname(__FILE__) . '/data/data.txt'; // dirname(__FILE__) でファイルまでのデータのパスを取得 $array = @file($file, FILE_IGNORE_NEW_LINES); print_r($array); 結局原因はただただ、繰り返し処理の while の中に出力処理を入れてなくて、もはや PHP の中から飛び出してかいておりました。 この部分 ↓ <tr> <td><?= $datas[0]; ?></td> <td><?= $datas[1]; ?></td> <td><?= $datas[2]; ?></td> <td><?= $datas[3]; ?></td> </tr> 要はこれを while 文の中に書いて整えるだけでしゅーりょー。 4 時間くらい溶かしたのでは。。。? // .txt データを配列にする処理 $fp = fopen('data/data.txt', 'r'); // fopenでファイルを開く('r'は読み込みモードで開く) while (!feof($fp)) { // feof は最後までの読みこれたかどうか。while の時に使用する。 $txt = fgets($fp);// fgetsでファイルを読み込み、変数に格納 // 下記で呼び出し表示 $ary = explode(",", $txt); print "<tr>"; print "<td>$ary[0]</td>"; print "<td>$ary[1]</td>"; print "<td>$ary[2]</td>"; print "</tr>"; } fclose($fp); // fcloseでファイルを閉じる ?> これからも頑張ろう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel】画像保存処理実行時のエラー対処

Laravelで画像保存処理を実行中、遭遇したエラーと対処法をまとめました。 開発環境 PHP7.4 Laravel6 Bootstrap4.4 Docker20.10.8 前提 画像保存処理は実装済み テーブル作成・マイグレーション実行済み バリデーション実装済み フォーム作成済み プレビュー表示処理実装済み(JavaScript) Intervention Image インストール済み public artisan storage:link 実行済み ❗注意 コマンドをコピペして実行する際、コマンドの最後に余分なスペースが入らないよう注意しましょう。 コマンドが強制実行される場合があります。実行の際は慎重に。 目次 画像が正常に保存されない。 GD Library extension not available with this PHP installation.解決 1. 画像が正常に保存されない。 フォームをクリックして拡張子.JPEGの画像ファイルを選択。 プレビューも正常に表示 更新ボタンクリック後、プロフィール画面に遷移するはずが、バリデーションに引っかかり保存できない。 バリデーションは以下の通りです。(画像アップロード処理の部分のみ抜粋) src/app/Request/UserRequest.php public function rules() { return [ 'avatar' => ['file', 'image'], ]; } public function attributes() { return [ 'avatar' => 'プロフィール画像', ]; } Viewは以下のとおりです。(画像フォームのみ抜粋) src/resources/view/users/edit.blade.php <form method="POST" class="p-3 mb-1" action="{{ route('users.update', ["name" => Auth::user()->name] )}}" > @method('PUT') @csrf -----省略----- <div class="avatar-form image-picker text-center"> <input type="file" name="avatar" id="avatar" class="d-none @error('avatar') is-invalid @enderror" accept="images/png,image/jpeg" /> <label for="avatar" class="d-inline-block"> @if (!empty($user->avatar)) <img src="/storage/avatars/{{$user->avatar}}" class="rounded-circle" style="object-fit: cover; width: 125px; height: 125px;"> @else   <img src="/images/default.svg" class="rounded-circle" style="object-fit: cover; width: 125px; height: 125px;"> @endif  </label>  <div class="small">プロフィール画像をアップロードできます。</div>  @error('avatar')  <span class="invalid-feedback" role="alert">  <strong>{{ $message }}</strong>  </span>  @enderror </div> 色々調べてみた結果、formタグに以下一文が抜けていたせいで保存出来なかった事が判明 enctype="multipart/form-data" enctype="multipart/form-data"については以下の方の記事をご参考いただくと、理解が深まります。 以下のように修正します。 src/resources/view/users/edit.blade.php <!-- enctype="multipart/form-data"追加 --> <form method="POST" class="p-3 mb-1" action="{{ route('users.update', ["name" => Auth::user()->name] )}}" enctype="multipart/form-data"> 再度フォームをクリックして拡張子.JPEGの画像ファイルを選択。 プレビューも正常に表示 更新ボタンクリック後、プロフィール画面に遷移するはずが、今度は以下のエラーに遭遇。 GD Library extension not available with this PHP installation. 2. GD Library extension not available with this PHP installation.解決 調べてみたところ、インストール済みのIntervention Imageを使用するためには、GD Libraryがインストールされていなければならないことが判明 GD Libraryのインストール手順は、Dockerfileにコードを追加して行う方法と、 ターミナルコマンドで実行する方法があるが、私はターミナルコマンドでインストールしたので、その方法を解説します。 appコンテナに入って実行してください  以下コマンドでappコンテナに入る。(環境によってはコマンドが異なります) ターミナル $ docker compose exec app bash 以下のコマンドを上から順に実行(PHP7.4の場合) ターミナル # apt-get update # apt-get install -y zlib1g-dev libpng-dev libjpeg62-turbo-dev # docker-php-ext-configure gd --with-jpeg # docker-php-ext-install -j$(nproc) gd インストールできたらコンテナの再起動をしてください。 以下のコマンドで実行 ターミナル  $ docker restart コンテナ名 コンテナ再起動後、再び画像を保存してみる。 * 再度フォームをクリックして拡張子.JPEGの画像ファイルを選択。 * プレビューも正常に表示 * 更新ボタンクリック後、プロフィール画面に遷移し画像も正常に表示されました。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelのEloquentに $fillable でなく $guarded を使う

はじめに 今回はEloquentの $fillable 指定についてです。 基本的に $guarded を使う方が良いと思っているのでそれについて簡単に書いてみようと思います。 $fillable と $guarded $fillable $fillable は指定したカラムのみ、create()やfill()、update()で値が代入される、所謂ホワイトリストです。 <?php namespace App\Models; use App\Models\Model as AppModel; class User extends AppModel { protected $fillable = [ 'name', 'sex', ]; } $guarded $guarded は指定したカラムのみ、create()やfill()、update()で値が代入されない、所謂ブラックリストです。 <?php namespace App\Models; use App\Models\Model as AppModel; class User extends AppModel { protected $guarded = [ 'id', ]; } 長所と短所 $fillable 長所 指定したもの以外は入り得ない 短所 カラムが増えるたびに追加する必要がある。 特に横に長いテーブルだとツラい $guarded 長所 基本的に指定は増えない id や email、 password などだけ 短所 指定していないものは全て入り得る 上記を踏まえた上で $guarded を選んぶ理由 複数代入されることを期待しているデータは、モデルごとにあるのではなく、アクション毎 にあります。 また、↓のような クソコード が書かれている場合カラムが横に長く伸びたテーブルの場合指定漏れが起きる可能性があります。 $model->fill($request->all())->save(); ↓のような場合のコードであればギリギリ許容されますが、そもそもの $fillable に漏れがあれば意味ありません。 $model->fill($request->only('foo', 'bar'))->save(); まとめ \$model->fill(\$request->all()) と言うクソコードを止めよう fill を防ぐのは Model の役目じゃない $fillable でも問題はないが、テーブルが横長になるほどツラくなる $fillable のほうが便利でいい!な意見がありましたら是非教えてほしいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelのAuth機能でログアウト後の遷移先を変更したい

はじめに LaravelのAuthを使用してログイン機能を実装。 ログアウト後の画面を"/home"ではなく"/"に変更したい。 実装内容 LoginControllerへ以下を追記しました。 loginController protected function loggedOut(\Illuminate\Http\Request $request) { return redirect("/"); } おわりに すごく簡単でした。。 Laravel本当に便利すぎます。。 もともと、loggedoutは実装されてませんでしたが、/homeへリダイレクトされるようになってるんですね どのような補完をおこなっているんでしょうか
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【WordPress】Ajaxでデータを送信する方法

WordPressに対してajaxでデータを送信する方法を、ご紹介したいと思います。 POSTで送信する方法を例に挙げて書いていますが、GETでも同じことが可能です。 環境 WordPress 4.9.18 PHP 7.0.27 jQuery 2.2.4 受け取り側の実装 まず、WordPressがajaxリクエストを受け入れるために任意のaction名を名前の末尾にもつアクションフックに、リクエストを受け取った時に実行されるコールバック関数を登録する必要があります。 アクションフック名の書き方は「wp_ajax_action名」または「wp_ajax_nopriv_action名」となります。 これら二つのアクションフックの違いは、前者がログインユーザー、後者の方は未ログインユーザーからのajaxリクエストを受け入れるものになります。 両ユーザーからのリクエストを受け入れる必要がある時は両方書く必要があります。 コードとしては以下のようになります。 function.php function ajax_sample () { $data = $_POST; echo $data['name']PHP_EOL; echo $data['age']PHP_EOL; wp_die(); } add_action('wp_ajax_my_ajax', 'ajax_sample'); add_action('wp_ajax_nopriv_my_ajax', 'ajax_sample'); 上記の例ではaction名を「my_ajax」としています。 コールバック関数「ajax_sample」の内容としては受け取ったデータを項目ごとに出力する簡単なものです。 次に、WordPressに対してajax通信を行うための送信先(エンドポイント)である「サイトURL/wp-admin/admin-ajax.php」をJavaScriptのパラメータに出力します。 今回URLは共通ヘッダーに出力したいので、function.phpに以下のコードを追記しておきます。(もちろんURLを個々のJavaScriptのコードに直接描いても構いません) function.php function generate_js_params() { ?> <script> let ajaxUrl = '<?php echo esc_html(admin_url( 'admin-ajax.php')); ?>'; </script> <?php } add_action('wp_head', 'generate_js_params'); 送信側の実装 送信側の処理をJavaScript(jQuery)で書いていきます。 この時、PHPのコードでアクションフックを記述した際に使用したaction名を送信するデータのパラメータに付与する必要があります。 もし付与せずに送信してしまうと400 Bad Requestが返されてしまいます。 main.js $('.button').click(function (e) { e.preventDefault(); let data = { action: 'my_ajax', name: "太郎", age: 25 } $.ajax({ type: "post", url: ajaxUrl, data: data }).done(function (data) { console.log("ajax成功"); console.log(data); }).fail(function (XMLHttpRequest, status, e) { console.log("ajax失敗: " + status + XMLHttpRequest); console.log(e); }); } このように送信するオブジェクトの中にactionプロパティを他のパラメータと一緒にセットします。 こうすることでajaxを受け取ったWordPress内部で処理が通り、コールバック関数に記述した内容が実行されます。 その時にconsoleに出力される内容は以下の通りです。 ajax成功 太郎 25 参考文献 https://haniwaman.com/wordpress-ajax/ http://semooh.jp/jquery/api/ajax/jQuery.ajax/options/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでのTwilio自動音声の実装方法と日本語の読ませ方のコツ

はじめに 株式会社じげんの前川です。 リフォームマッチングサイト リショップナビの開発を担当しています。 今回は、phpでTwilioを使った自動音声機能について書いていきます。 Twilioでは、取得した電話番号にwebhookを設定することで、APIを使って、音声を流したり、別の電話番号に転送をしたりすることができます。 今回は、電話がかかってきた際に、自動音声を流し、発信者の入力した郵便番号を取得するということを行いましたので、その実装方法と自動音声としてテキストを読み上げてもらうときに工夫した点について書いていきます。 環境 ・PHP7.2以上 ・Twilio Sdk をインストール済み(twilio/sdk: 5.24) ※Laravel5系を使用しております 前提 Twilioのアカウントを所有 (電話可能な)電話番号を取得済み 目次 概要 実装  Twilio側の設定 最後に 1.概要 下図の構成を実装します Twilioの電話番号にユーザーが電話をかけると、図のincomingにリクエストが飛ぶようにします。(Twilioの管理画面で設定します。) TwilioはTwimlというXMLを返すと、特定のアクション(決まった桁の番号を入力、特定のキーを押した、通話が切れたなど)をした場合に、再度Twilioからリクエストを返してくれます。 こちらを利用して、実装をしていきます。 2.実装 概要でも触れた通り、基本的には、リクエストが来た際にTwiml(Twilio用のタグが定義されたXML)を返す形で実装していきます。 Twilioから受け取るリクエストの詳細はこちらを参考にしてください。 incomingの実装 incomingでは「○○にお問合せの方は・・・」が流れた後に1 or 2を入力したらzipcodeに、#を入力 or 電話を切った場合にはendにリクエストを送るようなTwimlを返します。 use Twilio\Twiml; public function incoming(Request $request){ $zipcodeUrl = zipcodeのURL; $endUrl = endのURL; $response = new Twiml(); $gather = $response->gather(array('numDigits' => 1, 'action' => $zipcodeUrl, 'method' => 'POST', 'finishOnKey' => '#')); $gather->say('〇〇についてお問い合わせのかたは、1、を、△△のお問い合わせのかたは、2、を、押してください。',['voice' => 'man', 'language' => 'ja-jp']); $response->redirect($endUrl); return response($response, 200)->header('Content-Type', 'application/xml'); } 番号を収集する際にはTwimlのgatherタグを使用します。 gatherの設定については下記です。 パラメーター 設定内容 numDigit 入力できる数字の桁数 action 指定した桁数分入力が完了したときにリクエストを送るURL method actionで指定したURLにリクエストを送る際のmethod finishOnKey gatherの処理を終了するキー $gather = $response->gather(array('numDigits' => 1, 'action' => $zipcodeUrl, 'method' => 'POST', 'finishOnKey' => '#')); $gather->say('〇〇についてお問い合わせのかたは、1、を、△△のお問い合わせのかたは、2、を、押してください。',['voice' => 'man', 'language' => 'ja-jp']); 今回の設定だと、自動音声を読み上げ中、読み上げ後に 1桁の番号の入力があったときに、zipcodeにリクエストを送るということになります。 その他のタグについてです。 $gather->say('〇〇についてお問い合わせのかたは、1、を、△△のお問い合わせのかたは、2、を、押してください。',['voice' => 'man', 'language' => 'ja-jp']); sayはテキストを読み上げるタグです。 渡したテキストを読み上げてくれます。(これが、日本語を読ませるとなかなかクセのある読み方をしてくれますw)voiceで声の設定(男性、女性など)、languageで言語を設定できます。 「お問合せの方」が「お問合せのかた」となっているのは「方」を「ほう」と呼んでしまうためです。 また、読点を多く入れているのは、聞き取りやすくするためです。読み上げるスピードが速いため、聞き取りづらいと思うところに読点を入れています。 $response->redirect($endUrl); redirectタグは指定したURLにTwilioからリクエストを送ることができます。gatherと一緒に設定しておくとfinishOnKeyに設定されたキーを入力したり電話を切った場合に、設定したURLにTwilioがリクエストを送ってくれます。 今回は、endにリクエストを送るようにします。 zipcodeの実装 zipcodeでは「郵便番号を7桁で・・・」が流れた後に7桁の郵便番号を入力したらconfirmに、#を入力 or 電話を切った場合にはendにリクエストを送るようなTwimlを返します。 use Twilio\Twiml; public function zipcode(Request $request){ $incomingUrl = incomingのURL; $endUrl = endのURL; if ($request->has('Digits')) { if ($request->Digits == 1) { //1を入力 //1を入力したときにやりたい処理を書いてください。Twimlのtransmissionを使って電話転送や、DBになんの問い合わせがあったか残しておくなど } elseif($request->Digits == 2) { //2を入力 //2を入力したときにやりたい処理を書いてください。Twimlのtransmissionを使って電話転送や、DBになんの問い合わせがあったか残しておくなど } else { //1,2,#以外のボタンを押した場合 $response = new Twiml(); $response->redirect($incomingUrl); return response($response, 200)->header('Content-Type', 'application/xml'); } }                 $response = new Twiml(); $gather = $response->gather(array('numDigits' => 7, 'action' => $confirmUrl, 'method' => 'POST', 'finishOnKey' => '#')); $gather->say('郵便番号を7桁で入力してください。わからない場合はシャープを押してください。',['voice' => 'man', 'language' => 'ja-jp']); $response->redirect($storeUrl); return response($response, 200)->header('Content-Type', 'application/xml'); } コードの説明です if ($request->has('Digits')) { if ($request->Digits == 1) { //1を入力 //1を入力したときにやりたい処理を書いてください。Twimlのtransmissionを使って電話転送や、DBになんの問い合わせがあったか残しておくなど } elseif($request->Digits == 2) { //2を入力 //2を入力したときにやりたい処理を書いてください。Twimlのtransmissionを使って電話転送や、DBになんの問い合わせがあったか残しておくなど } else { //1,2,#以外のボタンを押した場合 $response = new Twiml(); $response->redirect($incomingUrl); return response($response, 200)->header('Content-Type', 'application/xml'); } } gatherのactionで設定したURLに返されたリクエストにはDigits(入力した番号)が返ってきます。 なので、Digitsの存在チェックと、何が返ってきたかの判断をしています。今回は何もしませんが、1、2の場合に特定の処理をしたい場合はここに記述します。1、2、#以外の値が返ってきた場合はredirectのTwimlを返して 「◯◯にお問い合わせの方は1を、△・・・」に戻るようにしています。 $response = new Twiml(); $gather = $response->gather(array('numDigits' => 7, 'action' => $confirmUrl, 'method' => 'POST', 'finishOnKey' => '#')); $gather->say('郵便番号を7桁で入力してください。わからないばあいはシャープを押してください。',['voice' => 'man', 'language' => 'ja-jp']); ここは先程と変わりませんが、テキストで「#」を渡すと「シャープ」と呼んでくれないので、カタカナにしています。(#で渡した時は何て読んでいるのかわからなかった、、) confirmの実装 confirmでは「郵便番号はXXXXXXXで・・・」が流れた後に1を入力 or #を入力 or 電話を切った場合にはendに、2を入力したらzipcodeにリクエストを送るようなTwimlを返します。 use Twilio\Twiml; public function confirm(Request $request){ $endUrl = endのURL;   if ($request->has('Digits')) { $readingZipCode = ""; $zipArray = str_split($request->Digits); //何百何万・・・と読み上げるので「、」区切りの文字列に変換 $readingZipCode = implode(' 、 ', $zipArray); //0を「れい」と読み上げ、聞き取り辛いので、0を「ゼロ」に変換 $readingZipCode = str_replace('0','ゼロ',$readingZipCode); //7を「しち」と読み上げ、聞き取り辛いので、7を「ナナ」に変換 $readingZipCode = str_replace('7','ナナ',$readingZipCode); } $response = new Twiml(); $gather = $response->gather(array('numDigits' => 1, 'action' => $endUrl, 'method' => 'POST', 'finishOnKey' => '#')); $gather->say('郵便番号は、 '.$readingZipCode.'、でよろしいですか、よろしければ、1、を、入力しなおす場合は、2、を押してください。',['voice' => 'man', 'language' => 'ja-jp']); $response->redirect($endUrl); return response($response, 200)->header('Content-Type', 'text/xml'); } コードの説明です if ($request->has('Digits')) { $readingZipCode = ""; $zipArray = str_split($request->Digits); //何百何万・・・と読み上げるので「、」区切りの文字列に変換 $readingZipCode = implode(' 、 ', $zipArray); //0を「れい」と読み上げ、聞き取り辛いので、0を「ゼロ」に変換 $readingZipCode = str_replace('0','ゼロ',$readingZipCode); //7を「しち」と読み上げ、聞き取り辛いので、7を「ナナ」に変換 $readingZipCode = str_replace('7','ナナ',$readingZipCode); } ここは、入力された郵便番号を聞き取りやすく読ませるための処理です。 受け取った値をそのまま渡すと何百何万、、、と読み上げてしまう(1234507なら百二十三万四千五百七)ので一文字ごとに読点を入れています。(1234507 => 1、2、3、4、5、0、7) また、0を「れい」、7を「しち」と読み上げるので、聞き取りやすいよう、「ゼロ」、「ナナ」と置き換えています。 (最終的には 1234507 => 1、2、3、4、5、ゼロ、ナナ) endの実装 endでは 2が入力されていた場合にはconfirmにリクエストを送り、それ以外の場合には「サポートセンターから・・・」を流した後に電話を切るTwimlを返します。 use Twilio\Twiml; public function end(Request $request){ $zipcodeUrl = zipcodeのURL; $confirmUrl = confirmのURL; $response = new Twiml(); if ($request->has('Digits')) { if ($request->Digits == 1) { //1を入力->郵便番号確定 $response->say('ありがとうございます。 サポートセンターからご連絡させていただきます。',['voice' => 'man', 'language' => 'ja-jp']); } elseif($request->Digits == 2) { //2を入力->再入力 $response->redirect($zipcodeUrl); } else { //関係ないボタンを押した $response->redirect($confirmUrl); } }else{ //番号の入力がない場合(相談内容の選択、郵便番号の入力で#を押した等) $response->say('サポートセンターからご連絡させていただきます。',['voice' => 'man', 'language' => 'ja-jp']); } return response($response, 200)->header('Content-Type', 'application/xml'); } コードの説明です。 $response = new Twiml(); if ($request->has('Digits')) { if ($request->Digits == 1) { //1を入力->郵便番号確定 $response->say('ありがとうございます。 サポートセンターからご連絡させていただきます。',['voice' => 'man', 'language' => 'ja-jp']); } elseif($request->Digits == 2) { //2を入力->再入力のためzipcodeへ $response->redirect($zipcodeUrl); } else { //関係ないボタンを押した->再度confirmへ $response->redirect($confirmUrl); } }else{ //番号の入力がない場合(相談内容の選択、郵便番号の入力で#を押した等) $response->say('サポートセンターからご連絡させていただきます。',['voice' => 'man', 'language' => 'ja-jp']); } endに関しては、他のURLで#を押したときにも実行されるのでDigitがリクエストに存在しなかった場合にも自動音声を流すようにsayを返すようにしています。 また、say以外のタグ(gatherやredirect)を設定していないので、自動音声を流した後に通話が切れます。 3.Twilio側の設定 Twilioの管理画面からwebhookを設定します。 画像赤枠の箇所にURLを記述します。 実装については以上です。 4.最後に 今回は、電話がかかってきた相手に対して、メッセージを流して、郵便番号を入力してもらう処理に関してだけ書きました。 $request->Fromでかけてきた電話番号を取得することもできるので、電話番号以外に必要な情報を数字と紐付けて選択してもらうことで電話経由でのコンバージョンを獲得することもできます。 また、一回の通話に対して、ユニークなIDが割り当てられる($request->CallSidで取得できます)ので、それを利用して一度入力された値を書き換えるということにも対応できます。 他にも様々な機能がりますし、SMSの方でも色々なことができるので、電話、SMSで何かしたいときには利用を検討してみるといいと思います。 おまけ 今回はガッツリPHPを使って実装しましたが、Twilio Studioを使っても同じようなことができます。 こちらはエンジニア以外の方でも触れやすいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HumHubの検索にApacheSolrを使わせてみる(前編)

はじめに HumHubには標準で検索機能がついてはいるものの、検索はコンテンツ本文、タグ、トピックに対する単語の列記でのAND検索、可能なフィルタは情報ソースのタイプ(種類:スペース名かユーザー名かコンテンツか)とスペースの絞り込みの対応である。このままでも使えなくはないのだが、将来、ウェブサーバーの冗長化に対応できるようにすることをモクロミ、全文検索システムサーバーを別に立てて利用することを検討する。 調査 HumHubでは、そのプロフェッショナルバージョン(有料)で、HumHub謹製Solrプラグインを追加利用できる。お金とHumHubを信じる心があるときはそれを使えばよいだろう。 いずれも持ち合わせない金銭的にも心も貧しい私のような人は、Solrに接続してクエリを投げて回答を得られる機能を持つプラグインを、自前で作ればよいのだ。その方向で調査を続行した。そう、私は心が貧しい・・・。 HumHubの検索機能 HumHubはDBにMysqlを使っているので、Mysqlの全文検索機能を使っているのかと思ったが、違った。 (参考) https://tech.bita.jp/article/4 https://techblog.gmo-ap.jp/2020/01/06/mysql-innodb-fulltext-search-tuto/ https://recruit.gmo.jp/engineer/jisedai/blog/mysql_fulltextsearch/ HumHubでは、全文検索用に自前でZendLuceneエンジンのためのインデックスファイルを作成する機能をもっていて、そのインデックスを使った検索を行う。なんと、収録したPDFやWord,Excelなんかのファイルの文章もインデックスに入れてしまうこともできるらしい。 https://docs.humhub.org/docs/admin/search/ HumHubでSolrを使うときも、Luceneを使えばいいのかも。 プラグインの材料 プラグインの材料として以下を見つけてきた。 2021年11月現在においても開発が継続されている。注意点は対応環境(Requirements)。Ver.6.x以降はPHP Ver.7.3以上が必要だ。Ver.1.9のHumHubにはちょうどよい(ハズ)。 開発環境構築 まずは、solariumのComposer.jsonを眺めながら、HumHubとのマッチングを調査する。 ・・・はい、ダメ。HumHubの使っているCodeceptionのバージョンは、solariumよりも旧いバージョンのphpuinitを要求するため、Composerがインストールに失敗する。そこで、solariumのcomposer.json のrequire-devセクションから、phpunitに対する要求を削除してインストールしてみた。テストをしなければいいのだ、たぶん・・・。 HumHubがいつまでも旧いパッケージに依存しているのでよくないのかもしれないが、それがオープンソースというものだ。万人受けするように作られる最新パッケージに対応してセキュリティも万全オッケーなオープンソースなどありえない、と悟ったふりをして遠くを見つめる目をしよう・・・心のなかでHumHubへの悪態を掃いて捨てるほど唱えながら。 開発開始 モジュールの設計 まずはHumHubのモジュールをいつもの雛形にそって作成する。今回はサーチエンジンのプラグインモジュールになるので、engineディレクトリを作ってその中にモジュールを記述しよう。例えばディレクトリの構成は次のようなカンジだ。 customsearch | |-engine | | | |-SolrSearch.php | |-config.php |-Events.php |-module.json |-Module.php さて、config.phpとEvents.phpの中身だが・・・フックするEventはない。そのため、記述内容は必須事項以上のものがなく、最低限の形で記述した。 サーチエンジンの指定 HumHubでは、Solrサーチエンジンは、HumHub/protected/config/common.php のcomponentsセクションに'search'アレイを記述して指定する。 https://www.humhub.com/en/marketplace/solr/ デフォルトでは、この'search'アレイ自体存在していないか、'search'アレイの中の'class'キーのみ、'humhub\modules\search\engine\ZendLuceneSearch'と値が記述して存在していている(ハズ)。そこで、それを参考にして、Searchエンジンをカスタムモジュールのものに仕向けることにした。 'components' => array ( .... 'search' => array ( 'class' => 'humhub\modules\customsearch\engine\SolrSearch', 'host' => 'solr', 'port' => 8983, 'path' => '/', 'core' => 'humhubcore', ), .... ), host,port,path,coreは、solariumに渡すオプション設定値だ(設定値の利用については後述)。 サーチエンジンの設定 HumHubのデフォルトのサーチエンジン /humhub/protected/modules/search/engine/ZendLuceneSearch.php を解読しながらカスタムサーチエンジンを作っていこうと思う。まず、このファイルに記載されている ZendLuceneSearch class は、 Search class (/humhub/protected/modules/search/engine/Search.php)の拡張(extends)だ。Search class は、component class の extends による abstract class として宣言されている。いくつかの定数と、find, add, update, delete, flush の 4つの abstract public function と rebuild, optimize, getMetaInfoArray, getDocumentType, setDefaultFindOptions, getAdditionalAttributes の 6つの protected function をもっている。 サーチエンジンの実装 abstract function には次の解説がついていた。 /** * Retrieves results from search * * Available options: * page * pageSize * * sortField Mixed String/Array * model Mixed String/Array * type Mixed String/Array * checkPermissions boolean (TRUE/false) * limitSpaces Array (Limit Content to given Spaces( * filters Array (Additional filter Field=>Value) * * @param type $query * @param array $options * @return SearchResultSet */ abstract public function find($query, Array $options); /** * Stores an object in search index. * * @param Searchable $object */ abstract public function add(Searchable $object); /** * Updates an object in search index. * * @param Searchable $object */ abstract public function update(Searchable $object); /** * Deletes an object from search. * * @param Searchable $object */ abstract public function delete(Searchable $object); /** * Deletes all objects from search index. */ abstract public function flush(); abstract public fungion は、これから作るモジュールで自前で実装する必要があるわけで、その必要な働きが記載してあることはありがたい。例えば、flush() には、 solr のコアに登録されているインデックスを全部消すコマンドを書けばいいわけだ。solr の扱いについては別の機会に述べるとして今回は省略することとするので一部の読者には申し訳ないが、いきなりsolariumでの処理を考えるとこんなふうになるだろう。 public function flush() { $adapter = new \Solarium\Core\Client\Adapter\Http(); // or any other adapter implementing AdapterInterface $eventDispatcher = new \Psr\EventDispatcher\EventDispatcherInterface(); $config = array( 'endpoint' => array( 'localhost' => array( // protected/config/common.php で記載した値を引用するように作成するクラス変数 'host' => $this->host, 'port' => $this->port, 'path' => $this->path, 'core' => $this->core, ) ) ); // create a client instance $client = new Solarium\Client($adapter, $eventDispatcher, $config); // get an update query instance $update = $client->createUpdate(); // add the delete query and a commit command to the update query $update->addDeleteQuery('*:*'); $update->addCommit(); // this executes the query and returns the result $result = $client->update($update); } ・・・よめる、よめるぞ私にも!ふあははは・・・と、そこへ冷静なツッコミが。 そりゃ、こいつは簡単だよ、なりそこないの天上人よ。 HumHub側から何の入力もなく、solrに対して何か情報を渡すわけでもなく、solrの特定のコアのインデックスを削除すればいいのだから。 ・・・とはいえ、これで一つ、solariumの使い方もわかった。\Solarium\Clientインスタンスを作って、そのquery instanceを使ってあれこれ操作するようだ。よーし・・・残りの、delete, update, add, find もなんとか実装していくぞ! (後編へ続く)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む