Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 55 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
55
Dung lượng
875,96 KB
Nội dung
360 CHAPTER 16 CONTROLLERS else { $messages = $filtering->getErrors(); } b We start by creating an instance of the filtering mechanism. C We add a couple of filters specifying which input variables should be filtered accord- ing to which criteria. We also have the ability to specify error messages that will be available if the checks fail. D The filter() method works on a raw request object. This object is not intended to be used directly. filter() returns TRUE if all variables match the filtering crite- ria. On success, we can get a new, clean request object; this object is a different class than the raw request object. The two classes have different methods, so the clean request object cannot be replaced with a raw request object by mistake. E We can get the validated values from the clean request object in a convenient way. F If the validation fails, we can get the error messages we need to display. We are getting a bit ahead of ourselves here, since the next chapter deals with input validation. For now, let’s just make a temporary diagram of which classes might be required to implement this design. Figure 16.1 is mostly obvious from the example. The CleanRequest and RawRequest objects are given, the Filtering object has the methods used in the example, and it’s reasonable to assume that we will need separate classes for different kinds of filtering. In the next chapter, we will go into the details of a secure request architecture, using slightly different terminology. Representing the HTTP request in a safe way is a prerequisite to maintaining secu- rity when applying web presentation patterns. Now that we have dealt with that, we’re ready to take on the first one of these patterns, Page Controller. f Get error messages Figure 16.1 Quick sketch of a possible design for request object filtering USING PAGE CONTROLLERS 361 16.2 USING PAGE CONTROLLERS As mentioned in the previous chapters, the two conventional web presentation patterns are called Page Controller and Front Controller. As with Composite View, these may be considered fancy names for practices that should mostly be familiar to the average PHP programmer. In fact, the Page Controller structure may seem very similar to the average naive PHP application. It may seem like just any collection of PHP pages with links to each other. There is a bit more to it than that, though. As we saw in the previous discus- sion, the word page is one of the hardest to define. In Patterns of Enterprise Application Architecture [ P of EAA], Martin Fowler defines Page Controller as “an object that handles a request for a specific page or action on a website.” To make it meaningful, we will define it in this context as a single PHP file that handles a command. In this section, we’ll first try out a simple Page Controller structure for our com- mands. Then we’ll discuss the most important challenges that arise from this design. The first one is choosing which View to display depending on validation results or other criteria. We also want to unit test the page controllers. When we try that, it will become clear that we need to avoid HTML output, and using templates is a good way to achieve that. Finally, we’ll discover how to unit test redirects as well. 16.2.1 A simple example Let’s start with the edit form. When we left off in the previous chapter, we had cre- ated two commands for this: editArticle and newArticle. For the sake of the Page Controller, it’s a good idea to make the command structure a little more coarse- grained, so we’ll put both commands in an edit.php file. In addition to merging the two commands, we’ll modernize the code by using an object-oriented approach to getting the data from the database. How to build the objects that retrieve the data is the subject of chapters 19-21; here, we’re just using the relatively simple APIs provided by these objects. We’ll also use a Composite View template as described in chapter 14. This is not necessary for the Page Controller to work. It’s entirely possible to do the Page Con- troller in the same style as the previous examples in this chapter, with MySQL func- tions and an HTML section containing the form. There are advantages to object- oriented data access and templates, but this example doesn’t strictly need them. Listing 16.3 shows the edit.php file. $mapper = new NewsMapper; if (is_int($request->get('id'))) { $article = $mapper->find($request->get('id')); } else { $article = new NewsArticle('',''); } $template = new Smarty; Listing 16.3 news/edit.php with templates and object-oriented data access b Create object to retrieve news If ID, get news article c d If not, create 362 CHAPTER 16 CONTROLLERS $template->assign('content','newsform.tpl'); $template->assign('article',$article); echo $template->fetch('normal.tpl'); b We retrieve the news article as an object from the database by using a separate object called a News Mapper. C Since we’ve merged the edit and new commands, we’re back to testing for the pres- ence of an ID to check which one the user wants. If the ID is set, we get the article with that ID. D If the ID is not set, we assume that the user wants to write a new article. We instantiate a NewsArticle object with an empty headline and an empty body text. These show up as empty fields in the form. For example, if we had wanted to display a default value for the headline, we could have used that as the first argument to the constructor: $article = new NewsArticle('Man bites dog',''); E We plug the news form template into the content area as described in chapter 14. We also merge the update and insert commands to make a post.php as shown in listing 16.4. $mapper = new NewsMapper; $article = new NewsArticle( $request->get('headline'), $request->get('text')); if ($request->get('id')) { $article->setID($request->get('id')); $mapper->update($article); } else { $mapper->insert($article); } header("Location: news/list.php"); exit; b This time we create a NewsArticle object using the headline and text entered in the form. C If there is an ID in the POST request, we assume that we want to update an existing article. To do that, we have to set the ID in the NewsArticle object and let the mapper run the update. e Plug news form into content area Listing 16.4 news/post.php using object-oriented data access b Create NewsArticle c Update if ID is present d Insert if ID is not present USING PAGE CONTROLLERS 363 D If the article is a new one, we run the insert() method on the mapper. The map- per or the database takes care of generating an ID for the article. The Page Controller structure has some advantages. It’s intuitive and uses the web server to do part of the job. You can organize and sort the commands in a natural way using the file system, even if there are lots of them. You can have news/edit.php and contacts/edit.php. It’s easy to avoid loading any more code than what you need in each case. On the other hand, it’s less flexible than the Front Controller structure. It’s harder to pre- and post-process and harder to test. And it’s harder to let one command call another command, which is one reason why we chose to make coarser-grained commands. 16.2.2 Choosing Views from a Page Controller The previous examples have no input validation. That weakness hides a deeper prob- lem. The problem is that if validation fails, we want to redisplay the form with the values the user already entered. Keeping the values means having a NewsArticle object on hand, and we do. When the user submits the form, we start by constructing the object, so that part of it is taken care of. But how do we redisplay the form? The classic solution in naive PHP is to include the edit.php file when that happens. So let’s do a super-simple validation just to find out how that works. We test whether the headline is empty, and if it is, we include the edit.php file. $article = new NewsArticle( $request->get('headline'), $request->get('text') ); $article->setID($request->get('id')); if (empty($request->get('headline'))) { include('news/edit.php'); exit; } // Save the article to the database and redirect to list.php The problem with this is that the edit.php file starts by establishing the NewsArticle object, either by getting it from the database or making an empty one. So to keep that from happening, we have to add an extra if test: if (!$article) { if (!empty($request->get('id'))) { $article = $mapper->find($request->get('id')); } else { $article = NewsArticle::createEmpty(); } } 364 CHAPTER 16 CONTROLLERS The conditional logic is getting uncomfortably complex, and this way of doing it is artificial anyway. We are letting the edit.php file serve both as a web page and as an include file, and that’s fundamentally unsound. A better solution is to isolate the tem- plate display code in a separate file, say view_form.php: $template = new Smarty; $template->assign('content','newsform.tpl'); $template->assign('article',$article); echo $template->fetch('normal.tpl'); This allows us to remove that extra if test. And what we have done is, in MVC terms, separate the View from the Controllers. edit.php and post.php are both Con- trollers, while view_form.php is a View. But there is still another problem. When we use include this way, we are passing variables implicitly from one file to another. The application depends on the $arti- cle object being created in post.php and then being passed silently into edit.php through the include mechanism. If instead we enclose the contents of view_form.php in a function, include it at the beginning, and run the function where it’s needed, we can pass those variables explicitly. function viewForm($article) { } It’s clear that this makes the code clearer and more reusable. But it’s easy to forget that when using includes in PHP. In these simple examples, using a plain include is rel- atively innocuous. But as complexity increases, including script code (as opposed to functions and classes) can lead to a great deal of confusion. We can go further along this path by encapsulating the View code in a class. For example, the class could be a template child class or a specialized form class. 16.2.3 Making commands unit-testable One of the downsides to the naive PHP page controllers is that they’re harder to test. It’s not impossible, but awkward: you can include the file and use output buffering to catch the HTML output and test its contents. If the commands are functions or classes instead, they’re easier to test separately. In general, making code testable tends to help improve its design. It requires modularity. So if we solve the problems that occur when we try to unit test our web application, it will help us make the code more reusable and easier to maintain. This is test-driven design in practice. When we want to test our commands, there are at least two clear problems that need to be solved: •When there’s HTML output, we have to catch that output or make some changes so we can get the output more easily. USING PAGE CONTROLLERS 365 • Some commands do a redirect. There is no simple way to detect the fact that a redirect has been sent, and it’s customary to exit unceremoniously after a redirect. Exiting causes the test script to stop executing before it reports any test results. 16.2.4 Avoiding HTML output The next problem is avoiding output. If we want to test an editArticle() com- mand, we should make sure the headline and the text of the existing article are dis- played in the input boxes in the form. To check this, we must be able to get the HTML code in a variable for testing instead of having it output all the time. One way to achieve this is to use output buffering: function testEditCommand() { // Pretend there's been an HTTP request $_REQUEST = array('id' => 1); ob_start(); editArticle(); $form = ob_get_contents(); ob_end_clean(); $this->assertPattern('!input\ type="text"\ name="headline" .*Fire\ in\ Silven\ Valley!six',$form); } (The x pattern modifier is there just to enable us to break the regular expression into two lines. That means we have to escape all the spaces.) This way of using output buffering certainly works, but it’s a bit cumbersome. It does enable us to check precisely what HTML output the PHP page produces. On the other hand, do we really need to wade through the HTML code using regular expres- sions? Perhaps it would be enough to test that the variables inserted into the HTML code are correct. We can do that, and avoid output buffering, if we keep the HTML in a template and check that the template object has been fed the correct variables. 16.2.5 Using templates We can let the command return a template and the client (the web page) can display it. Now we can redesign our tests so that we will be able to test the commands effec- tively. If we let the command function return a template object, we can get the vari- ables from the returned template and test that they are correct: $template = someCommand(); $this->assertEqual( 'Man bites dog', $template->get_template_vars('headline')); In the application itself, we display the result of template processing instead. What are called template objects here are objects that have all the information they need to display themselves. With some template engines, the template object can do that. A Smarty object, on the other hand, is not able to display itself, since it’s not a 366 CHAPTER 16 CONTROLLERS template. To generate the HTML result, you need to pass a template resource as a parameter to the Smarty object: $smarty->display('newslist.tpl'); We want the command to be able to make the decision about which template to dis- play, so the object returned from the command must have the name of the template file built in. Alternatively, we might let the command return an array containing the Smarty object and the template resource. Then we could do this: list($smarty,$template) = someCommand(); $smarty->display($template); But this makes the implementation unnecessarily dependent on Smarty. Better to have a generic template interface. While we’re at it, we might as well gather the command functions in a class. That will eliminate the risk that someone will execute a function that was not intended as a command. In the Page Controller example, we had edit.php include view_form.php. Doing something similar with methods in a NewsCommands class, we can let the view- Form() method return a Template object: class NewsCommands { function editCommand() { $mapper = new NewsMapper; $article = $mapper->find($request->get('id')); return $this->viewForm($article); } function viewForm($article) { $template = new Template_Smarty('normal'); $template->set('content','newsform.tpl'); $template->set('article',$article); return $template; } } To run this in a real application, we’ll need to build a Web Handler, which is part of the Front Controller pattern. But, for now, just to show how it works, here is some concrete code to run it: $commands = new NewsCommands; $template = $commands->editCommand(); $template->display(); This gets the news article from the database and displays it in the editing form. 16.2.6 The redirect problem Now we’ve solved one of the problems that make testing awkward. One remains: the fact that commands sometimes need to issue redirects. Redirects are typically used USING PAGE CONTROLLERS 367 after successful form submissions. In fact, it’s good practice to redirect after a POST request and then use GET to show the next page. It’s hard to test whether the code has done a redirect. If we could return a value instead, testing would be easy. But we want to do different things depending on cir- cumstances: Some commands will return a template so that we can redisplay the form. Others will return something to tell us to do a redirect so that we can display the news list. So if we return something that is not a template (say, a string representing a URL to redirect to), we may need to start adding conditional logic like this: if ($template instanceof Template) { $template->display(); } else { //The template is actually a URL for redirect? header("Location: $template"); exit; One solution: avoiding redirects It’s not elegant. One opportunity that opened up when we started using templates is to drop the redirect. If we make the news list available as a template, we can get that template, fill it with data, and display it. In fact, if we have a command to show the news list, we can use that both here and to display the news list in the first place. The approach works, but there is one snag: the news list page is displayed, but we started out processing a POST request on to newsform.php. The address bar in the browser will display newsform.php in the URL. When we redirected to newslist.php, that was the URL shown in the address bar. And here’s the real problem: if you click the Reload button in the browser, it decides it’s been asked to rerun the POST request and displays the message in figure 16.2. This is the main reason why you should (almost) always redirect after a POST request in traditional, non-AJAX web pro- gramming. In some cases, this may be only a minor nuisance. On the other hand, there is another way to handle it. Another solution: redirecting transparently We can do a redirect via a sort of “degenerate template”: a Redirect object that per- forms the redirect when you call a display method on it. Listing 16.5 shows how an object like this can be defined. Figure 16.2 Annoying message on clicking Reload 368 CHAPTER 16 CONTROLLERS class Redirect { var $url; function __construct($url) { $this->url = $url; } function display() { header("Location: ".$this->url); exit; } } So now, instead of calling the showNewslist command, we can return a Redirect object instead: return new Redirect('newslist.php'); The Redirect class is an example of a design pattern Fowler calls Special Case—or something very similar. You can use Special Case to avoid having to write conditional statements to take care of special cases. Polymorphism does the job instead: the cases are handled differently depending on what kind of object is handling them. Since PHP does not normally check the types of objects, we can use duck typing as described in chapter 4. We can execute a method call on an object and it will work even if the objects have no relationship beyond the fact that both their classes have that one particular method implemented. Alternatively, we can formalize the resemblance by using the interface keyword. This gives us an interface like the one in the sec- tion on type hints in chapter 3. The interface has fewer methods than the template, since it doesn’t have the ability to set or get variables or return HTML code: interface Response { public function display(); public function execute( ); } Now the Redirect class can implement the Response interface: class Redirect implements Response { } Listing 16.5 Class to make Redirect objects that imitate a template Figure 16.3 Redirect and Template as Response objects BUILDING A FRONT CONTROLLER 369 Figure 16.3 shows the relationship between the classes. As mentioned, whether the Redirect and Template objects formally implement the Response interface is of lit- tle practical importance. Again using code similar to the Page Controller example, we can now implement a post command that will return a redirect to the news list on success, the form tem- plate on failure: class NewsCommands { function postCommand($request) { $mapper = new NewsMapper; $article = new NewsArticle( $request->get('headline'), $request->get('text'), ); $article->setID($request->get('id')); if (empty($request->get('headline'))) { return $this->viewForm($article); } if ($request->get('id')) { $mapper->update($article); } else { $mapper->insert($article); } return new Redirect('list.php'); } } We are getting closer to being able to build a Front Controller. In fact, we have all the building blocks we need for a Front Controller except for the main controller compo- nent, the Web Handler. 16.3 BUILDING A FRONT CONTROLLER A Front Controller is a mechanism that handles all requests for a web site or applica- tion. PHP applications frequently achieve this by having a single file—more often than not called index.php—that is used for all requests and handles them by includ- ing other files. In different applications, index.php may contain different things depending on the structure of the application: links, templates, redirects, and includes. But to sim- plify all this, we can distinguish between two fundamentally different ways of using it: • As a start page that contains links to other pages. • As the page representing the only URL used in the application. There are links, but all the links go back to index.php and are distinguished only by the query string. [...]... Returning to server-side validation, we build an object-oriented design that solves many of the problems we outlined earlier Finally, we’ll discuss the possibility of synchronizing server-side and client-side validation so that we can avoid defining all validations twice 17.1 INPUT VALIDATION IN APPLICATION DESIGN Let’s try to make validation more exciting, or at least more interesting, by investigating... viable in an application to which only selected users have access, such as a private intranet; otherwise it is too insecure Form generation This is a more ambitious strategy that involves having all the information about the form—including validation rules in one place (typically PHP code or some kind of XML representation of the form) and using that both to generate the form and to read the information... for input validation and defining the components, we are ready to examine some specific problems we are trying to solve What makes input validation so hard that the plain PHP way falls short? 17.2 SERVER-SIDE VALIDATION AND ITS PROBLEMS The typical, plain PHP way of doing validation is simple but flawed That makes it a good place to start: we can start simple and try to eliminate the flaws while introducing... them using the plain superglobal arrays $_POST and $_GET, but most who are into object-oriented PHP choose to encapsulate the HTTP request in a separate object One advantage of this is that input filtering can be built into the request object instead of being foreign to the general design of the application Page Controller is simple and close to naive PHP Front Controller is a common pattern in most... Several PHP MVC frameworks use this kind of approach (what I’ve called a command group) The Zend Framework is one of these, with several actions (commands) in one class called a Controller: Zend::loadClass('Zend_Controller _Action' ); class NewsController extends Zend_Controller _Action { public function indexAction() { } public function editAction() { } public function postAction() { } } indexAction()... shortcomings, and find the simplest possible ways to overcome them Let’s say we have a form with a text field called headline that is required This will generate a variable in the HTTP request that’s translated to $_POST('headline') and $_REQUEST('headline') in the PHP script Assuming this has been transformed to a request object as shown in the last chapter, the painless and relatively brainless way... This means that we need at least two different validators: one for checking that the password has a minimum length, and one for checking that the two inputs are equal Checking for equal inputs is more demanding in terms of design, since the design is based on relating each validation to a single field We want a message like the one in figure 17.2, and it’s reasonable to display it when the user leaves... name for the input element (for displaying in the alert box), the object representing the input element (this), and an optional comparison argument that represents string length in one case and the name of another element in another case The visitAll() function is supplied with an object representing the form As mentioned previously, there are certain things we can’t do on the client The kind of validation... through input filtering, we want the command method to use it rather than $_POST and $_GET directly Figure 16.5 is a class diagram of the design 372 CHAPTER 16 CONTROLLERS The handler executes the template and returns the HTML result, so it doesn’t output anything We’ll do that in the index .php file We’ve stripped it of everything interesting, so by now it is minuscule: require_once 'WebHandler .php' ;... the information present in the form itself and the information in the validation code The most obvious problem is the text in the message echo "'Email address' is invalid"; The string email address must match the label for the input element in the HTML form So if we want the text on the web page to read just Email instead of Email address, we need to change the message, too The name of the input . it’s easy to forget that when using includes in PHP. In these simple examples, using a plain include is rel- atively innocuous. But as complexity increases, including script code (as opposed to functions. this by having a single file—more often than not called index .php that is used for all requests and handles them by includ- ing other files. In different applications, index .php may contain different. avoid defining all validations twice. 17.1 INPUT VALIDATION IN APPLICATION DESIGN Let’s try to make validation more exciting, or at least more interesting, by investigat- ing it and exploring some