Is $view in an MVC php program a proper use of glo

2019-07-26 13:12发布

问题:

I understand that global variables should be avoided generally. As a beginner, I'm trying to understand whether making a $view variable global in a dynamic web program built following MVC principles is one of those cases where globals are a good idea.

In my program, I create the $view (as an object that contains an empty array) in index.php, and turn it into a global in the controllers that use it.

Thanks!

JDelage

回答1:

In my program, I create the $view as an empty array() in index.php, and turn it into a global in the controllers that use it.

Why? If you controller needs the $view then just pass it in via the constructor or a setter. Dependencies on Globals can always be resolved by using Dependency Injection.

// index.php
$view = array();
$controller = new Controller($view);
$controller->doAction();

Also, please reconsider if the View should be just an array. The View has to be rendered at some point. Rendering is a responsibility I'd see on the View. An array cannot render itself, so I assume you are doing it elsewhere. That would be a violation of the Single Responsibility Principle. I'd rather do something along the lines of (simplified):

// index.php
$view = new View;
$controller = new Controller($view);
$controller->doAction();

// controller.php
...
public function doAction()
{
    ...
    $this->view->setTemplate('/path/to/template');
    $this->view->setVar('foo', 'bar');
    $this->view->render();
}
...


回答2:

Global variables should be avoided, true. They are however useful for holding global state - that doesn' lead to intercoupling - as in specifically a $config array/object.

But in your case, I'm not sure the $view is a good idea. Firstly, I believe what you are assembling is output data. The "view" in MVC is more of a output template, what I believe your index.php is? If so, then you are actually assembling the $model. Uncertain.
It's theoretically cleaner to pass this variable to the controllers explicitely then. If it's however really only that one global variable, and you promise not to overdo it, passing it around as global is ok.

But you could also follow the mentioned singleton pattern. However I'd recommend the procedural variant, because it's cleaner:

function view() {
    static $view;
    if (!isset($view)) { $view = new ArrayObject(); }
    return $view;
}

This way you could use $view=view(); to localize it in your controllers, keep using it as array $view["xy"]= and as object $view->xy= at the same time.
Or just write print view()->title; in the template.



回答3:

Making a variable global is not a good idea when you using an MVC patterns in a project. There are other solutions that will make use of your MVC more wisely.

If you should have a single use of a resource, use a singleton pattern. Something like

class My_View {

   private $_instance;

   static public function getInstance() {
     if (null === self::$_instance) {
        self::$_instance = new self();
     }
     return self::$_instance;
   }

   private function __construct() { }

   public function __clone() {
      trigger_error("Cannot make copy of this object", E_USER_ERROR);
   }

   // add public methods and/or properties here...
}

and get that view object anywhere with

$view = My_View::getInstance();

This way, you don't have global variables and you are using best OO practices.

However, as others have pointed out, having a singleton view is not necessarily a good idea... A probably better approach would be to make your dispatcher code create a new view and set it to the controller being called, so the controller would have a view available and access it directly.

The view (not being a singleton) could also be a public property, or accessible though a public method of an application singleton; My_Application::getInstance()->getView(); that could also hold the current configuration, paths,

The name My_view suggested is merely an example. Using some sort of naming convention helps organize the code and helps getting rid of all those include/require calls in your script headers. This is not in your question's scope, however for clarity's sake, I'll briefly explain :

In your bootstrap, you declare your autoloading function (as defined in the PHP manual) :

// My_Application.php located in /path/to/lib/My/Application.php
class My_View {

   private $_instance;

   static public function getInstance() {
     if (null === self::$_instance) {
        self::$_instance = new self();
     }
     return self::$_instance;
   }

   private $_basePath;
   public $view;

   private function __construct() { }

   public function __clone() {
      trigger_error("Cannot make copy of this object", E_USER_ERROR);
   }

   public function initAutoloader($base) {
      $this->_basePath = $base;
      spl_autoload_register(array($this, 'autoload'));
   }

   public function autoload($name) {
      require $this->_basePath . str_replace('_', '/', $name) . '.php';
   }

   // get the application global view
   public function getView() {
      if (null === $this->view) {
         $this->view = new My_View();
      }
      return $this->view;
   }
}
// perhaps have the parameter being a constant. I.g. APP_BASE_PATH
My_Application::getInstance()->initAutoload('/path/to/lib/');

And simply include the file '/path/to/lib/My/Application.php' and when you will access My_View, the load function will be called with $name = 'My_View' and the function will simply require the file '/path/to/lib/My/View.php' for you. This is not much for one file, but if all classes are namespaces like so, you only need one include (Autoloader) and everything else is loaded automatically.



回答4:

A solution to this problem I've seen is having controllers extend from a base controller. Within that base controller a $data property is instantiated; new view data is then appending to this property. For example:

class PageController extends BaseController {

    function IndexAction() {
        $this->data['title'] = "Page Title";
        $this->data['content'] = "<p>Page content.</p>";

        $this->render();
    }
}

The render method (which could be loaded automatically after a method has ran, rather than being explicitly) would then assemble a template using the data held within the $data array.

Hope this helps and you're able to implement a working solution.