PHP

Q. PHPは大文字小文字を区別するか?

A. いろいろある

PHPにもさまざまな言語要素がありますが、実は大文字小文字についての取り扱いはさまざまです。ここでは2018年1月現在のPHP7.1以下について言及します。

早見表

言語要素 大文字小文字を区別するか
文法・言語構造 区別しない
ファイル OS/ファイルシステム依存
函数 (名前空間) 区別しない
文字列比較 区別する
名前空間・クラス クラスローダー実装依存
変数 区別する
配列 区別するが、同一視する配列風クラスを定義可能
環境変数 区別する
プロパティ 区別するが、オーバーロードで同一視可能
メソッド 区別しないが、オーバーロードで区別可能
HTTPヘッダ 区別しない
goto/ラベル 区別する
定数 定義方法による(区別する定数としない定数がある)

文法

いきなりですがPHPの言語構造は基本的にすべてケースインセンシティブ—— つまり大文字小文字はどちらでも動きます

それは以下のようなコードも平然と動くことを意味します。 <?PhP と書こうが <?pHP と書こうが動きます。

<?pHP DECLARE(Strict_Types=1);

Require_Once getenv('HOME') . '/.composer/vendor/autoload.php';

Const FRUITES = ['apple', 'banana', 'orange', 'mikan'];

ForEach (FRUITES AS $f) {
    echo $f, ' ';

    If (is_citrus($f)) {
        EcHO '柑橘系', PHP_EOL;
    } ElseIF ($f === 'melon') {
        Echo '野菜', PHP_EOL;
    } ELSE {
        ECHO 'それ以外', PHP_EOL;
    }
}

Function is_citrus(String $fruit): BOOL
{
    Return in_array(strtolower($fruit), ['orange', 'mikan', 'mandarin']);
}

常識的に言って、業務コードなら言語構造は基本的に全部小文字で書いた方が良いです。

経験上わざとそんなことをするひとは少ないですが、ごくまれーに古いコードの地層から、打ち間違ったのか If とか AS とかが発掘されることがありました。もう残ってないといいなあ。

ちなみに僕が5年ほど前にPHPにふまじめにとりかかりはじめた頃は、わざとこんなコードを書いて遊んでました ヾ(〃><)ノ゙

ファイル

これは実はPHPは関係なく、ファイルシステム(OS)に依存します。

一般的にWindowsやmacOSのデフォルトのファイルシステムではファイル名はケースインセンシティブ(大文字小文字を区別しない)、GNU/Linuxではケースセンシティブ(大文字小文字を区別する)です。もうちょっと詳しくはファイルシステムと大文字小文字 - Qiitaみたいな感じ。

つまり以下のようなコードを動かしたときにどのような出力になるかは、ファイルを置いたファイルシステムとOSによります。

<?php

// ファイルの更新日時を更新、存在しない場合は新規作成
touch(__DIR__ . '/foo.txt');

var_dump([
    'foo.txt' => file_exists(__DIR__ . '/foo.txt'),
    'Foo.TXT' => file_exists(__DIR__ . '/Foo.TXT'),
]);

関数

関数名は組み込み関数、ユーザー定義関数ともに、ASCIIの範囲に限って大文字小文字を同一視します。つまり、以下のようなコードは動いたり動かなかったりします。

<?php

Var_Dump(MyFunc());

Var_Dump(myFunc2());
Var_Dump(MyFunc2());


function myfunc()
{
    return "called myfunc";
}

function myFunc2()
{
    return "called myfunc2";
}

私はPHP6のことはよく知らないのですが、もしPHPが本格的なUnicode言語に変革を成し遂げてたとしたらどうなってたのか、ちょっぴり興味が沸いてきますね ヾ(〃><)ノ゙

変数

ここまでとはうって変って、私が知る限りすべてのPHPバージョン(4.3.0以降)において、変数名は大文字と小文字を完全に区別します。

<?php

$a = 'a';

error_reporting(0);
var_dump($a, $A);

$A = 'A';
var_dump($a, $A);

文字列

PHPの文字列は実際にはバイト列なので、文字列どうしの比較において同一視されることはありません。つまり、以下のような等式が成り立つことはありません。

var_dump('A' === 'a');

大文字小文字の変換には mb_strtolower()mb_strtoupper()mb_convert_case() などを利用します。

これらの函数を利用することで大文字小文字を同一視させることができます。

var_dump(mb_strtolower($input, 'UTF-8') === 'apple');

また、これらのmb_函数はUnicodeの定義に従って、ASCIIの範囲外の文字も正常に大文字小文字変換をすることができます(当然に、いはゆる「全角英字」も含まれます)。

