ソムリエでエンジニアのブログ

ワインの事も書きたいけど基本エンジニア用

PHPでバックトレースを実施する

経緯

Zend frameworkを使ったシステムでGETリクエストに対して想定しない404画面が描写された。
対象のコントローラーには404になる処理は実装されておらず、ログをチェックするもリクエストに対応するコントローラー内を通っていない。
各コントローラーの継承元にログを差し込むと出力される....
どこから継承元が呼び出されているかを確認する為、バックトレース用のメソッドdebug_backtraceを使用し、仕様など調べたので備忘録として残しておく。

debug_backtraceとは...

PHPでバックトレースを生成する為の関数。関数がどこから呼び出されているかを把握する事ができる為、今回のような呼び出し元を調査してエラー原因を特定する時に重宝する。

使い方とサンプルコード

継承元コントローラー

<?php
class AbstractController {
    public function __construct() {
        var_dump(debug_backtrace());
        echo "AbstractController\n";
    }
}
?>

各コントローラー

<?php
require('./Abstract.php');

class SampleB extends AbstractController {
    public function __construct() {
        parent::__construct();
    }
    
    public function getClassName()
    {
        echo 'SampleB';
    }
}
?>
<?php
require('./Abstract.php');

class SampleB extends AbstractController {
    public function __construct() {
        parent::__construct();
    }
    
    public function getClassName()
    {
        echo 'SampleB';
    }
}
?>

呼び出しファイル

<?php
require('./SampleA.php');
$class = new SampleA();
$class->getClassName();
?>

出力

array(2) {
  [0]=>
  array(7) {
    ["file"]=>
    string(22) "/workspace/SampleA.php"
    ["line"]=>
    int(6)
    ["function"]=>
    string(11) "__construct"
    ["class"]=>
    string(18) "AbstractController"
    ["object"]=>
    object(SampleA)#1 (0) {
    }
    ["type"]=>
    string(2) "->"
    ["args"]=>
    array(0) {
    }
  }
  [1]=>
  array(7) {
    ["file"]=>
    string(19) "/workspace/Main.php"
    ["line"]=>
    int(3)
    ["function"]=>
    string(11) "__construct"
    ["class"]=>
    string(7) "SampleA"
    ["object"]=>
    object(SampleA)#1 (0) {
    }
    ["type"]=>
    string(2) "->"
    ["args"]=>
    array(0) {
    }
  }
}
AbstractController
SampleA

出力を確認すると、SampleA.php内で継承元のコンストラクタ処理が呼び出されている事を確認できる。
debug_backtraceを使用する事で、今回のエラーはルーティング競合で別のコントローラーが呼び出されている事が原因であることを特定する事ができた。
※メソッドにパラメータを持たせる事で出力内容を最適化できるので参考リンクを確認してください。

参考

www.php.net

【読書メモ】「技術書」の読書術

はじめに

気になったので「技術書の読書術」を読んでみました。
せっかくなので見返せるようにまとめておきます。

www.amazon.co.jp

本について

こちらの本は、技術書の「選び方」「読み方」「情報発信 & 共有のやり方」の3点について2人の筆者が実践する様々な手法を紹介したものです。
多くの方法が紹介されているので自分に合うものを見つけながら実践できると思います。

なぜこの本に興味をもったのか

エンジニアであれば技術のキャッチアップを様々な方法で実践しているかと思います。
自分の場合、体型的にまとめられている書籍は非常に有益だと考えており、日頃から技術書を読む機会が多くあります。
しかし、時間を投資して読んだ技術書をしっかり身にできているのかとういう疑問もあり、そのようなタイミングでこの書籍に出会ったので自分の読書方法をブラッシュアップしたいと考えこちらの書籍を手に取りました。

以降は書く章から気になった点をまとめていきます。

第一部 選び方

技術書の選び方について書かれた章では以下の項目が印象に残っています。

英語の技術書という選択肢

英語で書かれた技術書という選択肢は今までの自分にはなかったので選択肢が増えた気がします。(すぐに読むとはいっていない...) ハンズオン形式であれば、詳細は解らずともコードやスクリーンショットから読み取れるかと思うのでそのような書籍も選択肢に入れる事はありかと思うようになりました。

第二部 読み方

技術書の読み方についてはこの書籍で最もページを割いて紹介されています。
以下の4点がとくに気になった内容です。

あらゆる場面で「3」の発想を大切にする
  • 入門書・専門書・逆引きの視点が異なる本を3種類くらい読むと実務で使えるレベルになる
  • 入門書を3冊見比べる事で共通する重要な点や間違いに気づく事ができる
