3

MVC パラダイムを使用してアプリをリファクタリングしようとしています。

私のサイトにはグラフが表示されます。URL の形式は次のとおりです。

  • app.com/category1/chart1
  • app.com/category1/chart2
  • app.com/category2/chart1
  • app.com/category2/chart2

すべてのリクエストを index.php にルーティングするために Apache Rewrite を使用しているので、PHP で URL 解析を行っています。

active特定のページが選択されたときに、ナビゲーション リンクにクラスを追加するという永続的なタスクに取り組んでいます。具体的には、カテゴリ レベルのナビゲーションとチャート レベルのサブ ナビゲーションの両方があります。私の質問は、MVC の精神を保ちながらこれを行うための最良の方法は何ですか?

リファクタリングの前に、ナビゲーションが比較的複雑になったため、配列に入れることにしました。

$nav = array(
  '25th_monitoring' => array(
    'title'    => '25th Monitoring',
    'charts' => array(
      'month_over_month' => array(
        'default' => 'month_over_month?who=total&deal=loan&prev='.date('MY', strtotime('-1 month')).'&cur='.date('MY'),
        'title'   => 'Month over Month'),
      'cdu_tracker' => array(
        'default' => 'cdu_tracker',
        'title'   => 'CDU Tracker')
    )
  ),
  'internet_connectivity' => array(
    'title'   => 'Internet Connectivity',
    'default' => 'calc_end_to_end',
    'charts' => array(
      'calc_end_to_end' => array(
        'default' => 'calc_end_to_end',
        'title' => 'calc End to End'),
      'quickcontent_requests' => array(
        'default' => 'quickcontent_requests',
        'title' => 'Quickcontent Requests')
    )
  )
);

繰り返しますが、現在アクセスされている現在のカテゴリと現在のグラフの両方を知る必要があります。私のメインナビは

<nav>
  <ul>
    <?php foreach ($nav as $category => $category_details): ?>
    <li class='<?php echo ($current_category == $category) ? null : 'active'; ?>'>
      <a href="<?php echo 'http://' . $_SERVER['SERVER_NAME'] . '/' . $category . '/' . reset(reset($category_details['charts'])); ?>"><?php echo $category_details['title']; ?></a>
    </li>
    <?php endforeach; ?>
  </ul>
</nav>

サブナビゲーションも同様で、current_category ではなく current_chart をチェックしていました。

以前は、解析中に、 によって爆発$_SERVER['REQUEST_URI']し、断片をと/に分割していました。私はindex.phpでこれをやっていました。今、これはフォントコントローラーの精神に合わないと感じています。Symfony 2 の docsなどの参照から、各ルートには独自のコントローラーが必要なようです。しかし、その後、現在のカテゴリとチャートを、テンプレート ファイル自体 (MVC の精神にあるとは思えない) 内、またはモデル内の任意の関数内 (その後、複数のコントローラーから呼び出す必要があり、これは一見冗長です)。$current_category$current_chart

ここでのベストプラクティスは何ですか?

更新: これが私のフロントコントローラーの外観です:

// index.php
<?php
// Load libraries
require_once 'model.php';
require_once 'controllers.php';

// Route the request
$uri = str_replace('?'.$_SERVER['QUERY_STRING'], '', $_SERVER['REQUEST_URI']);
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && (!empty($_GET)) && $_GET['action'] == 'get_data') {

  $function = $_GET['chart'] . "_data";
  $dataJSON = call_user_func($function);
  header('Content-type: application/json');
  echo $dataJSON;

} elseif ( $uri == '/' ) {
  index_action();

} elseif ( $uri == '/25th_monitoring/month_over_month' ) {
  month_over_month_action();

} elseif ( $uri == '/25th_monitoring/cdu_tracker' ) {
  cdu_tracker_action();

} elseif ( $uri == '/internet_connectivity/intexcalc_end_to_end' ) {
  intexcalc_end_to_end_action();

} elseif ( $uri == '/internet_connectivity/quickcontent_requests' ) {
  quickcontent_requests_action();

} else {
  header('Status: 404 Not Found');
  echo '<html><body><h1>Page Not Found</h1></body></html>';   
}

