4

ファイルシステム構造に依存してリクエストをディスパッチするルーティングメカニズムがあります。

function Route($root) {
  $root = realpath($root) . '/';
  $segments = array_filter(explode('/',
    substr($_SERVER['PHP_SELF'], strlen($_SERVER['SCRIPT_NAME']))
  ), 'strlen');

  if ((count($segments) == 0) || (is_dir($root) === false)) {
    return true; // serve index
  }

  $controller = null;
  $segments = array_values($segments);

  while ((is_null($segment = array_shift($segments)) !== true)
    && (is_dir($root . $controller . $segment . '/'))) {
      $controller .= $segment . '/';
  }

  if ((is_file($controller = $root . $controller . $segment . '.php')) {
    $class = basename($controller . '.php');
    $method = array_shift($segments) ?: $_SERVER['REQUEST_METHOD'];

    require($controller);

    if (method_exists($class = new $class(), $method)) {
      return call_user_func_array(array($class, $method), $segments);
    }
  }

  throw new Exception('/' . implode('/', self::Segment()), 404); // serve 404
}

.php基本的に、次のセグメントを実際のコントローラー(同じ名前のファイル)に一致させて、できるだけ多くのURLセグメントをディレクトリーにマップしようとします。より多くのセグメントが提供される場合、最初に呼び出すアクション(HTTPメソッドにフォールバック)を定義し、残りをアクション引数として定義します。

問題は、(ファイルシステムの構造に応じて)いくつかのあいまいさが存在することです。このことを考慮:

- /controllers
  - /admin
    - /company
      - /edit.php   (has get() & post() methods)
    - /company.php  (has get($id = null) method)

あいまいさ-コントローラーにアクセスdomain.tld/admin/company/edit/するedit.phpと(必要に応じて)リクエストが処理されますが、残りのセグメントにはファイルシステムにマッピングがない場合でも、セグメントが対応するディレクトリにマッピングされているため、domain.tld/admin/company/経由GETまたは直接アクセスするdomain.tld/admin/company/get/と404エラーがスローされます。companyこの問題を解決するにはどうすればよいですか?できれば、ディスクにあまり力を入れずに。

この問題に関してSOにはすでに多くの同様の質問があり、それらのいくつかを調べましたが、信頼できる効率的な解決策を提供する単一の答えを見つけることができませんでした。

4

3 に答える 3

3

このような重要なものについては、PHPUnit のようなテスト フレームワークを使用してテストを作成することが非常に重要です。

ここで説明されているようにインストールします (ナシが必要です): https://github.com/sebastianbergmann/phpunit/

また、仮想ファイル システムを使用して、テスト フォルダーが乱雑にならないようにします: https://github.com/mikey179/vfsStream/wiki/Install

Route 関数を というファイルにドロップしただけですRoute.php。同じディレクトリにtest.php、次の内容のファイルを作成しました。

<?php

require_once 'Route.php';

class RouteTest extends PHPUnit_Framework_TestCase {
}

すべてが機能するかどうかを確認するには、コマンド ラインを開き、次の手順を実行します。

$ cd path/to/directory
$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 1.50Mb

There was 1 failure:

1) Warning
No tests found in class "RouteTest".


FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

これが表示された場合、PHPUnit は正しくインストールされており、テストを作成する準備ができています。

Route 関数をよりテストしやすくし、サーバーやファイル システムとの結合を少なくするために、少し変更を加えました。

// new parameter $request instead of relying on server variables
function Route($root, $request_uri, $request_method) {
  // vfsStream doesn't support realpath(). This will do.
  $root .= '/';
  // replaced server variable with $request_uri
  $segments = array_filter(explode('/', $request_uri), 'strlen');

  if ((count($segments) == 0) || (is_dir($root) === false)) {
    return true; // serve index
  }

  $controller = null;
  $all_segments = array_values($segments);
  $segments = $all_segments;

  while ((is_null($segment = array_shift($segments)) !== true)
    && (is_dir($root . $controller . $segment . '/'))) {
      $controller .= $segment . '/';
  }

  if (is_file($controller = $root . $controller . $segment . '.php')) {
    $class = basename($controller . '.php');
    // replaced server variable with $request_method
    $method = array_shift($segments) ?: $request_method;

    require($controller);

    if (method_exists($class = new $class(), $method)) {
      return call_user_func_array(array($class, $method), $segments);
    }
  }
  // $all_segments variable instead of a call to self::
  throw new Exception('/' . implode('/', $all_segments), 404); // serve 404
}

インデックス ルートが要求された場合に関数が true を返すかどうかを確認するテストを追加しましょう。

public function testIndexRoute() {
    $this->assertTrue(Route('.', '', 'get'));
    $this->assertTrue(Route('.', '/', 'get'));
}

テスト クラスが拡張 されるため、特定のステートメントが true と評価されるかどうかを確認PHPUnit_Framework_TestCaseするなどのメソッドを使用できるようになりました。$this->assertTrueもう一度実行してみましょう:

$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.

.

Time: 0 seconds, Memory: 1.75Mb

OK (1 test, 2 assertions)

このテストに合格しました!array_filterが空のセグメントを正しく削除するかどうかをテストしましょう:

public function testEmptySegments() {
    $this->assertTrue(Route('.', '//', 'get'));
    $this->assertTrue(Route('.', '//////////', 'get'));
}

$rootルートのディレクトリが存在しない場合に、インデックス ルートが要求されるかどうかもテストしてみましょう。

public function testInexistentRoot() {
    $this->assertTrue(Route('./inexistent', '/', 'get'));
    $this->assertTrue(Route('./does-not-exist', '/some/random/route', 'get'));
}

これよりも多くのものをテストするには、メソッドを持つクラスを含むファイルが必要です。それでは、仮想ファイル システムを使用して、各テストを実行する前に、ファイルを含むディレクトリ構造をセットアップしてみましょう。

require_once 'Route.php';
require_once 'vfsStream/vfsStream.php';

class RouteTest extends PHPUnit_Framework_TestCase {

    public function setUp() {
        // intiialize stuff before each test
    }

    public function tearDown() {
        // clean up ...
    }

PHPUnit には、この種のもののための特別なメソッドがいくつかあります。setUpメソッドは、このテスト クラスのすべてのテスト メソッドの前に実行されます。そして、tearDownテストメソッドが実行された後のメソッド。

ここで、vfsStream を使用してディレクトリ構造を作成します。(これを行うためのチュートリアルを探している場合: https://github.com/mikey179/vfsStream/wikiはかなり良いリソースです)

    public function setUp() {
        $edit_php = <<<EDIT_PHP
<?php
class edit {
    public function get() {
        return __METHOD__ . "()";
    }
    public function post() {
        return __METHOD__ . "()";
    }
}
EDIT_PHP;

        $company_php = <<<COMPANY_PHP
<?php
class company {
    public function get(\$id = null) {
        return __METHOD__ . "(\$id)";
    }
}
COMPANY_PHP;

        $this->root = vfsStream::setup('controllers', null, Array(
            'admin' => Array(
                'company' => Array(
                    'edit.php' => $edit_php
                ),
                'company.php' => $company_php
            )
        ));
    }

    public function tearDown() {
        unset($this->root);
    }

vfsStream::setup()指定されたファイル構造とファイルの内容で仮想ディレクトリを作成するようになりました。ご覧のとおり、コントローラーがメソッドの名前とパラメーターを文字列として返すようにしました。

これで、テスト スイートにさらにいくつかのテストを追加できます。

public function testSimpleDirectMethodAccess() {
    $this->assertEquals("edit::get()", Route(vfsStream::url('controllers'), '/controllers/admin/company/edit/get', 'get'));
}

しかし、今回はテストが失敗します。

$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.

...
Fatal error: Class 'edit.php.php' not found in C:\xampp\htdocs\r\Route.php on line 27

$classしたがって、変数に何か問題があります。ここで、Route 関数の次の行をデバッガー (またはいくつかechoの s) で調べます。

$class = basename($controller . '.php');

$controller変数が正しいファイル名を保持していることがわかりますが、なぜ.php追加されているのでしょうか? これは入力ミスのようです。私はそれがあるべきだと思います:

$class = basename($controller, '.php');

これにより、.php 拡張子が削除されるためです。そして、正しい classname を取得しますedit

ここで、ディレクトリ構造に存在しないランダム パスを要求した場合に例外がスローされるかどうかをテストしてみましょう。

/**
 * @expectedException Exception
 * @expectedMessage /random-route-to-the/void
 */
public function testForInexistentRoute() {
    Route(vfsStream::url('controllers'), '/random-route-to-the/void', 'get');
}

PHPUnit はこのコメントを自動的に読み取り、Exceptionこのメソッドの実行時にタイプの例外がスローされたかどうか、および例外のメッセージが/random-route-to-the/void

これはうまくいきます。$request_methodパラメータが正しく機能するかどうかを確認してみましょう。

public function testMethodAccessByHTTPMethod() {
    $this->assertEquals("edit::get()", Route(vfsStream::url('controllers'), '/admin/company/edit', 'get'));
    $this->assertEquals("edit::post()", Route(vfsStream::url('controllers'), '/admin/company/edit', 'post'));
}

このテストを実行すると、別の問題が発生します。

$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.

....
Fatal error: Cannot redeclare class edit in vfs://controllers/admin/company/edit.php on line 2

include同じファイルに対して/をrequire複数回使用しているようです。

require($controller);

それをに変更しましょう

require_once($controller);

companyそれでは、問題に直面して、ディレクトリとファイルcompany.phpが互いに干渉しないことを確認するテストを書きましょう。

$this->assertEquals("company::get()", Route(vfsStream::url('controllers'), '/admin/company', 'get'));
$this->assertEquals("company::get()", Route(vfsStream::url('controllers'), '/admin/company/get', 'get'));

そして、質問で述べたように、ここで 404 例外が発生します。

$ phpunit test.php
PHPUnit 3.7.13 by Sebastian Bergmann.

.....E.

Time: 0 seconds, Memory: 2.00Mb

There was 1 error:

1) RouteTest::testControllerWithSubControllers
Exception: /admin/company

C:\xampp\htdocs\r\Route.php:32
C:\xampp\htdocs\r\test.php:69

FAILURES!
Tests: 7, Assertions: 10, Errors: 1.

ここでの問題は、いつサブディレクトリに入るべきか、いつ .php ファイルでコントローラーを使用するべきか、正確にはわからないことです。そのため、何をしたいのかを正確に指定する必要があります。そして、理にかなっているので、次のように仮定します。