サンクスコストの考え方を取り入れる

その本を読む目的(ゴール)を明確にし、目的を達成したら他の部分は後回しにする。
自分は本を買うと隅から隅まで目を通したくなるタイプなので、本来の目的を見失わず読書する事で余分な時間をかける事なく学習を進められるかと思います。

同じ本を何度も読む事はあり
  • 一度目は流し読み。章、節などのタイトル部分と最初の数行をざっと眺める。
  • 二度目は手を動かしながら読む
  • 三度目は記録に残しながら読む 受験などでもよく言われる復習の重要性です。
    またこの方法がいいなと思ったのは、一度目の流し読みのフェーズで、全体像がまったく解らなければその書籍のレベルに達していない、逆に中身がほぼわかるのであれば簡単すぎる本というざっくりとした判断ができる点でも時間の節約ができる点です。
全部読む必要はない

限られた時間の中でスキルアップを図る為、この意識は重要かと感じます。書籍の中ではさらに読むページ、読まないページとして以下のような内容が紹介されていました。

  • 目次は読書の目的が明確でなく全体像を把握必要があれば読む。目的が既に明確であれば飛ばす
  • 謝辞は飛ばす
  • まえがき、あとがきは読むモチベーションを高める意味で読む

第三部 情報発信 & 共有

読む時に自分ならではの視点をもつ

無条件に情報を信じるのではなく、「自分なら」という視点をもって書籍を理解する事が重要。

これは読書に限った事ではないですね!

全体の感想

サンクスコストの考え方や、同じ本を複数回読む前提の読書を早速取り入れてみたいです!
情報発信 & 共有の意味も込めて今回は読書メモを書いてみました。
今後もたまにかけたらいいな...

(JavaScript)mapでcontinueできないので代替案を探す

はじめに

普段JSやTSで新しい配列要素を作成したい時にmapをよく利用します。
for~ofで回しながら先に用意している配列にpushしていく書き方よりも、配列を加工したい旨を明示でき、すっきり表現できると考えています。

何がしたいのか

全ての配列に対して加工する場合は問題ないですが、一部の値のみを対象に加工した配列を返却したい場面でmapは少し面倒な動作をします。

<仕様>

  • 管理者のユーザー名を「さん」をつけて収集したい

例)for~ofを利用した例

const users = [
    { name: 'sample1', age: 30, admin: true },
    { name: 'sample2', age: 20, admin: false},
];

let target = [];

for (const user of users) {
    if (!user.admin) continue;
    
    target.push(user.name + 'さん');
}


例)mapを利用した例

const users = [
    { name: 'sample1', age: 30, admin: true },
    { name: 'sample2', age: 20, admin: false},
];

const target = users.map(user => {
   // ここでcontinueしてスキップしたい、、、

    if (user.admin) {
        return user.name + 'さん';
    }
})

→ ['sample1さん', undefined]

※mapについては下記リンクを参照 developer.mozilla.org

仕様を実現しようとすると、管理者でないデータがundefinedで返却されてしまいます。
その為、map内でcontinueを利用したいとなるのですが、ご存知の通りmap内でcontinueを利用する事はできません。そこで少し工夫をして同様の動作をできるようにしてみたいと思います。

解決方法

filter関数を利用する事でundefinedが返却される問題に対応する事が可能になります。

const target = users
    .filter(user => user.admin)
    .map(user => user.name + 'さん');
→ ['sample1さん']

上記コードではfilterで管理者ユーザーのみ収集し、その後mapに収集したデータが渡されるので問題となっていたsample2ユーザーの判定時にundefinedが返却されるを解消できます。

PHPコンテナにLaravelスケジューラーの設定を追加するとnginxコンテナとの接続でエラーが出た

発生した事

cronLaravelアプリを同一コンテナに入れnginxと接続しようとすると502 Bad Gatewayが発生した。
nginx側のエラーログを確認すると以下のような内容が...

connect() failed (111: Connection refused) ... 続く

どのようなDockerfileを使っていたか

一部省略しますが以下のようなDockerfileを利用していました。

FROM php:8.0-fpm-buster


COPY ./config/cron/root /etc/cron.d/cron

RUN apt-get update \
    && apt-get -y install git libzip-dev wget cron \
    ... 省略 ...
    && chmod 0644 /etc/cron.d/cron \
    && touch /var/log/cron.log \
    && touch /var/log/sample.log

CMD cron

