In PHP 5 and greater there is a magic function called __autoload (note: two underscores). __autoload allows us to load a class on the fly so to speak. It only gets called when a class can not be found, then it has a chance to load the class. If the class still can not be found, you get your regular error stating so. __autoload can be really useful if all of your classes happen to be in one directory, but what happens if you are managing a large project with many packages of classes? We will look at some different options.
This is the simplest solution, and the best if you have a small project. You may have situations where you would like to load the class without creating a new instance of it, so I will be creating a loadClass function in each example which __autoload will call. That way you may load a class manually. Here is what you might do if all of your classes are in one directory:
function loadClass($className) {
include_once('classes/' . $className . '.php');
}
Pretty Simple.
Now how about package management, with many directories of classes? A few developers have named their classes with the package names in the class name. For example, util_Parser would be the Parser class in the util directory (in your base classes directory), and util_html_HTMLParser would be the HTML parser class in the util.html package. Here is an example of how that would work:
function loadClass($className) {
include_once('classes/' . str_replace('_', '/', $className) . '.php');
}
Simple like the first method, but we get packages now. However, we have to actually name our classes with the package names in front of it. We start to get very long names for some of our classes. If we extended the Parser class we might have util_html_HTMLParser and util_xml_XMLParser or perhaps even deeper packages. We also find refactoring difficult because if a class must move to another package, we must go through all our code and rename the class everywhere we use it.
importThere is another way, although not as simple, which allows packages as deep as you would like, and requires minimal work when refactoring. This is the import way of doing things. import is a function which keeps track of what classes you might use in a certain script/class and then uses __autoload to look up the location of the class. Here's how it works.
import function like this, import('util.html.HTMLParser');, at the top of your scriptimport stores the name of the class, 'HTMLParser', and the path to the class, 'util/html/HTMLParser.php', in a global associative array called imports__autoload will do a lookup in the imports array for the class and load it in.Here is how the code looks. It is a bit more complicated, but very nice to use for a larger system:
function loadClass($className) {
global $imports;
if (isset($imports[$className])) {
include_once($imports[$className]);
}
}
$imports = array();
function import($import) {
global $imports;
// seperate import into a package and a class
$lastDot = strrpos($import, '.');
$class = $lastDot ? substr($import, $lastDot + 1) : $import;
$package = substr($import, 0, $lastDot);
// if this import has already happened, return true
if (isset($imports[$class]) || isset($imports[$package.'.*'])) return true;
// create a folder path out of the package name
$folder = '' . ($package ? str_replace('.', '/', $package) : '');
$file = "$folder/$class.php";
// make sure the folder exists
if (!file_exists($folder)) {
$back = debug_backtrace();
return trigger_error("There is no such package <strong>'$package'</strong> -- Checked folder <strong>'$folder'</strong><br />
Imported from <strong>'{$back[0]['file']}'</strong> on line <strong>'{$back[0]['line']}'</strong><br />", E_USER_WARNING);
} elseif ($class != '*' && !file_exists($file)) {
$back = debug_backtrace();
return trigger_error("There is no such Class <strong>'$import'</strong> -- Checked for file <strong>'$file'</strong><br />
Imported from <strong>'{$back[0]['file']}'</strong> on line <strong>'{$back[0]['line']}'</strong><br />", E_USER_WARNING);
}
if ($class != '*') {
// add the class and it's file location to the imports array
$imports[$class] = $file;
} else {
// add all the classes from this package and their file location to the imports array
// first log the fact that this whole package was alread imported
$imports["$package.*"] = 1;
$dir = opendir($folder);
while (($file = readdir($dir)) !== false) {
if (strrpos($file, '.php')) {
$class = str_replace('.php', '', $file);
// put it in the import array!
$imports[$class] = "$folder/$file";
}
}
}
}
There you have it. A powerful way to manage your packages and load only classes which are needed for use. Also, a feature of this method of autoloading classes is that you can use an asterisk, *, to import all the classes in a package. And it only loads the classes needed because of __autoload.
Finally, this is how you would use the import method in your scripts:
// here would be all of your important code
// not worrying about including files hwen you need them
// because you've already set it up to include them
// automatically when you use them, and only
// _if_ you use them
$p = new HTMLParser();
$t = new ArrasTemplate();
// etc.
Thanks for this – solved my problem almost exactly. I wasn’t interested in packages, just stopping loads of classes being loaded until they were needed and wanted so this was pretty much what I wanted.
Thanks again
It should prolly be noted, since at some point someone’s going to use this and watch their app explode ( especially since alot of people are attempting to write real programs with PHP these days), that this is package management for the sake file organization but it doesn’t provide the name space protection of true packages. So if you have two classes in two packages called… oh lets say, “database.Record” and “media.music.Record” you’re SOL. Someday PHP will grow up, but until then this is a nice hack for keeping things organized.
Very true Vance. The first method is safer if you name the classes package_subpackage_Class. Of course, if you’re using existing libraries and you’ve got a database Record class and a music Record class your SOL whether you use this way of importing or not.
Interesting. Coming from a Java background, and now learning to use PHP5, this solution suits me.
One thought re name collision.. why not name the php file com.mycompany.mypackage.myclass.php ? i.e use the package name in the filename, rather than the directory structure? This way, all clases live in one folder, but have a unique name
Just a thought
Having all the classes in one folder would drive me crazy. But you could certainly modify the code to do that. :) Wouldn’t be difficult.
Its really intresting. I am a J2EE developer, really liked this article.
Cheers,
Ujjwal B Soni
I’ve added you to a “Thanks to” page on our website. It’s proved very useful to us.
The link is http://www.whosup4.com/anon/technology/
Thanks again.
Hi,
it’s interesting, many thanks.
i need further feature if possible; suppose test1.php is in the folowing directory myProject/test/views/test1.php; and my class Class1.php is in myProject/classes/Class1.php, so when using your function in test1.php like this:
import(classes.Class1.php) i got an error saying no folder… the error is clear because test1.php looks for, from its current directory (views), the directory classes and it doesn’t find it. how can i overcome this problem?
regards
PHP 5.3 is introducing name space support. More details are here :-
http://blog.agoraproduction.com/index.php?/archives/47-PHP-Namespaces-Part-1-Basic-usage-gotchas.html
and here
http://developers.slashdot.org
and here
/article.pl?sid=08/10/26/1610259&from=rss
http://wiki.php.net/rfc/namespaceseparator
I believe doing an import without name spaces support will not work for large code bases. Till the time sticking to X_Y_Z pattern is safer.
You have done really good job, thanx, its very useful for my current project, thanx once again, can you tell me that the function import is working actually include() all class file?
if you do an import(‘mypackage.*’); it will “register” all the filenames so that when a class is referenced it can know where to include() the PHP from. include() only happens when actually using the class you need. However, using “*” in the import will scan the directory for the filenames, so use sparingly.
thanks, great job, this really gave me some good ideas for my projects, its very interesting.
for that thing of “*” you could use some regular expressions instead of strrpos to make it more restrictive with the uppercase convention and the “.php” ending extension, something like this:
if (preg_match(‘/^[A-Z](?.*).php$/’, $file,$class)) {
$imports[$class['name']] = “$folder/$file”;
}
well i repeat the code in another for cause the system erase the name tag after the “?” haha, hopes this comes useful
if (preg_match(’/^[A-Z](.*).php$/’, $file,$class)) {
$imports[$class[1]] = “$folder/$file”;
}
thanks again.
Thanks, this is very cool. Is it available for use via a public license? Without one explicitly given it’s technically not legal to reuse since you can claim copyright at any time (an implicit copyright.)
A few example licenses:
MIT License allows for most use but you must include the copyright notice every time: http://www.opensource.org/licenses/mit-license.php
Public domain has no restrictions at all: http://creativecommons.org/licenses/publicdomain/
Anyway, thanks a lot!
This is under the creative commons Attribution license (http://creativecommons.org/licenses/by/3.0/).
how i can get the php package dawnlode
anisa: copy/paste into a file.
My own derivation from your code:
The following is placed in classes/loader.inc.php and included via require_once(‘classes/loader.inc.php’);
It will turn this
new TagStorageData()
into
require_once(‘classes/tag/storage/tag_storage_data.class.php’);
and
new ITagStorageData()
into
require_once(‘classes/tag/storage/tag_storage_data.interface.php’);
define(‘CLASS_EXT’, ‘.class.php’);
define(‘INTERFACE_EXT’, ‘.interface.php’);
function __autoload ($className) {
loadClass($className);
}
function loadClass ($className) {
$classesRootFolder = dirname(__FILE__);
$classFileExtension = ”;
if(preg_match(‘/^I[A-Z]/’, $className)) { // Interface
$classFileExtension = INTERFACE_EXT;
$className = preg_replace(‘/^I([A-Z])/’, ‘$1′, $className);
} else { // Class
$classFileExtension = CLASS_EXT;
}
$parts = preg_split(‘/(?<!^)(?=[A-Z])/', $className);
$classFileName = strtolower(implode('_', $parts).$classFileExtension);
array_pop($parts); // Drop final element
$classFileName = strtolower(implode('/', $parts)).'/'.$classFileName;
echo "Auto-loading Class: $className in $classFileName\n";
$classFileName = $classesRootFolder.'/'.$classFileName;
include_once($classFileName);
}
Jacob’s solution can be misleading in two ways.
Firstly, it creates the illusion that there are some kind of packages in PHP, which is not the case, so the problem of name collisions remains. Howie have already pointed that out.
Secondly, declaring “imported” classes at the top of the file does not imply that those are the only dependencies required by the script. Some classes might already have loaded other classes too. It’s kind of a hint, though.
Personally, since there is no notion of a package in PHP (though in PHP 5.3 namespaces are introduced), i find there is no need to specify the exact location of the “imported class”. There is no way for explicitly declaring the dependencies either. So, basically the only things i want to (and can) achieve are:
1. Get rid of the require or include statements
2. Organize files in some kind of hierarchy
The idea is just to build the include_path to contain all the subdirectories. That way i can easily move files around without worrying about any class loading issues.
init();
function init() {
$path = implode(dirs(“classes”), PATH_SEPARATOR);
set_include_path($path);
main();
}
function dirs($path) {
$dirs = array($path);
$contents = scandir($path);
foreach ($contents as $filename) {
if (ignored($filename)) {
continue;
}
$item = “{$path}/{$filename}”;
if (is_dir($item)) {
$dirs = array_merge($dirs, dirs($item));
}
}
return $dirs;
}
function ignored($filename) {
return preg_match(“/^\./”, $filename);
}
function __autoload($class) {
require_once(“{$class}.php”);
}
I prefer Andrey’s solution. I see no need to explicitly list directories. Manipulate the include_path to include all the directories where files would be located.
Consider whether a solution like this is really needed before implementing it.
Lastly, I think it would be a mistake to try to make PHP look like Java.