PHP面接対策

PHP 面接問題

レベル別にまとめた、選りすぐりの PHP バックエンド面接問題 15問。基礎、実践上のトレードオフ、シニアレベルの本番運用での判断を確認できます。

PHPのAI面接を開始クレジットカードは不要です。1回分の無料セッションがあります。
英語での技術面接練習非ネイティブ話者が技術面接の合格を目指して練習できるモードです。

初級向け質問

1PHPの型システムは弱い型付け(weak typing)と厳格な型付け(strict typing)をどのように区別し、`declare(strict_types=1)` を有効にすると何が変わりますか?

PHPのスカラー型宣言はデフォルトで弱い型付け(暗黙的な型変換を許容する動作)になっています。関数がスカラー型の引数や戻り値の型を宣言している場合、PHPは型が一致しない値を拒否するのではなく、数値形式の文字列を数値に変換するなど、互換性のある値への型変換を試みます。 `declare(strict_types=1)` を指定すると、そのファイル内でのスカラー型のチェックが厳格(strict)になり、互換性のないスカラー値の暗黙的な型変換は行われず、`TypeError` がスローされます。このディレクティブの適用範囲はファイル単位(file-scoped)です。ユーザー定義関数の引数については呼び出し側のファイルの設定が厳格性を制御し、`return` 文については関数が定義されているファイルの設定が戻り値の厳密性を制御します。このディレクティブによってPHPがグローバルに厳格化されるわけではなく、言語内のすべての型変換が無効化されるわけでもありません。

<?php
// lib.php
function takesInt(int $x): int {
    return $x;
}

// weak.php, no strict_types
echo takesInt('123'); // coerces string to int

// strict.php
declare(strict_types=1);
echo takesInt('123'); // TypeError
AI コーチを使ってこの質問に答えてみる

2PHP (PHP: Hypertext Preprocessor) における == と === の違いについて、数値形式の文字列、配列、オブジェクト、および in_array() における注意点を交えて説明してください。

`==` は緩やかな比較(loose comparison)を行うため、PHPは比較前に型変換(型ジャグリング)を行う場合があります。一方、`===` は厳密な比較(strict comparison)を行い、型変換を行わずに値と型の両方が一致していることを要求します。 数値形式の文字列(numeric strings)は、緩やかな比較では数値として扱われる可能性があるため、一般的な落とし穴となります。 配列とオブジェクトにはそれぞれ固有の比較規則があります。配列はキーと値によって比較されます。オブジェクトの場合、`==` は同じクラスのインスタンス同士でプロパティが一致しているかを比較しますが、`===` は全く同一のインスタンスであることを要求します。 `in_array()` はデフォルトで緩やかな比較を使用するため、型を一致させる必要がある場合は第3引数に `true` を渡します。

<?php
var_dump(123 == '123');      // loose: numeric string is converted
var_dump(123 === '123');     // strict: int !== string

$values = [0, 1, 2];
var_dump(in_array('0', $values));       // loose
var_dump(in_array('0', $values, true)); // strict
AI コーチを使ってこの質問に答えてみる

3PHP (Hypertext Preprocessor) の戻り値の型として mixed、void、never にはどのような違いがあり、それぞれどのような保証を提供しますか?

戻り値の型として、`mixed`、`void`、`never` はそれぞれ異なる保証を提供します。`mixed` は、関数が `null` を含む任意の下位の値を返す可能性があることを意味します。`void` は、関数が意味のある値を返すことを意図していないことを示します。`return;` と記述することは可能ですが、式を返すことはできません。`never` は、関数が呼び出し元に正常に戻らないことを意味します。常に例外をスローするか、スクリプトを終了(exit)させるか、あるいはその他の方法で制御フローを中断する必要があります。`never` が指定された関数の通常の呼び出しの後に処理が継続することはあり得ないため、`never` は `void` よりも強力な制約となります。

<?php
function parse(string $raw): mixed {
    return json_decode($raw, true);
}

function logMessage(string $message): void {
    error_log($message);
    return; // allowed
}

function fail(string $message): never {
    throw new RuntimeException($message);
}

logMessage('starting');
fail('stop');
echo 'unreachable';
AI コーチを使ってこの質問に答えてみる

4PHPにおける iterable とは何ですか。また、array、Traversable、Iterator、Generator とはどのような関係がありますか。