?>

たとえば、 month_over_month_action() が呼び出されたとき、コントローラーは current_chart が month_over_month であることを認識しているため、それをそのまま渡す必要があるようです。これは私がつまずいているところです。

4

2 に答える 2

3

この分野には「ベスト プラクティス」はありません。ただし、他のものよりも頻繁に使用されるものもあれば、非常に悪いアイデアのものもあります(残念ながら、これら 2 つのグループは重複する傾向があります)

MVC でのルーティング

技術的には MVC デザイン パターンの一部ではありませんが、Web に適用する場合、アプリケーションは初期化するコントローラーとその上で呼び出すメソッドを認識する必要があります。

この種の情報を収集explode()することは悪い考えです。デバッグと保守の両方が困難です。より良い解決策は、正規表現を使用することです。

基本的に、正規表現といくつかのフォールバック値を含むルートのリストができあがります。そのリストをループし、最初の一致でデータを抽出し、データが欠落しているデフォルト値を適用します。

このアプローチにより、パラメーターの順序の可能性が大幅に広がります。

ソリューションを使いやすくするために、表記文字列を正規表現に変換する機能を追加することもできます。

例(私が持っているいくつかの単体テストから取得):

  • 表記:     test[/:id]
    式:#^/test(:?/(?P<id>[^/\.,;?\n]+))?$#

  • 表記:     [[/:minor]/:major]
    式:#^(:?(:?/(?P<minor>[^/\.,;?\n]+))?/(?P<major>[^/\.,;?\n]+))?$#

  • 表記:     user/:id/:nickname
    式:#^/user/(?P<id>[^/\.,;?\n]+)/(?P<nickname>[^/\.,;?\n]+)$#

このようなジェネレーターを作成するのはそれほど簡単ではありませんが、かなり再利用可能です。私見それを作るのに費やした時間は十分に費やされるでしょう. また、(?P<key>expression)正規表現でコンストラクトを使用すると、一致したルートからキーと値のペアの非常に便利な配列が提供されます。

メニューと MVC

どのメニュー項目を強調表示するかについての決定activeは、常に現在のビュー インスタンスが担当する必要があります。

さらに複雑な問題は、そのような決定を下すために必要な情報がどこから来るかです。ビュー インスタンスで使用できるデータには 2 つのソースがあります。コントローラーによってビューに渡された情報と、そのビューがモデル レイヤーから要求されたデータです。

MVC のコントローラーはユーザーの入力を受け取り、この入力に基づいて、その値を渡すことにより、現在のビューとモデル レイヤーの状態を変更します。コントローラーは、モデル レイヤーから情報を抽出するべきではありません。

IMHO、この場合のより良いアプローチは、メニュー コンテンツと現在アクティブな要素の両方に関する情報をモデル レイヤーで中継することです。ビューで現在アクティブな要素をハードコードし、コントローラーに渡された情報を中継することは可能ですが、MVC は通常、大規模なアプリケーションで使用されます。

MVC デザイン パターンのビューは、ダム テンプレートではありません。これは、UI ロジックを担当する構造です。Web のコンテキストでは、必要に応じて複数のテンプレートから応答を作成するか、場合によっては単純に HTTP ロケーション ヘッダーを送信することを意味します。

于 2013-01-30T08:28:46.057 に答える
2

そうですね、CMSっぽいものを書いていた時もほぼ同じ悩みでした。そのため、これを機能させ、コードをより保守しやすく、クリーンに保つ方法を見つけようと、しばらく時間を費やしました。CakePHP と Symfony の両方のルート メカニズムに少し刺激を受けましたが、十分ではありませんでした。ですから、私が今これをどのように行っているかの例を挙げようと思います。

