After not really working with PhP for a long time, I am moving into php 5 territory. One of the things I am now trying to figure out, is how to use spl autoload functionality. And before I make stupid beginners errors, could you please confirm / advice:
As far as I understand, the SPL_autoload does not mean there is no need for includes anymore; I will still need to include the configuration I want to use manually like this:
require_once("includess/php_ini_settings.php");
In the php_ini_settings.php I subsequently can run an autoloader, to load all the php files in a certain directory, such as my classes directory:
// Directory for classes
define('CLASS_DIR', 'classes/');
// Add classes dir to include path
set_include_path(CLASS_DIR);
spl_autoload_extensions(".php"); // comma-separated list
spl_autoload_register();
Is this indeed the right (and most efficient) way to autoload classes into all my pages?
-- added: --
It is mentioned that unless you use a different naming scheme, the is no need to specify an autoloader. I assume the naming scheme default is using the class name as filename, in non-caps?
You don't really need the spl_autoload_extensions() and spl_autoload_register() part, unless you use a different naming scheme. So you basically just need to add your class path to the include path, like you already do.
I suggest using SPL_autoload_suxx() from http://bugs.php.net/49625 as your __autoload() function though to have more sensible case-sensitivity:
function __autoload($cn) {
$rc = false;
$exts = explode(',', spl_autoload_extensions());
$sep = (substr(PHP_OS, 0, 3) == 'Win') ? ';' : ':';
$paths = explode($sep, ini_get('include_path'));
foreach ($paths as $path) {
foreach ($exts as $ext) {
$file = $path.DIRECTORY_SEPARATOR.$cn.$ext;
if (is_readable($file)) {
require_once $file;
$rc = $file;
break;
}
}
}
return $rc;
}
Related
I'm actually trying to create a MVC framework for my own, however I'm having troubles with the Autoload. It's not a problem actually, but I'd like to ask the gurus, how are they using the spl_autoload_register function when there are different directories.
Lets say we have the following directories:
Controllers
Libs
Models
Each folder contains different classes, like:
Controllers:
Main.php
File.php
About.php
Libs:
Main.php
Front_controller.php
Models:
Index.php
File.php
Login.php
You can notice that some file names might be found with the same name in different directories. Okay, so this is what I've tried so far:
spl_autoload_register(function ($class) {
$pathContorllers = 'Controllers/' . $class . '.php';
$pathLibs = 'Libs/' . $class . '.php';
$pathModels = 'Models/' . $class . '.php';
if (file_exists($pathContorllers)) {
require_once $pathContorllers;
} elseif (file_exists($pathLibs)) {
require_once $pathLibs;
} elseif (file_exists($pathModels )) {
require_once $pathModels ;
}
});
It is working well, however I'm sure that there is another way to make everything simpler. Can anyone suggest me how can I make this code better or simpler / what are gurus using in this situation?
For the purpose of keeping individuals who may come across this answer from obtaining out-of-date information I have updated it in regards to the latest PSR autoloading standards. The original answer has been maintained for historical purposes and for those who are only interested in the PSR-0 autoloader.
Updated Answer
The PHP-FIG has officially deprecated the PSR-0 standard in favor of the alternative autoloader, PSR-4. Although the two are similar in some aspects they are also very different in others. (E.g.: the handling of underscores in class names.)
You may be thinking to yourself -- "I use PSR-0 now and it works fine." The truth of the matter is that PSR-0 will still work fine for certain projects. This is especially true when backwards compatibility with a package that doesn't use namespaces is concerned. PSR-0 is still a decent autoloading principle, but it has its own shortcomings.
Of course, if there is one thing that is a constant with programming, it is that code eventually changes and programming techniques continue to evolve. You can do yourself a favor today by preparing yourself for tomorrow. Therefore, if you are just starting a project or are trying to port a project to a newer version of PHP that can use namespaces, you should seriously consider using the PSR-4 autoloader.
It is also worth noting that if you are developing a project that does not use namespaces then PSR-4 does not apply to you. In this case PSR-0 or your own custom autoloader applies.
Original Answer
If you want to go with namespaces in your classes, then the PSR-0 route is a pretty good way to autoload. Basically your namespace represents you directory structure and classes can be loaded based on a convention.
If the PSR-0 method doesn't meet all your needs (or doesn't play nice with existing code) you can still add more functions with spl_autoload_register and PHP will go through them one by one in an attempt to load classes.
Example usage:
First thing is first, if you aren't familiar with namespaces in PHP then you will benefit from checking out the PHP manual on the subject. They can be a bit confusing at first, but their benefits are worth the initial confusion.
So I said that PSR-0 works by associating your namespaces with your directory structure. Let's use your directories for an example. You have in your root folder (wherever it may be) the following:
Project directory: <- Let's call this directory "MyProject"
Controllers:
Main.php
File.php
About.php
Libs:
Main.php
Front_controller.php
Models:
Index.php
File.php
Login.php
index.php <- Let's say this is your entry point file, this is where you will be autoloading stuff from.
Now let's take a look at your controller Main.php. Two things to keep in mind is that the class name needs to be the name of the file and the namespace for that class is the directory path to that file. So Main.php should look something like this:
<?php
namespace MyProject\Controllers;
class Main {
//Field vars, contructor, methods, etc. all go here.
}
?>
You would do the same thing for your your Login model
<?php
namespace MyProject\Models;
class Login {
//Field vars, contructor, methods, etc. all go here.
}
?>
Now in your index.php file (out in the root directory - MyProject) you would make your call to the spl_autoload_register and give it the PSR-0 autoloader.
spl_autoload_register( function ($className) {
$className = ltrim($className, '\\');
$fileName = '';
$namespace = '';
if ($lastNsPos = strrpos($className, '\\')) {
$namespace = substr($className, 0, $lastNsPos);
$className = substr($className, $lastNsPos + 1);
$fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
}
$fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';
require $fileName;
});
//Now you can make your call to your objects without a bunch of include/require statements
$main = new \MyProject\Controllers\Main(); //Instantiates your 'Main' controller
$login = new \MyProject\Models\Login(); //Instantiates your 'Login' model
Hopefully that helps make better sense of it, and again, if you don't want to use namespaces you can always just keep adding closures into the SPL autoload stack. You can have 10 different autoloaders in there if you want and PHP will go through them one by one (in the order you defined them) using each function to try and load a class. However, a couple convention based autoloaders is a bit cleaner and more of a preferred method. Also keep in mind that the autoloader translates both namespace separators \ and underscores _ as a directory separator. So your Front_controller.php would not autoload as you would expect.
The code below will help. But I'll advice you check on Namespaces.
spl_autoload_register ( function ($class) {
$sources = array("Controllers/$class.php", "Lib/$class.php ", "Models/$class.php " );
foreach ($sources as $source) {
if (file_exists($source)) {
require_once $source;
}
}
});
The folder structure is like so:
/main.php
/lib/class1.php
/lib/class2.php
/lib/class3.php
I want to have main.php make available all the classes in lib without doing a ton of require/include. Is this possible? Is it possible to just include all files within a directory?
Create an autoloading function to load the class directly from your URL
function __autoload($class_name) {
include "/lib/".$class_name . '.php'; //Add your folder structure like this
// ^ Change the path to your specific need
}
//Then Simply
$class1_object = new Class1();
Yes, yes it is entirely possible and quite easy to do. Best way to do so would be to utilize http://uk1.php.net/autoload
I would suggest you use an autoload function like #Puciek suggested.
If you are curious about doing it yourself though, you can do something like:
$path = "lib/";
foreach(new \DirectoryIterator($path) as $lib){
if(strstr($lib->getFilename(), ".php")){
require_once $path.$lib->getFilename();
}
}
The above will use DirectoryIterator which is part of the PHP SPL.
$path = '/lib/';
$dir = new DirectoryIterator($path);
foreach($dir as $spl){
if($spl->getExtension() != 'php') continue;
include($spl);
}
Basically I looped through the directory's files, filtered out the not php files, then included them. You can feel free to add more filters like files starting with "class" as filenames.
You can also have a look to glob() function to php, which returns an array with all the files within a directory:
http://it2.php.net/manual/en/function.glob.php
The good thing about the directory iterator is that you can use a RecursiveDirectoryIterator alongside with a RecursiveIteratorIterator to iterate through a directory tree in a similar way if you want, to get all the files as well.
Autoloading would be a good option for this but that assumes there is a way to map class names to file names, if that is an option you should follow #puciek advise.
Other options would be to create a single include that includes all of the files you want for example in /lib create a file called all.php and define it as
include("class1.php");
include("class2.php");
include("class3.php");
then in your script you could just include("lib/all.php");
Another option would be to glob the files in a directory then loop through the resultant array and issue an include.
$includes = glob("lib/*.php");
foreach($inclues as $inc){
include($inc);
}
Use lazy loading rather than importing everything - http://php.net/manual/en/function.spl-autoload-register.php
function autoload($sClass) {
$sPath = str_replace('_', DIRECTORY_SEPARATOR, $sClass) . '.php';
require_once($sPath);
}
spl_autoload_register('autoload');
new My_Class();
This is a very stripped back example but it's something to build on. Ideally you would check that the file exists on the include path (http://php.net/manual/en/function.stream-resolve-include-path.php), check that the file is readable, that the require/include was successful and that the class exists at the end of the autoload
I'm writing an autoload function and in the inner logic of it would like to test whether a certain file exists somewhere in the path prior to including it.
This is the logic:
If a file named $className'.specialversion.php' exists anywhere in the include path include it. Otherwise, let other autoloaders take care of including a file for this class.
At the moment I just do: #include($calculatedPath);
I'm not sure if it's a good approach to include and suppress the error. I would rather check if the file exists (somewhere in the include path) prior to including it.
My question is:
Can I test for existence of a file anywhere in the include path?
Is it really problematic to do #include($calculatedPath);?
Edit
An important accent: I don't know where the file should be. I just want to know whether it exists in one of the directories in the include path. So I can't just do file_exists() or something like that.
As of PHP 5.3.2 there is the option to use the stream_resolve_include_path() function whose purpose is to
Resolve [a] filename against the include path according to the same rules as fopen()/include() does.
If the file exists on one of the include paths, then that path (including the file name) will be returned. Otherwise (i.e. the file was not on any of the include paths) it will return FALSE.
Relating this to your needs, your autoloader might look something like:
function my_autoloader($classname) {
$found = stream_resolve_include_path($classname . '.specialversion.php');
if ($found !== FALSE) {
include $found;
}
}
You should avoid the error supressor operator #.
function autoload($class) {
// Build path (here is an example).
$path = DIR_CLASSES .
strtollower(str_replace('_', DIRECTORY_SEPARATOR, $class)) .
'.class.php';
if (file_exists($path)) {
include $path;
}
}
spl_autoload_register('autoload');
$front = new Controller_Front;
// Loads "application/classes/controller/front.class.php" for example.
Update
An important accent: I don't know where the file should be, I just want to know whether it exists in one of the directories in the include path. So I can't just do file_exists or something like this
If your class could be in a number of directories, you could...
Have your autoload function traverse them all, looking for the class. I would not recommend this.
Rename your classes to have a name that easily maps to a file path, like in the example code above.
If you do decide to traverse all folders looking for the class, and it becomes a bottleneck (benchmark it), you could benefit from caching the class name to file location mapping.
I would use file_exists rather than a warnings-suppressed include.
Then you'll have to iterate through the include_path:
$paths = explode(';', get_include_path());
foreach($paths as $p){
if(file_exists($p . '/' . $calculatedPath)){
include $p . '/' . $calculatedPath;
break;
}
}
As a simple resolution, you should a test within the SPL-function file_get_contents() by setting the second argument to TRUE.
--Rolf
I've written a function that can test it nicely
function fileExists($file) {
if(function_exists('stream_resolve_include_path'))
return stream_resolve_include_path($file);
else {
$include_path = explode(PATH_SEPARATOR, get_include_path());
foreach($include_path as $path)
if(file_exists($path.DS.$file))
return true;
return false;
}
}
I am trying to convert several php scripts to use the __autoload function. Right now I can use the include and require functions like this:
require_once('path/to/script.php');
But inside of the __autoload function, I can't use the line above. I have to use this:
require_once('absolute/path/to/script.php');
Why does it seem as though the __autoload function doesn't use the include path I have specified in php.ini?
Don't use __autoload... It has a few drawbacks (including limiting yourself to one per execution). Use instead spl_autoload_register if you're on 5.2+.
So what I typically do, is have a class:
class AutoLoader {
protected static $paths = array(
PATH_TO_LIBRARIES,
);
public static function addPath($path) {
$path = realpath($path);
if ($path) {
self::$paths[] = $path;
}
}
public static function load($class) {
$classPath = $class; // Do whatever logic here
foreach (self::$paths as $path) {
if (is_file($path . $classPath)) {
require_once $path . $classPath;
return;
}
}
}
}
spl_autoload_register(array('AutoLoader', 'load'));
That way, if you add a library set, you can just "add it" to your paths by calling AutoLoader::AddPath($path);. This makes testing with your autoloader a LOT easier (IMHO).
One other note. Don't throw exceptions from the autoload class unless absolutely necessary. The reason is that you can install multiple autoloaders, so if you don't know how to load the file, another one may exist to load it. But if you throw an exception, it'll skip the other one...
Personally, I don't ever like to use relative paths with includes. Especially with multiple include directories (like pear), it makes it very difficult to know exactly which file is being imported when you see require 'foo/bar.php';. I prefer to define the absolute path in the beginning of the file set define('PATH_ROOT', dirname(__FILE__));, and then define all my other useful paths off of that directory (PATH_LIBRARIES, PATH_TEMPLATES, etc...). That way, everything is absolutely defined... And no need to deal with relative paths (like the issue you're having now)...
I suspect your __autoload() function is in a separate file then the code which calls it. The path to the included files will be relative to the file which the __autoload() function declaration resides.
It seems like . is not in your include path. So add it use:
set_include_path('.' . PATH_SEPARATOR . get_include_path());
Now PHP should look relative to the executed scripts directory, too. (Executed script here is something like index.php, not autoload.php.
But why don't use simply use a normal relative path like ./path/to/class.php?
Not sure without seeing the whole set-up. My autoload function is within my global functions file, and looks like this:
function __autoload($class) {
if (file_exists("includes/{$class}.php")) {
require_once("includes/{$class}.php");
}
/**
* Add any additional directories to search in within an else if statement here
*/
else {
// handle error gracefully
}
}
I use a relative path because the script is included in my index.php file and all HTTP requests are passed through it.
Like most web developers these days, I'm thoroughly enjoying the benefits of solid MVC architecture for web apps and sites. When doing MVC with PHP, autoloading obviously comes in extremely handy.
I've become a fan of spl_autoload_register over simply defining a single __autoload() function, as this is obviously more flexible if you are incorporating different base modules that each use their own autoloading. However, I've never felt great about the loading functions that I write. They involve a lot of string checking and directory scanning in order to look for possible classes to load.
For example, let's say I have an app that has a base path defined as PATH_APP, and a simple structure with directories named models, views and controllers. I often employ a naming structure whereby files are named IndexView.php and IndexController.php inside the appropriate directory, and models generally have no particular scheme by default. I might have a loader function for this structure like this that gets registered with spl_autoload_register:
public function MVCLoader($class)
{
if (file_exists(PATH_APP.'/models/'.$class.'.php')) {
require_once(PATH_APP.'/models/'.$class.'.php');
return true;
}
else if (strpos($class,'View') !== false) {
if (file_exists(PATH_APP.'/views/'.$class.'.php')) {
require_once(PATH_APP.'/views/'.$class.'.php');
return true;
}
}
else if (strpos($class,'Controller') !== false) {
if (file_exists(PATH_APP.'/controllers/'.$class.'.php')) {
require_once(PATH_APP.'/controllers/'.$class.'.php');
return true;
}
}
return false;
}
If it's not found after that, I might have another function to scan sub-directories in the models directory. However, all the if/else-ing, string checking and directory scanning seems inefficient to me, and I'd like to improve it.
I'm very curious what file naming and autoloading strategies other developers might employ. I'm looking specifically for good techniques to employ for efficient autoloading, and not alternatives to autoloading.
This is what I have been using in all of my projects (lifted straight from the source of the last one):
public static function loadClass($class)
{
$files = array(
$class . '.php',
str_replace('_', '/', $class) . '.php',
);
foreach (explode(PATH_SEPARATOR, ini_get('include_path')) as $base_path)
{
foreach ($files as $file)
{
$path = "$base_path/$file";
if (file_exists($path) && is_readable($path))
{
include_once $path;
return;
}
}
}
}
If I look for SomeClass_SeperatedWith_Underscores it will look for SomeClass_SeperatedWith_Underscores.php followed by SomeClass/SeperatedWith/Underscores.php rooted at each directory in the current include path.
EDIT: I just wanted to put out there that I use this for efficiency in development, and not necessarily processing time. If you have PEAR on your path then with this you can just use the classes and don't have to include them when you need them.
I tend to keep my classes in a hierarchy of directories, with underscores breaking up namespaces... This code lets me keep the file structure nice and tidy if I want, or to inject a quick class file without nested directories if I want (for adding a single class or two to a library that it is defendant on, but not part of the project I am currently working on.)
I landed on this solution:
I created a single script that traverses my class library folder (which contains subfolders for separate modules / systems), and parses the file contents looking for class definitions. If it finds a class definition in a php file (pretty simple regex pattern), it creates a symlink:
class_name.php -> actual/source/file.php
This lets me use a single, simple autoload function that needs only the class name and the path to the main symlink folder, and doesn't have to do any path/string manipulation.
The best part is that I can rearrange my source code completely or add a new subsystem and just run the link generating script to have everything autoloaded.
If you want efficiency then you shouldn't be using the autoload feature at all. The autoload feature is for being lazy. You should be providing an explicit path to your include files when you include them. If your autoload function can find these files then you could code to find them explicitly. When you are working on the view part of the code and about to load a new view class, by letting the autoload function handle it, it first assumes your class is a model class? That's inefficient. Instead your code should just be:
include_once $this->views_path . $class . '.php';
If you need multiple "view" paths, make a function that loads views:
public function load_view($class) {
// perhaps there's a mapping here instead....
foreach ($this->views_paths as $path) {
$filename = $path . $class . '.php';
if (file_exists($filename)) {
include_once $filename;
}
}
throw ....
}
In any case, at the point where the include occurs, you have the greatest/most accurate information about the class you want to load. Using that information to load the class fully is the only efficient class loading strategy. Yes, you may end up with more class variables or (heaven forbid) some global variables. But that is a better tradeoff than just being lazy and scanning parts of the file system for your class.