PHPにおいて、`iterable` は「`array` または `Traversable` を実装したオブジェクト」を意味する型宣言です。`Traversable` は `foreach` で反復処理可能なオブジェクトを示す基底マーカーインターフェイスであり、ユーザーランドのコードでは通常、`Traversable` を継承する `Iterator` または `IteratorAggregate` を実装します。`Generator` は `yield` を使用するジェネレータ関数から返されるオブジェクトです。これは `Iterator` を実装しているため、同時に `Traversable` でもあり、`iterable` が要求されるすべての場所で受け入れられます。

function printItems(iterable $items): void {
    foreach ($items as $item) {
        echo $item, PHP_EOL;
    }
}

printItems(['a', 'b']);

function letters(): Generator {
    yield 'x';
    yield 'y';
}

printItems(letters());
AI コーチを使ってこの質問に答えてみる

5PHPにおけるpublic、protected、privateの可視性(アクセス権)ルールとは何ですか。また、これらは継承やトレイト(trait)とどのように相互作用しますか?

PHPにおいて、publicメンバはどこからでもアクセス可能であり、protectedメンバは宣言されたクラスおよびサブクラスからアクセス可能で、privateメンバはそれを宣言したクラスからのみアクセス可能です。継承においては、サブクラスは継承されたpublicおよびprotectedメンバを使用できますが、親クラスのprivateメンバは継承されたAPIとしてアクセスできません。メソッドをオーバーライドする際、通常サブクラス側で可視性を下げることはできません。例えば、親クラスでpublicのメソッドは子クラスでもpublicのままである必要があります。トレイトはそれを使用するクラスに組み込まれ、トレイトのメンバはPHPの可視性ルールに従います。また、トレイトをインポートする際に`as`構文を使用して、トレイトメソッドの可視性を変更したりエイリアスを設定したりすることも可能です。

<?php
class Base {
    public function a() {}
    protected function b() {}
    private function c() {}
}

trait Logs {
    public function log(string $message): void {}
}

class Child extends Base {
    use Logs { log as protected writeLog; }

    public function test(): void {
        $this->a();              // OK
        $this->b();              // OK
        // $this->c();           // Not accessible: private to Base
        $this->writeLog('ok');   // OK inside class, protected alias
    }
}

$child = new Child();
$child->a();
// $child->b();           // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
AI コーチを使ってこの質問に答えてみる

6PHPにおけるトレイト(trait)の仕組みと、複数のトレイトが同名のメソッドを定義している場合の競合解決方法を教えてください。

トレイトはPHPにおける水平的なコード再利用のための仕組みです。クラスは1つ以上のトレイトを使用(use)でき、トレイトのメンバはそのクラスへと組み込まれます。トレイトは独立したクラスではないため、直接インスタンス化することはできません。複数のトレイトが同名のメソッドを提供している場合、PHPでは明示的な競合解決が必要です。`insteadof` 演算子を使用して、名前が重複しているメソッドのうちどのトレイトのメソッドを採用するかを指定します。また、`as` 演算子を使用することでトレイトのメソッドにエイリアスを作成したり、インポートしたメソッドの可視性を変更したりできます。なお、クラス自身のメソッドはトレイトのメソッドをオーバーライドし、トレイトのメソッドは継承元の親クラスのメソッドよりも優先されます。

<?php
trait JsonLogger {
    public function log(string $message): void {
        echo "json:$message\n";
    }
}

trait FileLogger {
    public function log(string $message): void {
        echo "file:$message\n";
    }
}

class Service {
    use JsonLogger, FileLogger {
        JsonLogger::log insteadof FileLogger;
        FileLogger::log as logToFile;
        JsonLogger::log as protected logJson;
    }
}

$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
AI コーチを使ってこの質問に答えてみる

7Pure Enum(純粋なEnum)とBacked Enum(値を持つEnum)を含むPHPのEnum(列挙型)とは何ですか。また、それらはドメインモデリングをどのように改善しますか?

PHPのEnum(列挙型)は、注文ステータスやユーザーロールのように、ある型が取り得る名前付きケースの閉じた集合を定義するものです。Pure Enumは名前付きケースのみを持ち、Backed Enumは各ケースに一意の文字列または整数値を付与するため、永続化、API連携、他システムとの相互運用に適しています。 Enumは、壊れやすい生の文字列、整数、定数を、型安全で自己文書化された値に置き換えることでドメインモデリングを改善します。PHPのEnumはメソッドを定義したりインターフェイスを実装したりすることも可能です。また、すべてのEnumで定義ケースの一覧取得がサポートされており、Backed Enumはスカラー値との相互変換をサポートしています。

