Monday, January 27, 2014

Implementing PHP namespaces in an existing project

If you've been developing for a longer time you know the problem. You started coding a project and now PHP comes with this new cool feature. At first you want to stay backwards compatible with older versions but there comes a point where you want to use these new features. Otherwise your code will be outdated and less attractive to new developers.
Such a feature we didn't have were namespaces. Namespaces are very useful to organize code and to avoid naming conflicts when you include a vendor library. For example you can have a function called "sendmail()". Now you'll have a problem when you want to include a 3rd party mail library which uses the exact same function.

With Group-Office we solved this problem before PHP namespaces arrived by prefixing. For example a class in the address book module would be called "GO_Addressbook_Model_Contact". All of our classes where prefixed by "GO" and followed up by the module name. This way we hacked our namespaces in with underscores. This was invented before PHP 5.3 became mainstream. Now almost all servers run PHP 5.3 and higher so it's time to change to namespaces! But this can be a tricky task which such a large project as Group-Office. But because we used the underscore namespace hack it wasn't so difficult at all. I wrote up with a script that could convert our project in one GO! It turns

class GO_Addressbook_Model_Contact{} into:

<?php

namespace \GO\Addressbook\Model;

class Contact{

}


Some prerequisites

While writing this script I did face a couple of problems I had to resolve manually. We used some names that where incompatible. For example:

GO_Base_Util_Array would convert into:

<?php

namespace GO\Base\Util;


class Array{

}


Using "Array" as a class name is invalid in PHP so I had to refactor these names. I found out that refactoring these names before using the script was the best approach. This way we could use Netbeans to easily refactor these names.
The list of illegal names was:

  • Function
  • Interface
  • Abstract
  • Switch
  • List
There are probably more but these where the only ones used in Group-Office.

I also had to make sure all existing PHP classes that were used are prefixed with a backslash. For example "new PDO" had to be written as "new \PDO".

The script

This script assumes "GO" is a global static singleton class and all classes are written in the GO_Module_Type_Class form. It also requires one class per file. You could easily modify it to work for your project if you meet these requirements.


<?php
/**
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Intermesh BV 
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */


//find all PHP files except updates.php and updates.inc.php because we shouldn't touch them
$cmd = 'find . -type f \( -iname "*.php" ! -iname "updates*" \);';
exec($cmd, $scripts, $return_var);

//return var should be 0 otherwise something went wrong
if($return_var!=0)
 exit("Find command did not run successfully.\n");

foreach($scripts as $script){
 
 //skip old files. We don't use .inc.php anymore in the new framework
 if(substr($script,-14)=='.class.inc.php' || in_array(basename($script),array('namespacer.php','action.php','json.php')))
  continue;
 
 
 
 //get the contents of the PHP Script
 $content = $oldContent = file_get_contents($script);
 
 //Our main global static function GO::function() is easiest to identify like this
 $content = str_replace('GO::', '\\GO::', $content);
 
 //All GO classes are build up like GO_Module_SomeClass so we can match them with
 //a regular expression.
 $regex = '/[^A-Za-z0-9_-](GO(_[A-Za-z0-9]+)+)\b/';
 
 $classes = array();
 
 if(preg_match_all($regex, $content, $matches))
 {
  
  
  //loop through the matched class names and store the old classname as key
  //and the new classname as value
  foreach($matches[1] as $className){
   
   //skip all uppercase classnames. They are old eg. GO_USERS, GO_LINKS
   if($className!=strtoupper($className)){ 
    if(!in_array($className, $classes)){
     $classes[$className]='\\'.str_replace('_','\\', $className);
    }
   }
  }
  
  //replace all old class names with the new namespaced ones.
  foreach($classes as $oldClassName=>$newClassName){
   $content = str_replace($oldClassName, $newClassName, $content);
  }
  
  //now we have a problem with the class declarations.  
  //we only have one class per file!
  foreach($classes as $oldClassName=>$newClassName){
   $classDeclarationRegex = '/(class|interface)\s('.preg_quote($newClassName,'/').')/';

            //Attempt to find a class definition in this file.
   if(preg_match($classDeclarationRegex,$content, $classDeclarationMatches,PREG_OFFSET_CAPTURE)){
    
    echo "Found ".$newClassName."\n";
    
    //strip last part of the class name to become the namespace.
    //eg. class; \GO\Email\Model\ImapMessageAttachment will have namespace:
    //GO\Email\Model
    $namespace = trim($newClassName,'\\');
    $lastBackSlashPos = strrpos($namespace,'\\');
    $namespace = substr($namespace,0, $lastBackSlashPos);
    

    //find place in the file to enter the "namespace GO\Email\Model;" declaration.
    //we can do this above the line with declaration "class ImapMessageAttachment"    
    $offset = $classDeclarationMatches[0][1];
    $lastLineBreakPos = strrpos(substr($content,0,$offset), "\n");
    
    $declaration = "\n\nnamespace ".$namespace.";\n\n";

                //Inset the declaration in the file content
    $firstPart = substr($content,0,$lastLineBreakPos);
    $lastPart = substr($content, $lastLineBreakPos);    
    $content = $firstPart.$declaration.$lastPart;
    
    //now we must remove the namespace from class usages in this file.
                //eg. \GO\Base\Db\ActiveRecord becomes ActiveRecord.
    $content = preg_replace('/([^"\'])\\\\'.preg_quote($namespace,'/').'\\\\/', "$1", $content);

   }
  }
  

 }
 
 //some doubles could have been made
 $content = str_replace('\\\\GO', '\\GO', $content);
 
 //if the contents were modified then write them to the file.
 if($oldContent != $content){
  echo "\nReplacing $script\n";
  file_put_contents($script, $content);
 }


    echo "All done!\n";
 
}

3 comments:

  1. Hey there, You have done an excellent job. I’ll certainly Digg it and personally recommend to my friends. I’m confident they’ll be benefited from this web site great tips. very well-written, keyword-oriented and incredibly useful. its really interesting to many readers. I really appreciate this, thanks

    ReplyDelete
  2. Great article, It's one of the best content in your site. I really impressed the post. Good work keep it up. Thanks for sharing the wonderful post.
    Best Chicken kurti

    ReplyDelete