私の質問は、MVC の精神を保ちながらこれを行うための最良の方法は何ですか?

まず、一般的にベスト プラクティスは、Web 開発で MVC を使用した手続き型アプローチをまったく使用しないことです。次に、SRP を保持します。

Symfony 2 のドキュメントなどの参照から、各ルートには独自のコントローラーが必要なようです。

ええ、それは正しいアプローチですが、別のルート マッチが同じコントローラを持つことができないという意味ではありませんが、アクションは異なります。

あなたのアプローチ (投稿したコード) の主な欠点は、責任を混在させ、MVC にインスパイアされたパターンを実装していないことです。とにかく、手続き型アプローチを使用した PHP の MVC は恐ろしいものです。

したがって、正確に混合しているのは次のとおりです。

  • 「コントローラー」およびルートマップにも含まれていないルートメカニズムロジック(別のクラスである必要があります)
  • リクエストとレスポンスの責任 (あなたにはわかりません)
  • クラスのオートローディング
  • コントローラーのロジック

これらすべての「パーツ」には、1 つのクラスが必要です。基本的に、インデックスまたはブートストラップ ファイルに含める必要があります。

また、そうすることで:

require_once 'controllers.php';

一致ごとにすべてのコントローラーを自動的に含めます (一致しない場合でも)。実際には MVC とは何の関係もなく、メモリ リークが発生します。代わりに、URI 文字列に一致するコントローラーのみを含めてインスタンス化する必要があります。また、同じファイルをどこかに 2 回インクルードすると、コードが重複する可能性があるため、注意してinclude()ください。require()