enum OrderState
{
    case Draft;
    case Paid;
    case Shipped;
}

enum PaymentStatus: string
{
    case Pending = 'pending';
    case Complete = 'complete';
    case Failed = 'failed';
}

$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
AI コーチを使ってこの質問に答えてみる

中級向け質問

8スカラー型、nullable 型、直交型(union 型)、交差型(intersection 型)、および DNF (Disjunctive Normal Form) 型は PHP の API 契約をどのように表現し、それぞれどのような状況で使用すべきですか?

PHP の型宣言は、API が受け取る値や返す値を規定します。`int`、`float`、`string`、`bool` などのスカラー型は、単一のプリミティブな型のみが期待される場合に使用されます。`?T` や `T|null` と記述される nullable 型は、`T` 型の値または `null` のいずれかを許容します。`A|B` などの union 型は、複数の候補のうちいずれか1つを許容します。`A&B` などの intersection 型は、指定されたすべての型を満たすことを値に要求し、一般的に複数のインターフェイスを満たす必要がある場合に使用されます。DNF 型は、`(A&B)|C` のように union 型と intersection 型を組み合わせ、より複雑な契約を表現します。それぞれは、不要に広範または分かりにくい型を避け、実際の API の約束に合致する場合に使用すべきです。

<?php
interface Cacheable {}

class Report implements Cacheable, JsonSerializable {
    public function jsonSerialize(): mixed { return []; }
}

function findName(int $id): ?string {
    return $id > 0 ? 'Alice' : null;
}

function normalize(int|string $id): string {
    return (string) $id;
}

function store(Cacheable&JsonSerializable $item): void {
    // must satisfy both interfaces
}

function handle((Cacheable&JsonSerializable)|string $input): void {
    // either an object satisfying both contracts, or a string
}
AI コーチを使ってこの質問に答えてみる

9PHPにおけるcallable、Closure、第一級callable、インボーク可能オブジェクト、およびcallable配列の違いは何ですか?

`callable` は、PHPが呼び出し可能なあらゆる値を表す広範な型/規約であり、関数名の文字列、`Closure`、インボーク可能オブジェクト、または `[$object, 'method']` や `[ClassName::class, 'method']` のようなメソッド参照配列が含まれます。`Closure` は無名関数を表す具体的なオブジェクトであり、それ自体が呼び出し可能です。第一級callable構文(例: `$obj->method(...)` や `strlen(...)`)は、呼び出し可能な式から `Closure` を生成します。インボーク可能オブジェクトは、`__invoke()` を定義しているため呼び出し可能です。callable配列は、2つの要素でメソッドを参照する伝統的な形式です。実行時の呼び出し可能性の判定には `is_callable()` を使用できます。

class Greeter {
    public function hello(string $name): string { return "Hello $name"; }
    public function __invoke(string $name): string { return "Hi $name"; }
}

$g = new Greeter();

$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;

foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
    echo $cb('Sam'), PHP_EOL;
}
AI コーチを使ってこの質問に答えてみる

10PHP は整数のオーバーフロー、浮動小数点数の精度、および正確な十進数の計算をどのように処理しますか?

PHP の整数はプラットフォーム依存のサイズであり、上限・下限は `PHP_INT_MAX` などの定数によって公開されています。PHP はオーバーフロー後にネイティブな整数の演算結果を任意精度整数として自動的に保持しません。オーバーフローした整数演算は `float` に変換されるか、正確な整数のセマンティクスが失われる可能性があります。PHP の float は二進浮動小数点数値であるため、`0.1` などの多くの十進小数は近似値となります。正確な十進計算や金銭計算では float の使用を避け、適切な場合には最小通貨単位を整数で扱う手法や、BCMath などの固定精度/任意精度の十進ツールを使用してください。GMP は任意精度の整数演算に有用です。

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
AI コーチを使ってこの質問に答えてみる

11PHPのリファレンス(&)、コピーオンライト(COW: Copy-On-Write)、およびリファレンスが予期しない副作用を引き起こす仕組みについて説明してください。

