Best Coding Practices: PHP

Posted on Jan 12, 2010 by Panda
Best Coding Practices: PHP

What's the saying about code? Written today, forgotten tomorrow? As developers, we all like to think that simply because we wrote something, we know exactly how it works, but even the best of us can look back on something we've written a month ago and have to take a few minutes to figure out exactly what we were thinking at the time. Then, there's the possibility that other developers will be working with your code. In this case, poorly commented code can cause major headaches for otherwise easy fixes. In this article, I'm going to discuss a couple things you can do when writing PHP in order to make your life, and the lives of developers working with your code, a lot easier.

Close brackets with comments

Although most of the time you can avoid it by breaking your functions down into smaller, "single purpose" functions, sometimes you'll still end up with a large number of nested closing brackets. Example:

if(is_array($data)) {
    if(!empty($data['tables'])) {
        $tables_to_install = $this->install_table_exists($data['tables']);
        if(!empty($tables_to_install)) {
            foreach($tables_to_install as $install_table) {
                if(!$this->install_table($install_table, $data['tables'][$install_table])) {
                    return FALSE;
                }
                $success_tables[] = "Table '{$install_table}' installed successfully.";
                if(!empty($data['data'][$install_table])) {
                    if(!$this->install_data($install_table, $data['data'][$install_table])) {
                        return FALSE;
                    }
                    $success_tables[] = "Data for table '{$install_table}' installed successfully.";
                }
            }
        }
    }
} 

That's a lot of closing brackets. In this case, most of them are for if statements, but there's a foreach thrown in there. The code is properly tabbed, so it still only takes a moment to see what's what, but take a look at the same block of code with commented closing brackets:

if(is_array($data)) {
    if(!empty($data['tables'])) {
        $tables_to_install = $this->install_table_exists($data['tables']);
        if(!empty($tables_to_install)) {
            foreach($tables_to_install as $install_table) {
                if(!$this->install_table($install_table, $data['tables'][$install_table])) {
                    return FALSE;
                } // if
                $success_tables[] = "Table '{$install_table}' installed successfully.";
                if(!empty($data['data'][$install_table])) {
                    if(!$this->install_data($install_table, $data['data'][$install_table])) {
                        return FALSE;
                    } // if
                    $success_tables[] = "Data for table '{$install_table}' installed successfully.";
                } // if
            } // foreach
        } // if
    } // if
} // if

It only takes a quick glance to see what tags the closing brackets correspond to. This is a good habit to get into as it only takes a second to write, and saves seconds (or even minutes) for you and any other developer without requiring them to follow everything you've written in that chunk.

ALWAYS document every function, regardless of how simple it is

This is probably the most important piece of advice I can give for developers: if it's worth writing the function, it's absolutely, 100% worth writing properly commented documentation for it. No matter how simple, small or insignificant the function is, it only takes a few seconds of your time to write a summary of the function.

I'd recommend covering a function's: Purpose, Parameters, Return Value, Usage example (for public functions). In addition to this, line-by-line commenting is also strongly recommended, as you're basically providing anyone reading your code with a solid summary of what the function does, how it works, the expected results and how to use it, followed by a break-down of how the function is accomplishing that. Take a look at the example below:

private function get_url_controller_and_arguments_string() {
    $controller_and_args_string = NULL;
    if(!empty($_SERVER['PATH_INFO'])) {
        $controller_and_args_string = $_SERVER['PATH_INFO'];
        $this->debug->log('Using $_SERVER[\'PATH_INFO\'] for URL argument string.');
    } else {
        if(PATH_DOMAIN_DIRECTORY != '') {
            $expected_path_prefix_string = PATH_DOMAIN_DIRECTORY;
            $exploded_script_name_array = explode($expected_path_prefix_string, $_SERVER['SCRIPT_NAME']);
            $controller_and_args_string = $exploded_script_name_array[1];
        } else {
            $controller_and_args_string = $_SERVER['SCRIPT_NAME'];
        }
        $controller_and_args_string = preg_replace('/.*index[.]php$/', NULL, $controller_and_args_string);
        $this->debug->log('Using $_SERVER[\'SCRIPT_NAME\'] for URL argument string.');
    }
    $controller_and_args_string = strtolower(str_replace('-', '_', preg_replace('%[^a-zA-Z0-9_\/-]*%', NULL, $controller_and_args_string)));
    return preg_replace('%^[/]?(.*[^/])|[/][/]?$%', '$1', $controller_and_args_string);
}

