PHP面接対策

PHP 面接問題

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

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

初級向け質問

1PHPの弱い型付けと厳密な型付けはどう異なり、declare(strict_types=1)で何が変わりますか?

PHPは通常、宣言された引数・戻り値の型に対して互換性のあるスカラー変換を許可します。例えばint引数へ数値文字列を渡せます。declare(strict_types=1)は厳密なスカラー検証を有効にし、互換性のない引数や戻り値でTypeErrorを発生させます。ただしfloatにはintを渡せます。ユーザー定義関数の引数では呼び出し元ファイルが厳密さを決め、戻り値の検証では定義元ファイルが決めます。PHP内部関数からの呼び出しには独自の注意点があり、外側の呼び出し元のstrict_typesだけで決まるわけではありません。宣言はPHPファイルの先頭に置く必要があります。PHPを静的型付けにしたり、言語全体で演算子の型変換を無効にしたりするものではありません。

// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>

// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>

// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
    echo takesInt('123');
} catch (TypeError $e) {
    echo 'TypeError';
}
?>
AI コーチを使ってこの質問に答えてみる

2数値文字列、配列、オブジェクト、in_array()の注意点を含め、PHPの==と===の違いを説明してください。

==は必要に応じて型変換する緩い比較で、===はスカラー変換を行わず型と値の一致を要求します。数値文字列は数値として比較されることがあるため、型を含む厳密な一致が必要な値に緩い比較を使わないようにします。配列の==は、比較規則に従って同じキーと値の組を含めば順序を問いません。===は順序とキー・値の型も一致している必要があります。オブジェクトの==は同じクラスのオブジェクトのプロパティを比較し、===は同一インスタンスを要求します。in_arrayは既定で緩い比較なので、第3引数にtrueを指定すると厳密に比較します。数値と文字列の緩い比較はPHP 8で変わったため、バージョンを示さず古い型変換例に依存しないことが重要です。

<?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の戻り値型mixed、void、neverはどう異なり、何を保証しますか?

mixedはnullを含む任意の戻り値を許可します。voidは意味のある値を返さない宣言で、関数の末尾に達するかreturn;を使えますが、式を返すことはできません。void関数の通常の呼び出し結果はnullです。PHP 8.1以降のneverは、呼び出し元へ正常に制御を戻さないことを示します。例外の送出、exitによる終了、無限ループなどが可能で、最終的にプロセスを終了する必要はありません。never関数にreturn文を含めることはできません。重要なのは、voidは正常終了して呼び出し元が続行できる一方、neverには正常な継続がないという点です。

<?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とどう関係しますか?

iterableはarray|Traversableを意味します。GeneratorはIteratorを実装し、IteratorはTraversableを拡張するため、iterableを満たします。ユーザー定義クラスは、具体クラスでTraversableを直接実装するのではなく、IteratorまたはIteratorAggregateを通じて反復可能な動作を提供します。Iteratorは反復を制御するメソッドを公開し、IteratorAggregateはイテレーターを提供します。yieldを含むジェネレーター関数はGeneratorを返します。注意点として、foreachは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の可視性規則は、継承やトレイトとどう関係しますか?

publicメンバーは、オブジェクトやクラスにアクセスできる場所なら利用できます。protectedは宣言したクラスと関連する親子クラス階層から利用できますが、任意の外部コードからは利用できません。privateは宣言したクラス内で利用でき、子クラスは親のprivateメンバーを継承したインターフェースとして使えません。通常のオーバーライドメソッドは継承した可視性を狭められません。ただしコンストラクターには特別な継承規則があり、この規則の無条件な例にはできません。トレイトのメンバーは利用側のクラスに組み込まれ、その可視性規則に従います。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のトレイトはどう機能し、複数のトレイトが同じメソッドを定義する場合はどう解決しますか?

トレイトは水平的なコード再利用の仕組みです。クラスはuseでメンバーを取り込み、トレイト自体はインスタンス化できません。クラス自身のメソッドがトレイトのメソッドより優先され、トレイトのメソッドは継承した親メソッドより優先されます。複数のトレイトが同名メソッドを提供し、クラスのメソッドでも競合が解消されない場合は、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 コーチを使ってこの質問に答えてみる

中級向け質問

8PHPのスカラー型、nullable型、ユニオン型、交差型、DNF型は、インターフェースの契約をどう表し、いつ使いますか?