  • コントローラーに要求されたメソッドが含まれていない場合にのみ、サブディレクトリを入力してください。
  • コントローラーにもサブディレクトリにも要求されたメソッドが含まれていない場合は、404 をスローします。

したがって、次のようにディレクトリを検索する代わりに:

while ((is_null($segment = array_shift($segments)) !== true)
  && (is_dir($root . $controller . $segment . '/'))) {
    $controller .= $segment . '/';
}

ファイルを検索する必要があります。要求されたメソッドが含まれていないファイルが見つかった場合は、ディレクトリを検索します。

function Route($root, $request_uri, $request_method) {
  $segments = array_filter(explode('/', $request_uri), 'strlen');

  if ((count($segments) == 0) || (is_dir($root) === false)) {
    return true; // serve index
  }

  $all_segments = array_values($segments);
  $segments = $all_segments;

  $directory = $root . '/';
  do {
    $segment = array_shift($segments);
    if(is_file($controller = $directory . $segment . ".php")) {
      $class = basename($controller, '.php');
      $method = isset($segments[0]) ? $segments[0] : $request_method;

      require_once($controller);
      if (method_exists($class = new $class(), $method)) {
        return call_user_func_array(array($class, $method), array_slice($segments, 1));
      }
    }
    $directory .= $segment . '/';
  } while(is_dir($directory));

  throw new Exception('/' . implode('/', $all_segments), 404); // serve 404
}

このメソッドは期待どおりに機能するようになりました。

これでさらに多くのテスト ケースを追加できるようになりましたが、これ以上拡張するつもりはありません。ご覧のように、一連の自動化されたテストを実行して、関数の一部が機能することを確認すると非常に便利です。エラーが発生した場所を正確に知ることができるため、デバッグにも非常に役立ちます。ここでは、コードを自分でデバッグできるように、TDD の実行方法と PHPUnit の使用方法について説明したいと思います。

「人に魚を与えれば、その人を一日養うことができます。人に釣りを教えれば、一生養うことができます。」

もちろん、コードを書く前にテストを書くべきです。

興味深いかもしれないいくつかのリンクを次に示します。

于 2013-02-14T11:51:57.223 に答える
2

あなたの魔法のHVMCの方法は開発者にとって便利ですが..それは少しパフォーマンスキラーになる可能性があります(すべての統計/l統計)。私は以前、FS をルートにマッピングする同様の方法を使用していましたが、後で魔法をあきらめて、古き良きハードコードされた構成に置き換えました。

$controller_map = array(
  '/some/route/' => '/some/route.php',
  '/anouther/route/' => 'another/route.php',
  # etc, etc, ...
);

おそらく、それはあなたが持っているものほどエレガントではなく、コントローラーを追加/削除するたびにいくつかの構成変更が必要になるでしょう (srsly、これは一般的なタスクではないはずです..) しかし、それはより速く、すべてのあいまいさを取り除き、取り除きます無駄なディスク/ページ キャッシュ ルックアップのすべて。

于 2013-02-19T16:14:20.463 に答える
1

申し訳ありませんが、ソリューションをテストする時間がありませんでしたが、ここに私の提案があります:

while ((is_null($segment = array_shift($segments)) !== true)
    && (is_dir($root . $controller . $segment . '/'))
    && ( (is_file($controller = $root . $controller . $segment . '.php') 
        && (!in_array(array_shift(array_values($segments)), ['get','post']) || count($segments)!=0 ) ) ) {
      $controller .= $segment . '/';
  }

上記のコードの簡単な説明は、ファイルとディレクトリの両方であるルートが見つかった場合、それがget / postによって成功したかどうか、またはそれが$segments配列の最後のセグメントであるかどうかを確認することです。そうである場合はファイルとして扱い、そうでない場合は$controller変数にセグメントを追加し続けます。

私が提供したコードサンプルは、単に私が念頭に置いていたものですが、テストされていません。ただし、比較でこのワークフローを使用する場合は、それを実行できるはずです。smasseyの回答に従い、各コントローラーのルートを宣言し続けることをお勧めします。

:*array_values*で*array_shift*を使用しているため、 $segments配列を改ざんせずに次のセグメントの値のみを取得します。[編集]

于 2013-02-20T11:44:45.543 に答える