3

我正在尝试使用 MVC 范例重构我的应用程序。

我的网站显示图表。URL 的格式为

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

我正在使用 Apache Rewrite 将所有请求路由到 index.php,因此我正在使用 PHP 进行 URL 解析。

I am working on the enduring task of adding an activeclass to my navigation links when a certain page is selected. 具体来说,我既有类别级导航,也有图表级子导航。我的问题是,在保持 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>

并且 sub-nav 是类似的,检查 current_chart 而不是 current_category。

之前,在解析过程中,我正在爆炸$_SERVER['REQUEST_URI']/并将碎片分解为$current_categoryand $current_chart。我在 index.php 中这样做。现在,我觉得这不符合字体控制器的精神。从Symfony 2's docs之类的参考资料来看,似乎每条路由都应该有自己的控制器。但是后来,我发现自己必须多次定义当前类别和图表,要么在模板文件本身(这似乎不符合 MVC 的精神),要么在模型中的任意函数(然后必须由多个控制器调用,这似乎是多余的)。

这里的最佳做法是什么?

更新:这是我的前端控制器的样子:

// 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

这方面没有“最佳实践”。虽然有一些比其他更常用,还有一些是非常糟糕的想法(不幸的是,这两组往往重叠)

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应该始终是当前视图实例的责任。

更复杂的问题是做出此类决定所必需的信息从何而来。如果数据可用于视图实例,则有两个来源:控制器传递给视图的信息和数据,即从模型层请求的视图。

MVC 中的控制器接受用户的输入,并基于此输入,通过传递所述值来更改当前视图和模型层的状态。控制器不应模型层提取信息。

恕我直言,在这种情况下,更好的方法是在模型层上传递有关菜单内容和其中当前活动元素的信息。虽然可以对视图中当前活动的元素进行硬编码,也可以在控制器传递的信息上中继,但 MVC 通常用于大规模应用程序,这种做法最终会伤害到你。

MVC 设计模式中的视图不是一个愚蠢的模板。它是一个负责 UI 逻辑的结构。在 Web 上下文中,这意味着在必要时从多个模板创建响应,或者有时只是简单地发送 HTTP 位置标头。

于 2013-01-30T08:28:46.057 回答
2

好吧,在编写类似 CMS 的产品时,我遇到了几乎同样的麻烦。所以我花了一些时间试图弄清楚如何使它工作并保持代码更易于维护和清洁。CakePHP 和 Symfony 路由机制都给了我一些启发,但对我来说还不够好。所以我会试着给你一个我现在如何做的例子。

我的问题是,在保持 MVC 精神的同时,最好的方法是什么?

首先,一般来说,最佳实践是根本不要在 Web 开发中使用带有 MVC 的过程方法。其次,保留 SRP。

从像 Symfony 2's docs 这样的参考资料来看,似乎每条路由都应该有自己的控制器。

是的,这是正确的方法,但这并不意味着另一个路由匹配不能具有相同的控制器,而是不同的操作。

您的方法(您发布的代码)的主要缺点是您混合了职责并且您没有实现受 MVC 启发的模式。无论如何,PHP 中的 MVC 与过程方法只是一件可怕的事情。

所以,你正在混合的是:

  • 路由机制逻辑(它应该是另一个类)不在“控制器”和路由映射中
  • 请求和响应责任(我发现这对你来说并不明显)
  • 类自动加载
  • 控制器逻辑

所有这些“部分”都应该有一个类。基本上,它们必须包含在索引或引导文件中。

此外,通过这样做:

require_once 'controllers.php';

您会自动包含每次匹配的所有控制器(即使在不匹配的情况下)。它实际上与 MVC 无关,并导致内存泄漏。相反,您应该只包含并实例化与 URI 字符串匹配的控制器。另外,请注意,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 之后,我发现这更适合我。

我要注意的一件事:

在 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 回答