また、

} elseif ( $uri == '/' ) {
  index_action();

} elseif ( $uri == '/25th_monitoring/month_over_month' ) {
  month_over_month_action();

} elseif ( $uri == '/25th_monitoring/cdu_tracker' ) {
  cdu_tracker_action();

} elseif ( $uri == '/internet_connectivity/intexcalc_end_to_end' ) {
  intexcalc_end_to_end_action();

if/else/elseif制御構造を使用して一致を行うことは非常に賢明ではありません。では、50 マッチした場合はどうでしょうか。または100?それに応じて書き込むには、50回または100回書き込む必要がありますelse/elseif。代わりに、マップを用意して (配列など)、HTTP 要求ごとに反復する必要があります。

ルーティング メカニズムで MVC を使用する一般的なアプローチは、次のようになります。

  1. リクエストをルート マップと照合する (そして、ある場合はどこかのパラメーターを保持する)
  2. 次に、適切なコントローラーをインスタンス化します
  3. 次に、パラメーターがある場合はパラメーターを渡します

PHP では、実装は次のようになります。

ファイル: index.php

<?php

//.....

// -> Load classes here via SPL autoloader or smth like this

// .......

// Then -> define or (better include route map from config dir)

$routes = array(

    // -> This should default one
    '/' => array('controller' => 'Path_To_home_Controller', 'action' => 'indexAction'),

    '/user/:id' => array('controller' => 'Path_to_user_controller', 'action' => 'ViewAction'),   

    // -> Define the same controller
    '/user/:id/edit' => array('controller' => 'Path_to_user_controller', 'action' => 'editAction'),


    // -> This match we are going to hanlde in example below:
    '/article/:id/:user' => array('controller' => 'SomeArticleController', 'action' => )

);

// -> Also, note you can differently handle this: array('controller' => 'SomeArticleController', 'action' => )
// -> Generally controller key should point to the path of a matched controller, and action should be a method of the controller instance
// -> But if you're still on your own, you can define it the way you want.


// -> Then instantiate common classes

$request  = new Request();
$response = new Response();

$router = new Router();

$router->setMap( $routes );

// -> getURI() should return $_SERVER['REQUEST_URI']
$router->setURI( $request->getURI() ); 


if ( $router->match() !== FALSE ) {

  // -> So, let's assume that URI was:  '/article/1/foo'     

  $info = $router->getAll();

  print_r ( $info );

  /**
   * Array( 'parameters'  =>  Array(':id' => '1', ':user' => 'foo'))
   *        'controller'  => 'Path_To_Controller.php'
   *        'action'      => 'indexAction'
   */

   // -> The next things we are going to do are:

   // -> 1. Instantiate the controller
   // -> 2. Pass those parameters we got to the indexAction method   

   $controller =  $info['controller'];

   // -> Assume that the name of the controller is User_Controller
   require ( $controller ); 

   // -> The name of class should also be dynamic, not like this, thats just an example
   $controller = new User_Controller(); 

   $arguments = array_values( $info['parameters'] );

   call_user_func_array( array($controller, $info['action']), $arguments );  

   // -> i.e we just called $controller->indexAction('1', 'foo') "dynamically" according to the matched URI string

   // -> idealy this should be done like: $response->send( $content ), however

} else {

   // -> In order not to show any error
   // -> redirect back to "default" controller
   $request->redirect('/');

}

MVC にインスパイアされたアプリケーションでは、次のようにルーティングします。

(依存性注入を使用してSRPを保持する場所)

<?php

require (__DIR__ . '/core/System/Auload/Autoloader.php');

Autoloader::boot(); // one method includes all required classes

$map = require(__DIR__ . '/core/System/Route/map.php');

$request    = new Request();
$response   = new Response();

$mvc        = new MVC();
$mvc->setMap( array_values($map) ); 
// -> array_values($map) isn't accurate here, it'd be a map of controllers
// -> take this as a quick example


$router     = new Router();

$router->setMap( $map );
$router->setURI( $request()->getURI() );


if ( $router->match() !== FALSE ) {

    // -> Internally, it would automatically find both model and view instances
    // -> then do instantiate and invoke appropriate action
    $router->run( $mvc );

} else {

    // No matches handle here
    $request->redirect('/');
}

Cake と Symfony を調べた結果、これが自分にとってより適切であることがわかりました。

私が注意したいことの1つは:

PHP で MVC に関する優れた記事を見つけるのはそれほど簡単ではありません。それらのほとんどは単に間違っています。(多くの人がそうであるように、初めて彼らから学び始めたので、私はそれがどのように感じるかを知っています)

したがって、ここでの私のポイントは次のとおりです。

私が以前にしたような過ちを犯さないでください。MVC を学習したい場合は、Zend Framework または Symfony のチュートリアルを読むことから始めてください。作品は少し違いますが、シーンの背後にある考え方は同じです。

質問の別の部分に戻る

繰り返しますが、現在アクセスされている現在のカテゴリと現在のチャートの両方を知る必要があります。私のメインナビは

<nav>
  <ul>
    <?php foreach($nav as $category => $category_details): ?>
    <li class='<?php echo ($current_category == $category) ? null : 'active'; ?>'>
      <a href="<?php echo 'http://' . $_SERVER['SERVER_NAME'] . '/' . $category . '/' . reset(reset($category_details['charts'])); ?>"><?php echo $category_details['title']; ?></a>
    </li>
    <?php endforeach; ?>
  </ul>
</nav>

まず、文字列を連結しないで、代わりに次のprintf()ように使用します。

<a href="<?php printf('http://%s/%s/%s', $_SERVER['SERVER_NAME'], $category, reset(reset($category_details['charts']))); ?>"><?php echo $category_details['title']; ?></a> 

これをどこにでも(または少なくとも多くの異なるテンプレートに)配置する必要がある場合は、共通の抽象ビュークラスに配置することをお勧めします。

例えば、

abstract class View
{
    // -> bunch of view reusable methods here...

    // -> Including this one
    final protected function getCategories()
    {
        return array(

            //....
        );
    }
}

class Customers_View extends View
{
    public function render()
    {
        $categories =& $this->getCategories();

        // -> include HTML template and then interate over $categories
    }

}
于 2013-02-06T17:48:52.687 に答える