PHPの型宣言は受け取る値と返す値を表します。int、float、string、boolなどのスカラー宣言は値の分類を指定しますが、互換性のあるスカラー変換を許すかはstrict_typesと文脈にも依存します。?TまたはT|nullはnullを許可します。PHP 8.0以降のユニオン型はint|stringなどの選択肢を許可します。PHP 8.1以降の純粋な交差型は、Cacheable&JsonSerializableのように、列挙されたクラスやインターフェースの型をすべて満たすオブジェクトを要求します。int&stringは有効ではありません。PHP 8.2以降のDNF型(選言標準形)は、(Cacheable&JsonSerializable)|stringのように選択肢と括弧付きの交差を組み合わせます。不要な選択肢を増やさず、有効な入力を表す最も狭く理解しやすい契約を選びます。

<?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は、関数名の文字列、Closureオブジェクト、アクセス可能なメソッドへの参照、__invokeを持つオブジェクトなどの呼び出し可能な値を表します。Closureは具体的な呼び出し可能オブジェクトで、無名関数から、または名前付き関数・メソッドを変換して作れます。strlen(...)や$object->method(...)という第一級callable構文は、対象を呼び出さずにClosureを作成します。[$object, 'method']はアクセス可能なインスタンスメソッドを参照し、現行PHPの[ClassName::class, 'method']には適切なアクセス可能な静的メソッドが必要です。is_callableは該当スコープで呼び出せるかを確認します。callableは広い契約、Closureは特定の表現形式です。

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クライアントをより少数のデータベースサーバー接続へと多重化できます。長時間実行されるワーカーには、ヘルスチェック、アイドルタイムアウト・再起動・フェイルオーバー・プロセスフォーク後の再接続、セッション状態のリセット、および安全または冪等な処理のみを再試行するなど、明示的な接続ライフサイクルの管理が必要です。

Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child

Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
AI コーチを使ってこの質問に答えてみる

14PHPで読み取りレプリカ、読み書き分離、レプリケーション遅延、プライマリDBのフェイルオーバーをどう扱いますか?

SQLがSELECTで始まるかだけでなく、処理の意味に基づいてプライマリとレプリカの接続を分けます。ロックを伴う読み取り、書き込み、プライマリの整合性が必要なトランザクションはプライマリを使います。読み取り専用トランザクションは、整合性要件が許せば適切なレプリカを使えます。非同期レプリケーションには遅延があります。Laravelのstickyは、現在のリクエスト内で書き込み後の読み取りを書き込み接続に送ります。複数リクエストにまたがる固定時間の接続先固定は緩和策にすぎず、自分の書き込みを読めることを無条件には保証しません。保証するにはプライマリを読むか、必要な書き込み位置をレプリカが適用するまで待ち、タイムアウト時はプライマリへ切り替えます。プロキシの動作は異なり、RDS Proxyは読み取り・書き込み別のエンドポイントを使うため、各SQL文を汎用的に自動振り分けするものではありません。切り替え時は古い接続を破棄し、上限付きのバックオフで再接続します。commit応答の喪失は結果が不明な状態です。非冪等な書き込みを盲目的に再実行せず、冪等性キーを使うか結果を先に照合します。

<?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アプリケーションのDB移行やスキーマ変更を、無停止または最小限の停止で安全に展開するにはどうしますか?

段階的な展開中に新旧アプリケーションが共存できるよう、拡張してから古い構造を縮小する方式を使います。まず互換性のある構造を追加し、その後両方の表現を扱うコードを展開します。二重書き込みが必要なら整合性を保ち、より新しい書き込みを上書きしない、再開可能で速度制限付きの既存データ補完を設計します。コピーを検証し、読み取りを切り替えてから古いコード経路を廃止します。オンラインDDL(データ定義言語)は一部のブロッキングリスクを減らしますが、常にロック不要ではありません。DBとバージョン固有のロック、トランザクション制約、資源使用量、レプリケーションへの影響を確認します。例えばPostgreSQLのCREATE INDEX CONCURRENTLYには制約があり、失敗すると無効なインデックスが残る場合があります。古い列を削除するのは、旧インスタンスがなくなり、必要なロールバック期間が終了した後です。破壊的な縮小後は、削除済みデータを必要とするコードへの復帰が自動的に安全とは限りません。

<?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 コーチを使ってこの質問に答えてみる