41
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PHPAdvent Calendar 2022

Day 15

名前空間をさっくり理解する

Last updated at Posted at 2022-12-15

名前、つけてますか?

PHPにはnamespace(名前空間)という言語機能があります。

原初のPHPにはなかったのですが、PHP 5.3くらいからあるので、まあ平安時代には成立していたということです。それ以前の時代は App_Http_Controllers_User のような _ 区切りの擬似名前空間が用いられていたことがありました。現在では App\Http\Controllers\User のような \ 区切りの名前空間が利用できます。

名前空間付きのコード

名前空間が見慣れないという方のためにnamespaceのあるコードとしてLaravelで自動生成したControllerファイルの例を先に出しておきます。

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use App\Http\Requests\StoreBookRequest;
use App\Http\Requests\UpdateBookRequest;

class BookController extends Controller
{
    /**
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        //
    }
}

アプリケーションにAppなんて漠然とした名前をつけるなんて蛮行が許されるのかは議論のあるところですが、アプリケーションはライブラリではなく独立して成立するものなので、まあ許されるのでしょう。

PHPマニュアル曰く

namespaceが何かということを知りたければPHPマニュアルを読むのがよいでしょう。

名前空間とは何でしょう? 広義の「名前空間」とは、項目をカプセル化するもののことです。 これは多くの場面で見られる抽象概念です。 たとえば、たいていの OS はディレクトリでファイルをグループ化します。 この場合、ディレクトリがその中のファイルの名前空間として機能しています。 具体的に言うと、foo.txt というファイルは /home/greg/home/other の両方に存在することが可能ですが、それらふたつの foo.txt を同じディレクトリに配置することはできません。 さらに、/home/greg ディレクトリの外から foo.txt にアクセスするには、ディレクトリ名をファイル名の前につけて /home/greg/foo.txt としなければなりません。 プログラミングの世界における名前空間も、この延長線上にあります。

まあうん、わかるようなわからないような……。とりあえず、/home/greg/foo.txt/home/other/foo.txt はどちらも foo.txt というファイル名ですが別々に存在できるということは読み解けます。

namespaceで定義されたものを使うにはuse、つまりインポートという機能を使うのだということもPHPマニュアルに書いてあります。

外部の完全修飾名をエイリアスで参照したりインポートしたりする機能は、 名前空間において非常に重要なものです。 これは、Unix 系のファイルシステムでファイルやディレクトリへのシンボリックリンクを作成することに似ています。

PHP は定数、関数、クラス、インターフェイス、トレイト、列挙型(Enum)、名前空間のエイリアスやインポートをサポートしています。

エイリアス作成には use 演算子を使用します。 ここに、5 種類すべてのインポート方法の例を示します。

ちょっと待って、インポートって何? includeとは違うの? という疑問は解決できたでしょうか。

このマニュアルを読んで完全に理解できたという方はここでこのページを閉じていただいて、あとは楽しい年末を心待ちにするだけです。

メリークリスマス! :tada:
:santa_tone2:「よいお年を!」

日本人の名前で学ぶ名前空間

私、クリスマスとかいう海外のお祭りはしっくりこないんです ということで説明を続けます。

PHPの名前空間は名字のようなものだということで説明ができます。

<?php

namespace 山田;

ファイルの冒頭でこのような記述があるファイルは山田家だと思ってください。

script.php
<?php

namespace 山田;

$man = new 一郎();

おもむろに一郎を呼び付けます。「おい一郎!」

山田家で一郎といえば誰でしょうか。

そうです、われらが山田家の長男「山田\一郎さん」のことを指すに決まっています。みんな知ってるね。

えっ知らない? 仕方がないので定義してあげましょう。

src/一郎.php
<?php

namespace 山田;

class 一郎
{
}

念のためディレクトリの構造も示しておきます。

yamada
├── script.php
└── src
    └── 一郎.php

1 directory, 2 files

script.phpから山田\一郎さんのクラスを読み出すにはどうすればいいのでしょうか。

むかしながらのPHPではこのようにしていました。

script.php
 <?php
 namespace 山田;
 
+require_once __DIR__ . '/src/一郎.php';
 
 $man = new 一郎();

この構造の問題はなんでしょうか。

……そうですね、家族が増えていくたびにファイルのrequireを増やしていかなければいけません。

script.php
 <?php
 namespace 山田;
 
 require_once __DIR__ . '/src/一郎.php';
+require_once __DIR__ . '/src/二郎.php';
+require_once __DIR__ . '/src/三郎.php';

 $man = new 一郎();
+$man2 = new 二郎();
+$man3 = new 三郎();

ここは2022年、現代文明ではこのようなコードを見ることは非常にまれになってしまいました。

ではどうするかというと、現代的な環境ではComposerを使うことが一般的です。

Composerは「パッケージ」という単位でPHPスクリプトを扱うことができるツールです。一般的にはPackagistで公開されているフレームワークやライブラリをインストールするために使うのではないかと思いますが、Composerのすぐれている点はパッケージをインストールする対象も同じ「パッケージ」として管理対象にできるということです。

Composer用語で「Root Packageルートパッケージ」と呼びます。

Composerでインストールしたパッケージは一般的には個別のクラスをinclude/requireを書かなくても使えます。どうしてそれが実現できるのかというと、Composerがクラスローダーの機能を提供しているので、これを使うことでクラスがオートロードされるのです。

クラスのオートロードについて知りたい人のために「includeって書きたくない僕たちのためのオートローディングとComposer」という記事を書いたのですが、まあこれは後回しでよいでしょう。

この機能を使うためにはプロジェクトのルート直下にcomposer.jsonを用意して、このように定義してあげます。

composer.json
{
    "name": "zonuexe/yamada",
    "autoload": {
        "psr-4": {
            "山田\\": "src/"
        }
    }
}

このように書いてあげることで、このプロジェクトは山田さんの敷地であって、src/ディレクトリには「山田一家が整列している」ということを表しています。

ではさっそく、Composerの力を借りて一郎を呼び付けてやりましょう。Composerは composer install コマンドを実行することでvendor/ディレクトリ以下にファイルが生成されます。これらのファイルについてもご自身で読み解いていただくときっと興味深いことがわかると思うのですが、今回の記事では思考停止して単に使ってみましょう。

script.php
 <?php
 namespace 山田;
 
-require_once __DIR__ . '/src/一郎.php';
-require_once __DIR__ . '/src/二郎.php';
-require_once __DIR__ . '/src/三郎.php';
+require_once __DIR__ . '/vendor/autoload.php';

 $man = new 一郎();
 $man2 = new 二郎();
 $man3 = new 三郎();

注意
/vendor/autoload.php の読み込みはすべてのファイルに記述するのではなく、
個別に実行するスクリプトやWebの起動ファイル(public/index.php)にのみ記述します

おともだちを家に呼ぼう

せっかくなので田中さんの家を建てましょう。別のリポジトリを作ってもいいのですが、今回はめんどくさいので山田さんの家は広いので、敷地内に田中さんの家を建築させてもらうことにします。

other/tanaka以下は田中さんの家ということで、独立したcomposer.jsonを用意します。

other/tanaka/composer.json
{
    "name": "zonuexe/tanaka",
    "autoload": {
        "psr-4": {
            "田中\\": "src/"
        }
    }
}

そして、composer.jsonで以下のように書くことでローカルファイルシステムにあるパッケージに依存できます。

composer.json
 {
     "name": "zonuexe/yamada",
+    "repositories": [
+         {
+             "type": "path",
+             "url": "other/tanaka/"
+         }
+    ],
     "autoload": {
         "psr-4": {
             "山田\\": "src/"
         }
     },
+    "require": {
+        "zonuexe/tanaka": "dev-master"
+    }
 }

この状態で composer require zonuexe/tanaka:dev-master のように実行すると zonuexe/tanaka パッケージがインストールできてしまうはずです。

ディレクトリ配置も見てみしょう (折りたたみ)
yamada/
├── composer.json
├── composer.lock
├── other
│   └── tanaka
│       ├── composer.json
│       ├── composer.lock
│       ├── src
│       │   └── 太郎.php
│       └── vendor
│           ├── autoload.php
│           └── composer
│               └── ...
├── script.php
├── src
│   └── 一郎.php
└── vendor
    ├── autoload.php
    ├── composer
    │   └── ...
    └── zonuexe
        └── tanaka -> ../../other/tanaka/

無駄なことをしていると感じましたか?
このように同じGitリポジトリ内でComposerパッケージを分割することでモジュラモノリスに活用もできます

さて、script.php田中\太郎くんを召喚してみましょう。

方法はふたつあります。

  • use でインポートする
  • インポートせずフルネームで呼ぶ
script.php (インポートする場合)
<?php
namespace 山田;

use 田中\太郎;

require_once __DIR__ . '/vendor/autoload.php';

var_dump(new 太郎());
script.php (インポートしない場合)
<?php
namespace 山田;

require_once __DIR__ . '/vendor/autoload.php';

var_dump(new \田中\太郎());

構成に成功していれば、どちらのコードでも田中\太郎くんが呼び出せるはずです。

よく気をつけてほしいのは、use 田中\太郎;と書くと最後の\;の間、つまりそのファイル内では太郎と呼び捨てにできるということです。

以下のコードを考えてみてください。

<?php
namespace 山田;

require_once __DIR__ . '/vendor/autoload.php';

var_dump(new 田中\太郎());

このコードは期待通りに動きません。ここは山田さんの家だというルールが生きています。ここでインポートされずに呼ばれる名前というのは山田ファミリーの一員なので、つまり 山田\田中\太郎として名前解決されます

script.php (名前空間を部分的にインポートする)
 <?php
 namespace 山田;
 
+use 田中;
 
 require_once __DIR__ . '/vendor/autoload.php';
 
 var_dump(new 田中\太郎());

use 田中 を追加してあげることで、このファイル内では山田ファミリーの一員 ではない 田中 という野郎も居るのだと認められることになります。

では、田中くんの妹の田中\アーデルハイト\花子ちゃんも招待してあげることにしましょう。

<?php
namespace 山田;

use 田中\太郎;
use 田中\アーデルハイト\花子;

require_once __DIR__ . '/vendor/autoload.php';

var_dump(new 花子());

花子さんは名前があまり好きではないようで、「ハイジ」と呼んでほしいみたいです。

php
 <?php
 namespace 山田;
 
 use 田中\太郎;
-use 田中\アーデルハイト\花子;
+use 田中\アーデルハイト\花子 as ハイジ;
 
 require_once __DIR__ . '/vendor/autoload.php';
 
-var_dump(new 花子());
+var_dump(new ハイジ());

これで無事にお気に召すように呼んで差し上げられます。

ここで重要なのは、use 田中\太郎;と書いた時点ではクラスを読み込んでいないということです。ではいつ読み込まれるかというと new 太郎() が評価されたときです。これはPHPとComposerが提供するオートロード機能によって読み込まれるのですが、PHPの特徴はそのクラスを使う瞬間までロードを遅らせることができます。すなわちLazy load遅延ロードです。

実際に必要になるまで遅延するということは、存在しないクラスをuseしても実行時にエラーになりません。
PhpStormや静的解析ツールなどできちんとコーディングミスがないようにチェックすることが重要です。

PHPの組み込みクラス(Exception, DateTimeなど)の多くはサブ名前空間に属していません。
つまり、この記事的にいうと名字がありません。名字なしのことをトップレベルといいます。

花子をハイジと呼び替えるのは極端な例ですが、Foo\Bar\Twitter\Clientというクラスを使うことを考えてみましょう。

use Foo\Bar\Twitter\Client;
use Foo\Bar\Twitter\Exception;

try {
    $twitter_client = new Client();
    $twitter_client->request(...$args);
} catch (Exception $e) {
    log($e);
}

このコードは十分に小さいといえますが、複数のClientを同じ文脈で扱うとなると多少混乱を呼ぶかもしれません。このようなコードは use as であえて冗長な名前を付けることでファイル内で意味が明確になります

php
-use Foo\Bar\Twitter\Client;
-use Foo\Bar\Twitter\Exception;
+use Foo\Bar\Twitter\Client as TwitterClient;
+use Foo\Bar\Twitter\Exception as TwitterException;

 try {
-    $twitter_client = new Client();
+    $twitter_client = new TwitterClient();
     $twitter_client->request(...$args);
-} catch (Exception $e) {
+} catch (TwitterException $e) {
     log($e);
 }

クラス定義の段階でFoo\Bar\Twitter\Clientのように重複のない短い名前をつけるべきか、Foo\Bar\Twitter\TwitterClient のように重複を許容しasが不要な明示的な名前をつけるべきか、私の中でも決着がついていません。皆さんはどう考えますか?

また、これは田中\太郎山田\太郎を同じファイルで活躍させるときにも有用です。

use Foo\Bar\Twitter\Client as TwitterClient;
use Hoge\Piyo\Http\Client as HttpClient;
use 田中\太郎 as 田中太郎;
use 山田\太郎 as 山田太郎;

この方法はこれから定義するクラスと継承元のクラスが(namespace違いの)同名である場合にも有用です。

namespace App;

use PHPUnit\Framework\TestCase as BaseTestCase;

abstract TestCase extends BaseTestCase
{
}

インポートの対象

クラス・トレイト・インターフェイス

ここまで使ってきたように use はクラスをインポートしますが、トレイトインターフェイス列挙型(enum)も基本的にはクラスと同様の振る舞いをします。さらにはuseは名前空間の一部分もインポートできます。

以下のインポートとインスタンス化はすべて合法です。

<?php
namespace 山田;

use 田中;                               // インポート(1)
use 田中\アーデルハイト;                // インポート(2)
use 田中\アーデルハイト\花子;           // インポート(3)
use 田中\アーデルハイト\花子 as ハイジ; // インポート(4)

require_once __DIR__ . '/vendor/autoload.php';

var_dump(new \田中\アーデルハイト\花子()); // インポートなし
var_dump(new 田中\アーデルハイト\花子());  // インポート(1)
var_dump(new アーデルハイト\花子());       // インポート(2)
var_dump(new 花子());                      // インポート(3)
var_dump(new ハイジ());                    // インポート(4)

ところで、PHPではクラス名の文字列からインスタンス化したり、DIコンテナにクラス名の文字列を渡して実体と引き換えるパターンがよくあります。また、PHPUnitのexpectExceptionメソッドで送出されることが見込まれるクラス名を渡したりすることもあります。

PHPではクラス名::classという記法(マジック定数)でクラス名の文字列に展開されます。

var_dump([
    \田中\アーデルハイト\花子::class,
    田中\アーデルハイト\花子::class,
    アーデルハイト\花子::class,
    花子::class,
    ハイジ::class,
]);
// => すべて "田中\アーデルハイト\花子" に展開される

これらの仕様によってインポートですっきりとクラス文字列を生成できます。

try-catch

以下のようなコードは初心者が犯しがちなミスです。

<?php
namespace 山田;

try {
    query();
} catch (PDOException $e) {
    log($e);
}

このファイル内で単にPDOExceptionと書くと、それは山田\PDOExceptionを意味してしまいます。そんなクラスは普通定義しないので、本物のPDOExceptionをキャッチするには以下のどちらかの変更を加える必要があります。

  • use PDOException; を追加する
  • catch (\PDOException $e) のように先頭に \ をつける

繰り返しますが、存在しないクラスをcatchしても実行時にエラーにならず、単に例外を素通しします。
PhpStormや静的解析ツールなどの警告を無視せず、きちんと対処することが重要です。

定数・関数

PHPではクラスとは別にnamespace以下に関数や定数を定義できます。

<?php
namespace Name\Sub;

const 定数 = __NAMESPACE__;

function 関数(): string {
    return __FUNCTION__;
}

class クラス {
    const クラス定数 = __CLASS__;

    public static function メソッド(): string {
        return __METHOD__;
    }
}

var_dump([
    \Name\Sub\定数,
    \Name\Sub\関数(),
    \Name\Sub\クラス::class,
    \Name\Sub\クラス::クラス定数,
    \Name\Sub\クラス::メソッド(),
]);
つまり、やろうと思えば以下のようなこともできます (折りたたみ)
<?php
namespace Name\Sub;

const クラス = クラス::class;

function クラス(): クラス {
    return new クラス();
}

class クラス {
    public static function new(): self {
        return new self();
    }
}

var_dump([
    new (\Name\Sub\クラス),
    \Name\Sub\クラス(),
    \Name\Sub\クラス::new(),
    new \Name\Sub\クラス(),
]);

あまりメリットがないから普通にコンストラクタを呼べばいいと思いますが…

注意
クラス類とは異なり、関数や定数はオートロードできません。
composer.json から autoload.files として読み込むか、直接読み込む必要があります。

namespace内で関数や定数が定義できるということは、もちろんPHP標準のものと同じ名前の関数や定数も定義できてしまいます。

そのため、トップレベル以外のnamespace宣言されたファイル内で関数呼び出しをすると、「現在の名前空間に関数(定数)が定義されているか」「トップレベル名前空間に関数(定数)が定義されているか」と呼び出しの都度2度探索するので若干パフォーマンスが落ちることがあります。

特に標準関数を呼び出すときは use function を書くか、関数呼び出し時に \ を先頭につけることでパフォーマンス的にはやや有利になります。

特に一部の関数は、ただの関数呼び出しではなく専用命令に置き換えられることがあります

一方で、ボトルネックになるような箇所でなければ望むようなパフォーマンス改善効果も出ないでしょう

まとめ

いままで口頭で名前空間を「名字」による説明をしていたので文章にまとめました。非常に雑に思いつくまま書き殴ったので書いておくべきことがいろいろ漏れているような気がするのですが、これを読むだけでPHPのnamespaceを6割くらい完全マスターできるのではないでしょうか。

関連記事

あとなんかむかしいろいろ書いていた気もするので、起きて思い出したらなんか追記していきます。

41
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?