While it might be fairly obvious what this function does by the name alone, how it works and why it's written the way it's written might not be immediately apparent to your unfortunate future self or fellow developer. Now, let's take a look at the same piece of code written with our function summary at the top, followed by well-written comments for each action we're taking:

private function get_url_controller_and_arguments_string() {
//////////////////////////////////////////////////////
// PURPOSE: We want to get the current controller and
//  any additional arguments that may be contained
//  therein.  There are two potential ways of getting
//  the controller/and/arguments...
//
//  First, there's the global var $_SERVER['PATH_INFO']
//  which will give us:
//  index.php'/everything/after/the/index' as a string.
//  That is, if we're still displaying index.php.
//  If the server uses URL rewriting via mod_rewrite
//  (.htaccess) then this is worthless to us.
//
//  Next, there's the global var $_SERVER['SCRIPT_NAME'].
//  Now, if mod_rewrite is turned on and 'index.php' is
//  being removed from the URL, this variable will
//  actually give us the full path, including any
//  additional arguments.  So far so good, right?  Only
//  problem is if we're running this from a directory
//  other than the domain root it's going to give us that
//  as a part of the path.  Not a problem, because we've
//  defined 'PATH_DOMAIN_DIRECTORY' which we can then
//  strip from the beginning of this script name.
//
//  Ultimately, the final data this method provides still
//  is not 'secure' because 'core' still has to parse it
//  and check for available files.
// ------------------------------------------------------
// PARAMETERS: None
// ------------------------------------------------------
// RETURNS:
//  STRING containing the controller/and/arguments (no
//      slashes at the start or end) if something is
//      found.
//  NULL if nothing is found.
// ------------------------------------------------------
// EXAMPLES:
//  EX 1) 'PATH_DOMAIN_DIRECTORY' is 'prefix',
//      mod_rewrite is OFF
//  /prefix/index.php/path/goes/here
//  Returns: path/goes/here
//
//  EX 2) 'PATH_DOMAIN_DIRECTORY' is '',
//      mod_rewrite is OFF
//  /prefix/index.php/path/goes/here
//  Returns: path/goes/here
//
//  EX 3) 'PATH_DOMAIN_DIRECTORY' is '',
//      mod_rewrite is ON
//  /prefix/path/goes/here
//  Returns: prefix/path/goes/here
//
//  In the case of example 2, this would work for
//  loading the controller but keep in mind that
//  other libraries rely on 'PATH_DOMAIN_DIRECTORY'
//  so things like anchor tags generated by the 'url'
//  lib would not work as expected -- only shown as
//  an example of returned values.
//////////////////////////////////////////////////////

$controller_and_args_string = NULL;

// Check for $_SERVER['PATH_INFO'] (In the case that
// mod_rewrite is OFF or /index.php/ has been included
// in the URL anyway by user input)
if(!empty($_SERVER['PATH_INFO'])) {
    $controller_and_args_string = $_SERVER['PATH_INFO'];

    // Debug log update
    $this->debug->log('Using $_SERVER[\'PATH_INFO\'] for URL argument string.');
// Since PATH_INFO doesn't exist, we're going to be
// working with SCRIPT_NAME Now, it's possible that
// mod_rewrite is still OFF, but that we've simply
// not been given ANY input (domain.com/), so we'll
// check for that too
} else { // if
    // If a 'PATH_DOMAIN_DIRECTORY' prefix has been
    // specified, let's remove that from the
    // beginning of the $_SERVER['SCRIPT_NAME'] var
    if(PATH_DOMAIN_DIRECTORY != '') {
        $expected_path_prefix_string = PATH_DOMAIN_DIRECTORY;
        $exploded_script_name_array = explode($expected_path_prefix_string, $_SERVER['SCRIPT_NAME']);
        $controller_and_args_string = $exploded_script_name_array[1];
    // If PATH_DOMAIN_DIRECTORY was empty then we
    // can go ahead and just use $_SERVER['SCRIPT_NAME']
    } else { // if
        $controller_and_args_string = $_SERVER['SCRIPT_NAME'];
    } // if

    // Now we can go ahead and strip the index.php
    // from the end of our script name.  We're also
    // going to be stripping out everything BEFORE
    // the index.php file.  There should be no
    // arguments provided prior to index.php (if it
    // exists in the string), only after it.
    // This leaves the string alone if there was no
    // index.php in it.  For example if
    // PATH_DOMAIN_DIRECTORY was set to 'test', and
    // the URL is /test/goes/here/, then we'd still
    // be setting the var to '/goes/here/'.  However,
    // if the PATH_DOMAIN_DIRECTORY is set to 'test'
    // and the URL is /test/goes/here/index.php'
    // then clearly PATH_DOMAIN_DIRECTORY is wrong.
    $controller_and_args_string = preg_replace('/.*index[.]php$/', NULL, $controller_and_args_string);

    // Debug log update
    $this->debug->log('Using $_SERVER[\'SCRIPT_NAME\'] for URL argument string.');
} // if

// We just need to "clean" our path and convert it
// to the following characters: a-z, A-Z, 0-9, -
// and _.  Dashes aren't available for class names,
// so we're going to convert those to underscores,
// but considering dashes are typically considered
// more SEO friendly we still want to support them!
// Also, we're dropping the whole thing to lowercase.
// We'll just need to strip the slashes next.
$controller_and_args_string = strtolower(str_replace('-', '_', preg_replace('%[^a-zA-Z0-9_\/-]*%', NULL, $controller_and_args_string)));

// Now that we've got a path regardless of how it
// was put together, we can remove the
// beginning/trailing slashes from our string and
// return that.  If it so happens that our string is
// ONLY a slash at this point, that's fine.  That just
// means we'll be returning a NULL string which we
// had from the start.
return preg_replace('%^[/]?(.*[^/])|[/][/]?$%', '$1', $controller_and_args_string);
} // function