PHPの変数は内部的にzval構造体によって表現されます。配列や文字列などの値に対して、PHPはコピーオンライト(COW: Copy-On-Write)を採用しています。`$b = $a` という代入では通常、いずれかの変数が変更されるまで同一の内部値を共有し、変更された時点でPHPが分離・コピーを行います。`&` を用いて作成されたリファレンスは、変数を同一コンテナへのエイリアスにするため、一方を変更すると他方も変更され、通常の代入で期待される分離の動作が行われなくなります。参照渡しを行うと、呼び出し先の関数から呼び出し元の変数を直接変更できます。リファレンスは特に `foreach ($array as &$value)` で予期しない副作用を引き起こします。ループ終了後もループ変数は配列の最後の要素への参照を保持したままになるため、`unset($value)` を行わずに後続の処理で同じ変数を再利用すると、誤ってその要素を上書きしてしまう可能性があります。

$a = [1, 2];
$b = $a;      // shared until modification
$b[0] = 99;   // separates; $a is unchanged
var_dump($a, $b);

$c =& $a;     // alias/reference
$c[0] = 42;   // modifies $a too
var_dump($a, $c);
AI コーチを使ってこの質問に答えてみる

12PHPにおいて、オブジェクトハンドル、代入、clone、__clone、シャローコピー(浅いコピー)、ディープコピー(深いコピー)はそれぞれどのように動作しますか?

PHPでは、オブジェクト変数には完全なオブジェクトのコピーではなく、オブジェクトへのハンドルが格納されます。`$b = $a` という代入はハンドルをコピーするため、両方の変数が同じオブジェクトを参照します。そのため、`&` による参照渡しを使わなくても、一方の変数を介してオブジェクトを変更すると他方にも反映されます。`clone $a` は元のオブジェクトのプロパティをシャローコピー(浅いコピー)した新しいオブジェクトインスタンスを作成します。プロパティ自体がオブジェクトである場合、明示的に複製しない限り、複製されたオブジェクトのプロパティも同じネストされたオブジェクトを参照し続けます。`__clone()` はシャローコピーの実行後に新しいオブジェクト側で呼び出され、IDのリセット、リソースのデタッチ、あるいはネストされたオブジェクトのディープコピー(深いコピー)を行う場所として使用されます。ディープコピーとは、共有すべきではないネストされた変更可能なオブジェクトを再帰的に複製することを意味します。

class Address {
    public function __construct(public string $city) {}
}

class User {
    public function __construct(public string $name, public Address $address) {}

    public function __clone() {
        $this->address = clone $this->address; // make nested object independent
    }
}

$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1;          // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;

$u3 = clone $u1;    // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
AI コーチを使ってこの質問に答えてみる

上級向け質問

13データベースの接続枯渇をどのようにトラブルシューティングし、PHP-FPM (FastCGI Process Manager) や長時間実行されるワーカー向けの接続管理をどのように設計しますか?

接続枯渇のトラブルシューティングでは、データベースの最大接続数と現在のセッション数をPHP全体の同時実行数と比較します。アクティブなPHP-FPMの子プロセスはそれぞれ独自のデータベース接続を保持できるため、複数のアプリケーションプール、ホスト、cronジョブ、キューワーカー、マイグレーション、デプロイが重なると需要が掛け算で増加します。データベースのセッションビュー、接続状態、トランザクション内アイドル(idle-in-transaction)セッション、スロークエリ、ロック待機、エラーログ、コネクションプールのメトリクス、そして直近のトラフィックやワーカー数の変動を調査します。 PHP-FPMの設計においては、`max_children` とホストごとの同時実行数をデータベースの許容量内に収め、リクエストスコープの短時間な接続およびトランザクションを利用し、迅速にコミットまたはロールバックを行い、低速な外部API呼び出し中に接続を保持しないようにします。PDOの持続的接続(Persistent connections)は接続オーバーヘッドを削減できますが、FPMプロセスごとに多数のアイドル接続が固定化され、セッション状態が残留するリスクがあるため慎重な判断が必要です。PgBouncerなどのコネクションプーラーやプロキシを導入すれば、アプリケーションの互換性がある場合に多数のPHPクライアントをより少数のデータベースサーバー接続へと多重化できます。長時間実行されるワーカーには、ヘルスチェック、アイドルタイムアウト・再起動・フェイルオーバー・プロセスフォーク後の再接続、セッション状態のリセット、および安全または冪等な処理のみを再試行するなど、明示的な接続ライフサイクルの管理が必要です。