dockerにcronを登録する方法に関しては下記の記事を参考にさせていただきました。

zenn.dev

原因

CMDは1ファイル一回に一度しかかけず最後に記載のあるものが実行されるようです。
その為、php:8.0-fpm-busterで定義されているCMD ["php-fpm"]が実行されずこのようなエラーとなっているようです。

対応策

CMD ["php-fpm"]が実行されるようDockerfileを以下のように修正します。

FROM php:8.0-fpm-buster


COPY ./config/cron/root /etc/cron.d/cron

RUN apt-get update \
    && apt-get -y install git libzip-dev wget cron \
    ... 省略 ...
    && chmod 0644 /etc/cron.d/cron \
    && touch /var/log/cron.log \
    && touch /var/log/sample.log

CMD sh -c "cron && php-fpm"

上記でエラーを解決する事ができました。

SQL Alchemy, Alembicでマルチテナント型のデータベース生成とマイグレーションを実行する方法

概要

本業でPythonで処理を書き, ORMとしてSQL Alchemy, マイグレーションの管理をSQL Alchemy, で実施しているプロジェクトがあります。
そのプロジェクトではマルチテナント方式?(一つのユーザーに一つのスキーマーが切られているイメージです。)を採用しており、契約時に新規のスキーマー作成、マイグレーションの実施を行いたいニーズがありました。
ネットには、SQL Alchemy, Alembicを利用してテーブル作成を行う記事は存在したのですが、今回のようなデータベース自体の作成やマイグレーション先をコード上で動的に切り替える方法などは見つかりませんでしたので実装方法の殴り書きとして残しておきます。 (※注意)MariaDBを利用した為、スキーマではなく以後DBを作成と書かせていただきます。

前提条件

通常のマイグレーションは実施できる状態。 alembicのenv.pyで以下のように環境変数からDB関連の値を読み取ってurlを指定しています。

alembic/env.py

from env import DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME
from alembic import context
import models

... 略

db_url = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
config.set_main_option("sqlalchemy.url", db_url)

... 略

この場合、環境変数で指定したDB_NAMEというデータベースにマイグレーションが実行されます。
この時点では、サーバーに入り手動でDBを作成しalembicのマイグレーション実行コマンドを叩いていました。今回はこれをあるエンドポイントを叩くと引数のDB名でDBを作成しマイグレーションまで実行してくれるように設定していきます。

実装

from env import DB_USER, DB_PASSWORD, DB_HOST, DB_PORT
from sqlalchemy_utils import create_database, database_exists
from sqlalchemy import create_engine
from alembic.config import Config
from alembic import command


def settingDatabase(database_name: str):
    DB_URL = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{database_name}"
    Engine = create_engine(DB_URL, encoding="utf-8")

    if database_exists(DB_URL):
        return False

    create_database(Engine.url)
    alembic_config.set_main_option("sqlalchemy.url", DB_URL)
    command.upgrade(alembic_config, 'head')

    return database_exists(DB_URL)

alembic_config = Config('./alembic.ini')

db.pyでURLを指定するようにしたのでalembicのenvファイルではURLの指定をコメントアウト
これが残っているとdb.pyで指定したURLが上書きされる為うまく動作しません。

alembic/env.py

from env import DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME
from alembic import context
import models

... 略

# db_url = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
# config.set_main_option("sqlalchemy.url", db_url)

... 略

あとは定義したsettingDatabaseメソッドを呼び出せば引数の値を名前にもつDBが作成されマイグレーションも実行されます。

最後に

Python、Alembic、SQL Alchemy全て今回初めて触る技術になりますのでもしかすると見当違いな実装になっているかもしれません。
何か別の方法があればぜひご教授いただけますと幸いです。

ブラウザ→webサイトに接続できなくなった... そんな時のwebエンジニア向けトラブルシューティング

お疲れ様です!おっくんです。
2022年も早1ヶ月半ほどたちましたねー。毎日が早すぎる...

記事執筆のきっかけ

私は現在、エンジニア数10名以下のベンチャー企業で勤務しています。
あるあるですが、インフラをメインで触っているエンジニアの数が少なく、インフラにおけるトラブル解決のスピードが課題となっている状況です。

つい先日、新規案件のステージング環境でURLにアクセスしても画面が表示されないトラブルが発生した際、ただ「webサイトが表示されません。」と報告するのではなく、「このような状態又は動作の為トラブルと判断した」と問題を切り分けてからインフラエンジニアに依頼をかける必要性を感じ、簡単なトラブル切り分け方法を調べたのでまとめておきたいと思います。