Seem a bit verbose? Perhaps, but I can guarantee that code written like this will almost NEVER confuse you or your fellow developers when someone has to go in and make some changes. I hope this little article is helpful, and I plan on releasing a few more articles on the subject of coding practices for HTML, CSS, Javascript and of course PHP. (All code examples used from the underlying custom framework running this very website, written by Paul Hesson)

Post a Comment

Comments

Unregistered User's Avatar
Unregistered User
Posted Feb 12, 2010

Instead of commenting every closing brackets, you could use this less known notation:

http://www.php.net/manual/en/control-structures.alternative-syntax.php

[code]

if(is_ready($dinner)):

foreach($dish as $food):

serve($food);

endforeach;

endif;

[/code]

It achieves the very same result with semantically relevant code;

*philosp*

Unregistered User's Avatar
Unregistered User
Posted Feb 13, 2010

Thankyouu for the tips :) stumbled from the UK :D

Unregistered User's Avatar
Unregistered User
Posted Jun 18, 2010

I can definitely agree with this point, Paul, as I'm one of those people who comes back later having completely forgotten what it is I've written. The documentation in your example is exceptionally thorough though - perhaps there's a length compromise somewhere that doesn't take too much time but still allows others to see what's going on.

This is relevant for CSS as well, although in that case there's a bit more concern about file size. Even so I try to separate out what's going on in the hopes that it will help me figure it out later (or at the very least, give me a quick way to use Find to get between sections).

Unregistered User's Avatar
Unregistered User
Posted Jul 06, 2010

In my opinion, these principles are "mission critical" when wriintg ANY code.

I recently spent half a day searching line by line in 500 lines of Actionscript to update a basic [ nested] function written three years ago by someone who vanished into thin air after making the vanilla app for the business I work for.

I ended up commented this code myself. Coulda` saved a ton of time and headache if this was done from the get go. All those closing brackets and divtags need reference love.

Post a Comment

  • You are not currently logged in:
  • Unregistered users' comments are moderated, so your post won't be viewable until it's approved.
  • You will only be able to post once every 15 minutes as an unregistered user.
  • Registered users can post every 30 seconds, and their comments appear immediately!
  • Registered users can ignore other users' comments, including all unregistered users if they choose
  • Don't already have an account? Register here!