PHP in Action phần 6 doc

55 218 0
PHP in Action phần 6 doc

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

250 CHAPTER 11 REFACTORING WEB APPLICATIONS This is all we need to create the template file. We take this output, stick it in a file, and call it something original like template.php. What we are doing here is actually a form of Kent Beck’s FakeIt pattern [Beck]. It’s analogous to what we did in chapter 9 to make the TestOfMysqlTransaction pass. We make the code work by hard-coding the data that will make the tests pass; then we can start inserting real data. As a first step toward real data, we create a PHP section at the beginning, set the desired data as variables, and use the variables in the HTML section at the end of the template file. After we make our test reporter class generate the vari- ables, we can remove this PHP section. The “template” is shown in listing 11.3. <?php $testname = 'SomeTest'; $run = 1; //Number of cases actually run $cases = 1; //Total number of cases $passes = 0; $failures = 2; $exceptions = 0; $count = 0; //Start counting tests at 0 $ok = FALSE; $failreports = array( array( 'message'=>"Equal expectation fails because [Integer: 1]". "differs from [Integer: 2] by 1 at line [8]", 'breadcrumb'=>'testSomething' ), array( 'message'=>"Equal expectation fails because [Integer: 2]". "differs from [Integer: 3] by 1 at line [9]", 'breadcrumb'=>'testSomething' ), ); ?> <?=$testname ?> <?php foreach ($failreports as $failure): ?> <?=++$count ?>) <?=$failure['message'] ?> <?=$failure['breadcrumb'] ?> <?php endforeach; ?> <?php if ($ok): ?> OK <?php else: ?> FAILURES!!! <?php endif; ?> Test cases run: <?=$run ?>/<?=$cases ?>, Passes: <?=$passes ?>, Failures: <?=$failures ?>, Exceptions: <?=$exceptions ?> Listing 11.3 PHP “template” file created from the test output SEPARATING MARKUP FROM PROGRAM CODE 251 The template consists mostly of variables; in addition it has the essential logic for generating the output: •A foreach loop to show the test failures •A $count variable to keep track of how many failures we’ve displayed •An if-else conditional to display a different message depending on whether some tests failed The first half of the file just sets the variables; the second half is the actual template that outputs the results. The second half is what would normally be an HTML sec- tion, although in this case, there is no actual HTML markup. Instead, it contains lots of small PHP sections that mostly just display a single variable. This might seem excessive and not very readable as it stands, but the point is layout flexibility. The lay- out elements can be treated like layout elements instead of code; if you add spaces, they will show up in the command-line output without the need to use print or echo. More importantly, by adding HTML markup, this template can easily be con- verted into an HTML-based template for browser viewing. Our next goal is to generate the required variables from the class. Since we are not in control of the SimpleTest code, we need to make a copy of TextReporter and call it TemplateBasedReporter. Following the test-first principle, the next thing we need is a test of the ability of the class to generate the variables. For the sake of the test, it’s just as well to have a separate method called templateVars() that returns the vari- ables for the template. To get the correct assertions for the test, we just copy and mechanically transform the assignments in the template. This test case is shown in listing 11.4. function testOutputVars() { $reporter = new TemplateBasedReporter; ob_start(); $test = new SomeTest(); $test->run($reporter); ob_end_clean(); extract($reporter->templateVars()); $this->assertEqual('SomeTest',$testname); $this->assertEqual(1,$run); $this->assertEqual(1,$cases); $this->assertEqual(0,$passes); $this->assertEqual(2,$failures); $this->assertEqual(0,$exceptions); $this->assertEqual(FALSE,$ok); Listing 11.4 Testing that our reporter class can generate the variables we want b Create reporter c Run test with output buffering d Extract variables e Test the variables 252 CHAPTER 11 REFACTORING WEB APPLICATIONS $this->assertEqual(array( array( 'message'=>"Equal expectation fails because ". "[Integer: 1] differs from [Integer: 2] ". "by 1 at line [8]", 'breadcrumb'=>'testSomething' ), array( 'message'=>"Equal expectation fails because ". "[Integer: 2] differs from [Integer: 3] ". "by 1 at line [9]", 'breadcrumb'=>'testSomething' ), ),$failreports); } b We start by creating an instance of our test reporter class. c We’re only interested in testing the method that will return the template variables. The old test output is still active, but we don’t need any output for this test, so we turn on output buffering to keep it from bothering us. Then we run the test. We could have used a mock object in place of the real test, but since the test is so simple, we just run it. D We want to get the variables in a form that is easily digested by our template. Since it is a plain PHP include file, we extract the variables from the array returned by the templateVars() method. E We test all the simple variables with asserts that have been mapped from the assign- ments in the template. F For the failure data, we need complex arrays. Since we started with the template, we know that the form of this data is reasonable for use in the template. The next step is another FakeIt. We create the templateVars() method and just hard-code the variables we need to return. The test will pass, and then we can replace the variables one by one with real ones generated during the test run. This is where much of the real work happens, but we won’t go into all the details involving the intricacies of the test reporter class. Eventually, we end up with a templateVars() method that returns real data exclusively. Note the use of compact() here to match the extract() in the test method. In effect, we are transferring all those variables via the return statement by packing them into an array and then unpacking them again. class TemplateBasedReporter function templateVars() { $testname = $this->test_name; $run = $this->getTestCaseProgress(); $cases = $this->getTestCaseCount(); Failure data in complex arrays f SIMPLIFYING CONDITIONAL EXPRESSIONS 253 $passes = $this->getPassCount(); $failures = $this->getFailCount(); $exceptions = $this->getExceptionCount(); $ok = ($this->getFailCount() + $this->getExceptionCount() == 0); $failreports = $this->failreports; return compact("testname","run","cases","passes","failures", "exceptions","count","ok","failreports"); } } Now we’ve implemented most of what we need. We have made sure the template does its job (testing by visual inspection); we have made sure the test reporter class is capable of returning the variables the template needs. What’s lacking is to connect the dots. As mentioned, the paintFooter() method can do all the output work. Now all it needs is to get the template variables and include the template file. class TemplateBasedReporter function paintFooter() { extract($this->templateVars()); include('template.php'); } } Finally, we can remove the PHP code at the beginning of the template file, and the template will display the variables it has been fed by the reporter class instead. Total intermingling of PHP code and HTML markup is probably the number-one refactoring issue in legacy PHP applications. The second most important issue is overly complex and nested conditional expressions and loops. 11.4 SIMPLIFYING CONDITIONAL EXPRESSIONS Conditionals tend to be particularly hard to read and refactor. In PHP applications, it’s not uncommon to see five or more levels of nested conditionals and loops. It’s almost impossible to do anything about it without some way to identify small steps for the refactoring. Testing is another thorny issue. Complete test coverage of a complex conditional statement requires that all paths through the statement are covered. Writing a separate test for each path is advisable. But this is easier said than done. Trying to get by with incomplete test coverage is possible, but entails the risk of introducing bugs that are found at some inconvenient later time. Writing complete unit tests is not that hard if you know exactly what the conditional statement is supposed to do, but frequently this is not the case. There might be special cases you have ignored, and you risk writing tests that turn out to be pointless eventually. If you know exactly what part of the web interface the conditional statement affects, it may be possible to get by with web tests only (see the next chapter). If the web interface is not going to change, these tests will stay useful. 254 CHAPTER 11 REFACTORING WEB APPLICATIONS We’ll discuss these testing problems some more in the section on refactoring from procedural to object-oriented. There is no magic bullet that will make it easy, but at least we can learn the tricks and try them out, as in the examples to follow. 11.4.1 A simple example Listing 11.5 is another example from a real application, but with all variable names changed. What’s happening here? It seems clear that the code is intended to help interpret the HTTP request. (In fact, it seems to be doing something similar to register_globals, which is highly discouraged. It’s included here only to show the mechanics of refactoring.) But the deep nesting makes it harder to see what’s going on. In general, both conditionals and loops can be handled by extracting func- tions or methods externally or internally: Externally: extract the whole conditional statement or the whole loop. Internally: extract one branch—or each branch—of the conditional or the contents of the loop. We’ll consider some possible refactorings of listing 11.5 without going into detail on how to do it. for ($i=0; $i<count($vars); $i += 1) { $var = $vars[$i]; if (!isset($$var)) { if (empty($_POST[$var])) { if (empty($_GET[$var]) && empty($query[$var])) { $$var = ''; } elseif (!empty($_GET[$var])) { $$var = $_GET[$var]; } else { $$var = $query[$var]; } } else { $$var = $_POST[$var]; } } } b These two first lines define the loop itself. They could be replaced with the simpler foreach($vars as $var) { C This if statement could be extracted as a separate function. It represents the entire content of the loop, since the first two lines just define the loop. The obstacle is the fact that there are two non-global variables that are being used inside the if block: $var (which is actually the name of the variable $$var) and the $query array. Listing 11.5 Nested if and for statements b Use foreach instead c Replace with function d Use Reverse Conditional Extract as function e SIMPLIFYING CONDITIONAL EXPRESSIONS 255 The simple way to handle that is just to pass the variables into the function. Then the first line can be changed to a return statement instead of an if. That gets rid of one level of nesting: function getVariable($var,$query) { if (!isset($$var)) return; Alternatively, without the function, we could still get rid of the nesting by using continue to skip the rest of the loop iteration: if (!isset($$var)) continue; D When we have an if-else conditional with a relatively long if and a short else, one possible refactoring is Reverse Conditional. By reversing the sense of the test ( empty becomes !empty), it becomes easier to see the logic: if (!empty($_POST[$var])) { $$var = $_POST[$var]; } else { if (empty($_GET[$var]) && empty($query[$var])) { } } Aha! When an else block starts with an if, that’s an elseif. That means we can get rid of another level of nesting. Another possible refactoring here is Decompose Conditional, which involves extract- ing the test and the branches of the conditional statement as separate methods. The if part is the hottest candidate for extraction, since it’s the most complex. In the next section, we will see a fuller example of Decompose Conditional. e If the remaining if-elseif-else statement is inside a function, we can return values instead of collecting the result in a variable. We could end up with something like this: if (!empty($_POST[$var]) return $_POST[$var]; if (!empty($_GET[$var]) return $_GET[$var]; if (!empty($query[$var]) return $query[$var]; return ''; By now it’s starting to become obvious what the code is actually doing. It looks right, but since we haven’t actually done the refactoring with full test coverage, there is no guarantee it would not break something in the other parts of the application. 11.4.2 A longer example: authentication code Let’s look at a longish example: a form for submitting news articles. The form requires the user to log in before accessing it. In a real application, there would typi- cally be a news list page as well, which would contain links to the form for the pur- pose of editing news articles and submitting new ones. So the example is slightly unnatural in that we would normally not be led directly to the form after logging in; on the other hand, it’s entirely normal that the form is login-protected so that if we 256 CHAPTER 11 REFACTORING WEB APPLICATIONS happened to type the form URL into the browser without having logged in first, we would in fact be asked to log in. The reason for this example is that a form illustrates more web programming principles than a list page would. The news entry form The example assumes that register_globals is turned on. That’s the directive that lets us use session variables, GET and POST variables, and others as if they were simple global variables with simple names. As the PHP manual reminds us repeatedly, register_globals shouldn’t be turned on. It should be avoided like the plague for security reasons. But there is always the chance that you might come across it, years after it was officially denounced. There is another reason to avoid it as well: it’s critical to avoid confusion and chaos. For reasons of clarity, a session variable and a request variable should never have iden- tical names, and with register_globals turned off, they never will. This point—why unmarked globals are confusing—is one of the things listing 11.6 demonstrates. Even the refactored version is far from perfect and should not necessarily be emu- lated. The process of refactoring is what we’re trying to learn here. The example has problems that we will not be focusing specifically on. Some of these are security issues: • As mentioned, register_globals is dangerous. • The login mechanism itself is rather primitive. • The database code is not secured against SQL injection attacks. • There is no validation or error-checking of user input. session_start(); session_register('current_user'); mysql_connect('localhost','dbuser','secret'); mysql_select_db('ourapp'); if ($username || $current_user) if ($username) { $sql = "SELECT id,username,password FROM Users ". "WHERE password = '".md5($password)."' ". "AND username = '".$username."'"; $r = mysql_query($sql); $current_user = mysql_fetch_assoc($r); } if ($current_user) { if ($headline) { if ($id) { $sql = "UPDATE News SET ". "headline = '".$headline."',". "text = '".$text."' ". "WHERE id = ".$id; Listing 11.6 Login-protected news entry form b Use $_SESSION instead c Logging in or logged in d Check password e Start application code f Updating an existing article SIMPLIFYING CONDITIONAL EXPRESSIONS 257 } else { $sql = "INSERT INTO News ". "(headline,text) ". "VALUES ('".$headline."','" .$text."') "; } mysql_query($sql); header("Location: http://localhost/newslist.php"); exit; } else { if ($id) { $sql = 'SELECT text, headline '. 'FROM News WHERE id = '.$id; $r = mysql_query($sql); list($text,$headline) = mysql_fetch_row($r); } echo '<html>'; echo '<body>'; echo '<h1>Submit news</h1>'; echo '<form method="POST">'; echo '<input type="hidden" name="id"'; echo 'value="'.$id.'">'; echo 'Headline:'; echo '<input type="text" name="headline" '; echo 'value="'.$headline.'"><br>'; echo 'text:'; echo '<textarea name="text" cols="50" rows="20">'; echo ''.$text.'</textarea><br>'; echo '<input type="submit" value="Submit news">'; echo '</form>'; echo '</body>'; echo '</html>'; } } } else { echo '<html>'; echo '<body>'; echo '<h1>Log in</h1>'; echo '<form method="POST">'; echo 'User name: <input type="text" name="username">'; echo '<br>'; echo 'Password : <input type="password" name="password">'; echo '<br>'; echo '<input type="submit" value="Log in">'; echo '</form>'; echo '</body>'; echo '</html>'; } ?> b When register_globals is turned on, session_register() lets us use $current_user instead of $_SESSION['current_user']. In general, this is a bad practice; we’re doing it here to illustrate it and to show how to avoid it. g Creating new article h Execute SQL i Redirect to news list page j Retrieve an existing article The news form 1) The login form 1! 258 CHAPTER 11 REFACTORING WEB APPLICATIONS C $username is an HTTP variable; $current_user is a session variable. There is nothing to indicate that fact. This way of doing it is convenient (less typing), but makes it harder to guess what the variables are doing. If instead we were to use $_SESSION['current_user'] and $_POST['username'], it would effec- tively document where each variable was coming from. The purpose of these variables here is to tell us where we stand with regard to login. If $username is set, it means the user just submitted the login form. If $current_user is set, it means the user is already logged in. The reason there is one conditional branch for both of these cases is that they are the alternatives that don’t require showing the login form. D If the user has submitted the login form, we check whether the user exists in the data- base and has the password the user entered. The passwords are stored in the database table encrypted using the PHP md5() function. They can’t be decrypted, but we can check whether a string matches the password by encrypting the string. E This is where the application code (as opposed to the authentication and login code) starts. $current_user is a session variable. If it’s set, we know that the user is already logged in, no authentication is needed, and we can display the form. F If the HTTP request contains a news article ID, we assume that the user is editing an existing article and build an UPDATE statement based on that. G If not, we assume the user wants to create a new news article and build an INSERT statement. H Then we execute the UPDATE or INSERT statement. I After the database has been successfully updated, we redirect to the news list page. (No, there’s no validation and no error checking. That’s because we want to avoid dealing with too many kinds of complexity in one example.) J If there is a news article ID present when we are ready to show the news form, we assume that it came from an edit link and get the article from the database. 1) The news form has all the HTML code inside echo statements. This is another bad practice that is used in this example just for the sake of illustration. 1! Finally, the login form, which is displayed if the user is not already logged in or trying to log in. Isolating login and authentication How do we start to refactor a beast like this? There are several places we could start. The simplest thing to begin with would be to change some of the long sections of echoed HTML markup into HTML sections. On the other hand, the greatest com- plexity and difficulty is in the conditionals. SIMPLIFYING CONDITIONAL EXPRESSIONS 259 How can we make it clearer which parts of this example do what? The outer con- ditionals are involved in login and authentication. The part that properly belongs to this particular web page is all inside the conditional branch following if ($current_user). So a way to separate the page-specific code from login and authentication is to extract everything inside this branch into a separate function. Or we could place it in a file and include it. The problem with using include for the application content is that it’s exactly the wrong way around. The URL would belong to the login page, and since login will be used for most or all pages, all pages get the same URL. It is possible, and common, to do it that way, and we will get to that later. But we don’t want that to be our only option. So for now it’s better to have URL belong to the news form page, and let that page include the login and authentication code. To do that, it will be helpful to make the login and authentication code more man- ageable. In listing 11.7, the conditional statements related to login and authentication have been isolated so they’re easier to see. if ($username || $current_user) { if ($username) { // Check for the username and password in the database } if ($current_user) { // Do the news form with all its ifs and elses } } else { // Show the login form } There is a standard refactoring we can apply to get started. It’s called Decompose Con- ditional. The principle is to take the content of branches, and the tests as well, if nec- essary, into separate methods or functions. Figure 11.4 shows how this works in principle. The flowchart at left represents the conditional statement. Let’s try it. We’ll make a function out of every single branch in the authentication logic and test to get a feel for how that works (see listing 11.8). Listing 11.7 Authentication-related conditional logic from the previous example Figure 11.4 Decompose Conditional refactoring [...]... not to the index .php script Satisfying the test with fake web page interaction At this stage of development, submitting to ourselves is a good stepping stone It’s convenient at this point that form handling can be dealt with from within the same script, rather than having the form creation in one file and the handling code in another It also prevents the form handling code from getting mixed in with other... contain most GET and POST variables Unless, that is, the variable names are somehow constructed by the PHP code Encapsulate script includes One of the worst problems in PHP web applications is includes that run PHP code in the form of a script In PHP, it’s possible to use an include file that only contains PHP code that is not in the form of functions and classes and just executes the code at the point... move the include statement because the global variables might not be set or might be set incorrectly Figure 11.5 gives some idea of the difficulty The global variable $items is set in the main script file, changed in the include file, and then used again in the main file, but there is no simple way to keep track of its changes Even doing a full search through the main file could be misleading, since you... of a test 12.1.2 272 Setting up web testing We won’t even consider testing this form manually If manually testing a class is hard enough, testing forms with a browser is excruciating Modern browsers have a habit of caching pages and auto-filling form fields, which can be confusing when testing CHAPTER 12 TAKING CONTROL WITH WEB TESTS Not only that, but most web site testing involves cookies, sessions,... little; in that case, reimplementing is almost certainly much more efficient On the other hand, when you only need to make a small change in a large PHP file, throwing everything out may be much too demanding, in the short run at least Turn off register_globals As you may know, register_globals is highly discouraged for security reasons Avoiding it also helps refactoring PHP has several distinct categories... can start replacing the fake web page with something that actually works Our tests are green, and we will be keeping them green 12.2 GETTING A WORKING FORM To change the fake web interface into a real one, we need to add persistence code that saves the Contact object and retrieves it again We’re aiming for an interaction like the one in figure 12.4 The diagram has been simplified by grouping all the persistence... /database/$script")); $transaction->commit(); } } In real life, we would go back and change the other tests from chapter 10, using the Configuration class Right now, we’ll press on, maintaining our focus on getting our web application from fake to real The tests are green again, so we can keep refactoring The index .php page needs to read our data: < ?php require_once(dirname( FILE ) '/ /classes/configuration .php' ); require_once(dirname(... print "{$contact->getName()}\n"; print "{$contact->getEmail()}\n"; print "\n"; } ?> Add contact Now the advantages of top-down design start to shine through The top-level code is dictating the interface to the lower-level code Stubbing this in our contact .php file is easy We must add the findAll() method to ContactFinder: GETTING... showPage() { include($_SERVER['ORIG_PATH_TRANSLATED']); } This is an odd thing to do, since this is now an include file that includes the file that included it It works, but only under the following conditions: • We use include_once or require_once (rather than include or require) in the first file • There are no functions and classes in the first file If there are functions and classes in the first... extracting functions and adding them to classes as the need arises If we extract several functions, we may start seeing that the same variables keep recurring in the argument lists of these functions That kind of variable is a prime candidate for becoming an instance variable in a class Alternatively, if we have some idea of the design we’re moving toward, we may know what kind of class we need In that . ".$id; Listing 11 .6 Login-protected news entry form b Use $_SESSION instead c Logging in or logged in d Check password e Start application code f Updating an existing article SIMPLIFYING CONDITIONAL. user-defined functions, but not with built -in functions, since there is no way to eliminate the existing definitions. (Except by compiling a separate PHP executable for testing and disabling the. by the PHP code. Encapsulate script includes One of the worst problems in PHP web applications is includes that run PHP code in the form of a script. In PHP, it’s possible to use an include

Ngày đăng: 12/08/2014, 21:21

Mục lục

  • PHP in Action

    • Refactoring web applications

      • 11.4 Simplifying conditional expressions

      • 11.4.1 A simple example

      • 11.4.2 A longer example: authentication code

      • 11.4.3 Handling conditional HTML

      • 11.5 Refactoring from procedural to object-oriented

      • 11.5.1 Getting procedural code under test

      • 11.5.2 Doing the refactorings

      • 11.6 Summary

      • Taking control with web tests

        • 12.1 Revisiting the contact manager

        • 12.1.1 The mock-up

        • 12.1.2 Setting up web testing

        • 12.1.3 Satisfying the test with fake web page interaction

        • 12.1.4 Write once, test everywhere

        • 12.2 Getting a working form

        • 12.2.1 Trying to save the contact to the database

        • 12.2.2 Setting up the database

        • 12.2.3 Stubbing out the finder

        • 12.3 Quality assurance

        • 12.3.1 Making the contact manager unit-testable

        • 12.3.2 From use case to acceptance test

Tài liệu cùng người dùng

Tài liệu liên quan