想定しているケース

本記事では運営している「http://www.okkun-sample.com/」というwebサイトが突然ブラウザから接続できなくなってしまったというシュチュエーションを想定しています。
クライアントのOSはMac、Webサイトは、Webサーバとその裏にあるDBサーバで構築されている前提で進めます。

記事の対象者

  • 初めてエンジニアとして働くことになった方
  • インフラ周りに苦手意識をもっているwebエンジニアさん

手順

1. クライアント側のネットワーク問題を疑う

まずはクライアント側のネットワークに問題がないか調べていきます。
といっても簡単でブラウザから「http://www.okkun-sample.com/」以外のサイトに正しくアクセスできる事を確認します。

2. ホスト名解決がされているか調査する

nslookup又はdigコマンドを利用してDNSサーバに名前解決を確認します。
このnslookupコマンドはDNSサーバに対して名前解決リクエストを送信するコマンドです。

$ nslookup www.okkun-sample.com

** server can't find www.okkun-sample.com

IPアドレスが返却されない場合は、DNSの設定、もしくはDNSサービス自体に問題があります。

3. Webサーバとの疎通を調査する

pingコマンドでWebサーバとのネットワーク疎通を確認します。

$ ping www.okkun-sample.com(IPアドレスでも可)

Webサーバからの応答が返ってこない場合は、途中のネットワークに問題があるか、Webサーバがダウンしている可能性があります。

※注意点

pingコマンドにはICMPというプロトコルが利用されます。その為、サーバ側でICMPが許可されていない場合は、pingから応答は返ってこないがサーバは正しく動作している場合もあります。

4. ポート番号が閉じられていないか調査する

telnetコマンドを利用してHTTPのウェルノウンポートである「80番」に接続してみます。

$telnet www.okkun-sample.com 80

Webサーバから応答が返ってこない場合は、ポートがセキュリティ設定などで閉じられている可能性があります。

5. アプリケーション側の問題を疑う

上記①~④までのチェックをおこなった上で接続が確認できない場合は、アプリケーション側での問題である可能性が高くなります。

所感

Webサイトに接続できない場合もアプリケーションのエラー解決と同じく、一つ一つ可能性を潰し、根本原因を探し当てるプロセスは変わりません。
今までインフラ関連は難しいと先入観をもってしまい中々手を出してきませんでしたが、いざ触ってみると面白く、さらに勉強していきたいと感じています。
もっといい方法あればコメントにてご教授いただけますと幸いです。

Laravelで用意したファイルを使ってテストする

はじめに

CTIのサービスに携わっている為、音声ファイル関連の機能に関わる事が多く自動テストで音声ファイルを扱う機能をカバーしたいと考えていました。


初期実装では、UploadedFile::fake()を利用して以下のようにダミーデータ作り対応していました。

<?php

use Illuminate\Http\UploadedFile;


$record_file = UploadedFile::fake()->create(
    name: 'record-file-sample.wav',
    mimeType: 'audio/x-wav'
 );

しかしダミーデータでは、ファイルサイズチェックなどの一部メソッドのテストが上手くいかなかった為(この辺りの調査は必要かもしれない)、自分で用意したテストデータを持つファイルをPOSTする方法を調べたのでまとめておきます。

環境

Laravel8系

実装方法

1, テスト用の音声ファイルを用意する

今回はテスト用データとして利用する場面が多くなる事を想定し、テスト用ファクトリディレクトリを作成。そこに該当データを置きます。


f:id:sommelierEngineer:20220130082441p:plain

2, Illuminate\Http\UploadedFileオブジェクトを生成

<?php
use Illuminate\Http\UploadedFile;


dummy = new UploadedFile(
    './tests/Factory/File/sample-test.wav',
    'record-file-sample.wav',
    'audio/x-wav',
    null,
    true,
);


補足

UploadFileのコンストラクタ引数についても確認しておきたいと思います。
このクラスはSymfony\Component\HttpFoundation\File\UploadedFileクラスを継承しているのでそちらを見るとコンストラクタが書かれています。

<?php

    /**
     * @param string  $path  ファイルへのパス
     * @param string  $originalName アップロードされたファイルのオリジナルファイル名
     * @param string|null $mimeType  PHP が提供するファイルの型。
     * @param int|null  $error  アップロード時のエラー定数 
     * @param bool        $test        テストモードが有効かどうか
     */
    public function __construct(string $path, string $originalName, string $mimeType = null, int $error = null, bool $test = false)
    {
        ...}

参考

stackoverflow.com