>>> mb_strtolower("MyFunc2", 'UTF-8')
=> "myfunc2"
>>> strtolower("MyFunc2")
=> "MyFunc2"

ちなみにmb_がつかないstrtolower()strtoupper()は実行環境のロケールと対象文字の組み合せによっては化けることがありうるので、私はおすすめしません。mb_を使ってください。

配列

配列も変数と同様で、大文字小文字は区別されます。ただし、ArrayAccessインターフェイスを実装することで大文字小文字を同一視した配列のようなものをユーザー定義することができます。 (実装は読者への課題とします)

プロパティ

基本的には変数・配列と同じで、大文字小文字は区別されます。こちらもユーザー定義でプロパティのオーバーロード (__get()__set()__isset() および __unset() )を実装することで大文字小文字を同一視することも可能です。あまりメリットはない気がしますが……。

メソッド

基本的には函数と同じで、ASCIIの範囲に限って大文字小文字を同一視します。ただし、メソッドのオーバーロードを実装することで意図的に大文字小文字を区別することもできなくはないです。プロパティ以上にメリットよりもデメリットが大きい気がしますが……。

環境変数

PHPから環境変数にアクセスする方法は2種類あります。

  1. スーパーグローバル変数 $_SERVER または $_ENV
  2. getenv() 函数

PHPはどちらにおいても大文字小文字は区別しません。

hoge=hoge Fuga=Fuga php -r'var_dump($_SERVER, getenv("hoge"), getenv("Fuga"));'

ただ、慣習的に環境変数は大文字にするなど統一を図った方が混乱は避けられるでせう。

名前空間とクラス

クラスについては少々話が厄介です。

基本的に名前空間とクラスは、関数と同様にASCIIの範囲で大文字小文字を区別しないことは確認ができます。

<?php

namespace Foo\Bar
{
    class Buz
    {
        public function print()
        {
            var_dump(__CLASS__);
        }
    }
}

namespace {
    (new foo\bar\buz)->print();
}

ところが実際には、いままで説明した事情が絡み合ってケースセンシティブのような挙動に見えることがあります。

典型的なクラスローダーの実装には「クラスマップ」と「ファイルシステム」の2種類があり、どちらもケースセンシティブに纏るトラブルを持ち込むことがありえます。

PHPのクラスを読み込む方法についてご存じではない型はincludeって書きたくない僕たちのためのオートローディングとComposer - Qiitaをお読みください。

ケース1: クラスマップ

クラスマップとは、クラスローダー自体が名前空間・クラス名とファイル名の対応表を持つ実装のことです。

spl_autoload_register(function ($class_name) {
    $map = [
        'Hoge' => '/Hoge.php',
        'Fuga\Piyo' => '/Fuga/piyo.php', // 本当は /Fuga/Piyo.php
    ];

    if (isset($map[$class_name])) {
        require_once __DIR__ . $map[$class_name];
    }
});

このクラスローダーは対応表の大文字小文字を間違ってるので、ファイルシステムによって動いたり動かなかったりします。定義に誤りがあるとどうしようもないので、通常はクラスマップ方式のクラスローダーは手書きはせず、Composerやオートロードビルダーを使って機械的に生成します。

次に定義上は Fuga\Piyo だが、実行時にどうしても new fugA\piYo のようにオートロードさせたい場合があるとします。このときは以下のようにクラスローダーを改良します。

spl_autoload_register(function ($class_name) {
    $map = [
        'hoge' => '/Hoge.php',
        'fuga\piyo' => '/Fuga/piyo.php', // 本当は /Fuga/Piyo.php
    ];

    $normalized_name = mb_strtolower($class_name, 'UTF-8');
    if (isset($map[$normalized_name])) {
        require_once __DIR__ . $map[$class_name];
    }
});

実はこの実装はPHP Autoload Builderが生成するクラスローダーのデフォルト実装と同様です。生成時に --nolower オプションを付けると、大文字小文字を意図的に区別するようになります。

また、Composerのクラスローダーのクラスマップは基本的に大文字小文字を区別するので注意してください。

ケース2: ファイルシステム

さて、ファイルを検索する原始的なSPLオートロード対応のクラスローダー関数は以下のように実装することができます。

spl_autoload_register(function ($class_name) {
    // 名前空間に含まれるファイルは対象外
    if (strpos($class_name, '\\') !== false) {
        return false;
    }

    $path = __DIR__ . DIRECTORY_SEPARATOR . "{$class_name}.php";

    if (file_exists($path)) {
        require_once $path;
    }
});

これは実際うまく動きますが、特定の環境でうまくいかないことがあります。それは開発環境がmacOSまたはWindows、本運用環境がGNU/Linuxであるような場合です。

以下の2つのファイルを適当なフォルダに配置して php test.php を実行してみてください。