Example:
- 6 application servers
- 40 PHP-FPM max_children per server
- 2 separate FPM pools that can hit the database

Potential concurrent PHP request DB connections = 6 * 40 * 2 = 480

If PostgreSQL max_connections is 300 and queues/cron/admin tools also connect,
connection exhaustion is expected during traffic spikes.
AI コーチを使ってこの質問に答えてみる

14PHPにおいて、リードレプリカ、読み書き分離(read/write splitting)、レプリケーション遅延、およびプライマリデータベースのフェイルオーバーをどのように処理しますか?

PHPでリードレプリカとフェイルオーバーを処理するには、読み取りと書き込みのトラフィックを分離し、レプリケーション遅延の影響を緩和し、フェイルオーバー時の接続切断を適切に管理する必要があります。 アプリケーション層では、データベース設定やミドルウェアによって読み取りクエリ(`SELECT`)をリードレプリカに、書き込みクエリ(`INSERT`、`UPDATE`、`DELETE`)をプライマリインスタンスにルーティングします(あるいは、ProxySQLやAWS RDS Proxyなどのデータベースプロキシを介して管理することも可能です)。 レプリケーションは非同期であるため、レプリケーション遅延によって古いデータが読み取られる可能性があります。「自分の書き込みを即座に読み取る(read-your-writes)」整合性を保証するには、アクティブなトランザクション内の読み取りをプライマリにルーティングし、書き込み直後は特定のユーザーセッションやリクエストライフサイクルにおいて後続の読み取りを一時的にプライマリへ固定します。 プライマリのフェイルオーバー中、最新のデータベースクラスタはレプリカを昇格させ、DNSやプロキシのエンドポイントを更新します。PHPでは、接続レイヤが接続切断や読み取り専用エラーを捕捉し、無効になった古いPDOソケットを破棄し、指数バックオフを伴う制御されたリトライを実施する必要があります。

<?php
// config/database.php
return [
    'mysql' => [
        'read' => [
            'host' => [env('DB_REPLICA_HOST_1'), env('DB_REPLICA_HOST_2')],
        ],
        'write' => [
            'host' => [env('DB_PRIMARY_HOST')],
        ],
        'sticky'    => true,
        'driver'    => 'mysql',
        'database'  => env('DB_DATABASE'),
        'username'  => env('DB_USERNAME'),
        'password'  => env('DB_PASSWORD'),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
    ],
];
AI コーチを使ってこの質問に答えてみる

15PHPアプリケーションにおいて、データベースマイグレーションやスキーマ変更をダウンタイムゼロまたは最小限に抑えて安全にデプロイするにはどうすればよいですか?

PHPアプリケーションでダウンタイムゼロのデータベースマイグレーションを実現するには、Expand-Contract(並行稼働)パターンとノンブロッキングなオンラインDDL(Data Definition Language)の手法を組み合わせ、スキーマ変更とアプリケーションコードのデプロイを疎結合にする必要があります。まずExpandフェーズでは、後方互換性のあるスキーマ変更を適用します(例: NULL許容カラムの追加、新規テーブルの作成、あるいはpt-online-schema-change、gh-ost、MySQL/PostgreSQLのネイティブオンラインDDLを使用した並行インデックス作成)。次に、新旧両方のスキーマ構造へ書き込み(デュアルライト)、読み出しはどちらからでも可能な状態をサポートするアプリケーションをデプロイします。その後、テーブルのロックやレプリケーション遅延を防ぐため、スロットリング制御を施したバックグラウンド処理のバッチによって過去データを非同期にバックフィル(データ補完)します。Contractフェーズでは、データ整合性を検証して読み出しを新しい構造へ完全に切り替えた後、コードデプロイによって古いスキーマへの参照を削除します。最後に、クリーンアップ用のマイグレーションでレガシーなカラムやテーブルを削除します。ローリングリリース中に安全なロールバックを可能にするには、各中間スキーマの状態が、旧バージョンと新バージョンの双方のアプリケーションと完全に互換性を維持している必要があります。

<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
    $table->string('full_name')->nullable()->after('name');
});

// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
    public function setNameAttribute($value) {
        $this->attributes['name'] = $value;
        $this->attributes['full_name'] = $value; // Dual write
    }
}

// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column
AI コーチを使ってこの質問に答えてみる