test.php
<?php

spl_autoload_register(function ($class_name) {
    // 名前空間に含まれるファイルは対象外
    if (strpos($class_name, '\\') !== false) {
        return false;
    }

    $path = __DIR__ . DIRECTORY_SEPARATOR . "{$class_name}.php";

    if (file_exists($path)) {
        require_once $path;
    }
});

(new Foo)->print();
foo.php
<?php
class Foo
{
    public function print()
    {
        var_dump(__CLASS__);
    }
}

ふたつめのクラスはファイル名がfoo.php、クラス名がFooであることに気をつけてください。

さて、このコードはあなたの環境ではどう動きますか?

HTTPヘッダ

同じHTTPヘッダを送信しようとした場合は、基本的に後勝ちです。その際、大文字小文字は同一視されます。(これはRFC7230 の通りです)

header("A: aaaa");
header("a: bbbb");

var_export(headers_list());
echo PHP_VERSION;

さて、このスクリプトをPHP 7.1で「上の行をコメントアウトしたとき」「下の行をコメントアウトしたとき」「そのまま」で実行し、curl -XなどでHTTPヘッダを観測したとき、私はとてもおもしろい現象を観測することができました。結果は読者への課題とします。

goto/ラベル

gotoはPHPの歴史においては、かなり新しい部類に入る文法です。新しいだけあって、大文字と小文字を同一視するような誤ちは犯しませんでした。 なーんだつまんないの

<?php

goto Label; // undefined label 'Hoge'

hoge: echo "Jumped label;";

定数

今回の記事の暫定ラスボスは定数です。定数はPHPの言語仕様の中でも奇怪な挙動をするもののひとつです。PHPスクリプトから定数を定義する方法には3種類あることはご存じでせうか。

  1. const キーワード
  2. define()函数
  3. define()函数 ($case_insensitiveオプション)

constはクラス定数の定義にも利用するキーワードですが、クラス定義の外でも利用可能です。ただし実行時に動的に評価されるdefine()函数と違って、constは書ける文脈に制約があります。

<?php

const Foo = "const: Foo";

define('BAR', 'define: BAR');
define('Buz', 'define: Buz ($case_insensitive)', true);

// エラー報告レベルを最低にする
error_reporting(0);

var_dump([foo, Foo, FOO]);
var_dump([bar, Bar, BAR]);
var_dump([buz, Buz, BUZ]);

結果は以下のようになります。

array(3) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(10) "const: Foo"
  [2]=>
  string(3) "FOO"
}
array(3) {
  [0]=>
  string(3) "bar"
  [1]=>
  string(3) "Bar"
  [2]=>
  string(11) "define: BAR"
}
array(3) {
  [0]=>
  string(31) "define: Buz ($case_insensitive)"
  [1]=>
  string(31) "define: Buz ($case_insensitive)"
  [2]=>
  string(31) "define: Buz ($case_insensitive)"
}

define()函数の$case_insensitiveオプションをつけて定義したものだけが大文字と小文字を同一視して、constキーワードで定義した定数とオプションなしdefine()は大文字小文字を区別します。

以下の出力を見ると、確実にそのような挙動をすることを確信できますね。

var_dump([foo => defined('foo'), Foo => defined('Foo'), FOO => defined('FOO')]);
var_dump([bar => defined('bar'), Bar => defined('Bar'), BAR => defined('BAR')]);
var_dump([buz => defined('buz'), Buz => defined('Buz'), BUZ => defined('BUZ')]);

比較に入れ忘れましたが、クラス定数は常に大文字小文字を区別します。

定数についても語りたいことはさまざまありますが、基本的にはconstを利用するのが望ましいと筆者は考へます。 (暇だったらそのうち書きます)

まとめ

  • 大文字小文字を同一視するとしても、ふつうは定義と同じように書くべきだろjk
  • 構文(言語構造)に大文字小文字を混ぜて書くと見ためをはちゃめちゃにできるので、職場で先輩にまさかりで殴られやすくなります
  • 変数とか配列は昔からある機能ですが、大文字小文字をきっちり区別してくれるのでえらい子です
  • 比較的最近入った構文(gotoとかconstとか)は大文字小文字を同一視しない傾向にあります
  • MacとかWindowsのファイルの問題はRubyなんかでもふつうに起こるし、言語じゃなくてOSのせいです
  • PHPの実行時に制御できることを把握しておくと、PHP力は1000000000000倍になります
  • 最近ずっと咳のしすぎで夜は眠れません
  • おなかすいた
  • だいじなコミュニティのカンファレンスにはスポンサーすべき
  • なんかほかにもPHPのおもしろ要素を書き漏らしてる気がする。。。

私からは以上です。