PHP - Autoload only needed classes - php

I have an autoloader that is placed as a php file above all other sub directories in my project.
What it does is it loads all possible classes at once for any specific server request. After further thought I concluded I need to autoload only the required classes.
What do I need to do to avoid loading other classes not needed?
If I need to post the relevant code snippets of the class files in my subdirectories, I can.
<?php
namespace autoloader;
class autoloader
{
private $directoryName;
public function __construct($directoryName)
{
$this->directoryName = $directoryName;
}
public function autoload()
{
foreach (glob("{$this->directoryName}/*.class.php") as $filename)
{
include_once $filename;
}
foreach (glob("{$this->directoryName}/*.php") as $filename)
{
include_once $filename;
}
}
}
# nullify any existing autoloads
spl_autoload_register(null, false);
# instantiate the autoloader object
$classes = [
new autoloader('request'),
new autoloader('config'),
new autoloader('controllers'),
new autoloader('models'),
new autoloader('data')
];
# register the loader functions
foreach ($classes as $class)
spl_autoload_register(array($class, 'autoload'));

All registered autoloader functions will be called when you try to instantiate a new class or until it finally loads the class or throws an error. The way you have it now, you're registering the same autoloader function again and again for each directory, and file.
What you'd want to do is something along the lines of this.
namespace autoloader;
class autoloader
{
public function __construct()
{
spl_autoload_register([$this, 'autoload']);
}
public function autoload($classname)
{
if (! file_exists("{$classname}.class.php")) {
return;
}
include_once "{$classname}.class.php";
}
}
new autoloader();
Every autoloader function gets the class FQCN passed into it, and from there you'll have to parse it and figure out if you can load the file where that class exists. For instance, if I do the following.
use Some\Awesome\ClassFile;
$class = new ClassFile();
The autoloader we've registered will get the string Some\Awesome\ClassFile passed in as an argument, which we can then parse and see if we have a file for that class, if we don't we return out of the function and let the next registered autoloader function try and find the class.
You can read more about autoloaders in the documentation, I also wrote a blog post about it like 2 months ago that might interest you.

I had to refactor the code and remove the unnecessary load all functionality that I mistakenly thought would lazy load my classes on request.
Here is what I came up with:
Entry Point
<?php
require_once 'enums.php';
require_once 'api.class.php';
spl_autoload('AutoLoader\AutoLoader');
use App\API;
class MyAPI extends API
{
public function __construct($request){
parent::__construct($request);
}
}
$api = new MyAPI($_REQUEST);
echo $api->processRequest();
AutoLoader Implementation (located under subdirectory Autoloader/Autoloader.php and inaccessible via browser by using .htaccess)
<?php
namespace Autoloader;
spl_autoload_register("AutoLoader\AutoLoader::ClassLoader");
spl_autoload_register("AutoLoader\AutoLoader::RequestLoader");
class Autoloader
{
public static function ClassLoader(String $fileName)
{
foreach ([".Class.php", ".php"] as $extension)
if (file_exists($fileName.$extension))
include $fileName.$extension;
}
public static function RequestLoader()
{
self::ClassLoader('Request');
}
}
Snippet for processRequest() (located in api.class.php - my request router)
public function processRequest()
{
$id1 = $this->requestObj->id1;
$id2 = $this->requestObj->id2;
$endpoint1 = $this->requestObj->endpoint1;
$endpoint2 = $this->requestObj->endpoint2;
$goto = $this->requestObj->goto;
$isDestination = in_array($id1, ['first', 'prev', 'next', 'last']);
$numSetEndpoints = (int)isset($endpoint1) + (int)isset($endpoint2);
switch($numSetEndpoints)
{
case 0:
if ($isDestination)
return json_decode($this->_response("No Endpoint: ", $endpoint1));
return json_decode($this->_response("No Endpoint: " . $endpoint2 ?? $endpoint));
case 1:
$className = $endpoint1.'Controller';
break;
case 2:
$className = $endpoint2.'Controller';
break;
}
$class = "\\Controllers\\$className";
if (class_exists($class))
{
$method = strtolower($this->method);
if (method_exists($class, $method))
{
$response = (new $class($this->requestObj))->{$method}();
if ($response['Succeeded'] == false)
{
return $response['Result'];
}
else if ($response['Succeeded'] == true)
{
header("Content-Type: application/json");
return $this->_response($response);
}
else if ($response['Result'])
{
header("Content-Type: text/html");
return $this->_response($response);
}
}
}
}

Related

PDO::FETCHCLASSTYPE doesnot look for autoloaded classes

In my index.php
include 'autoload.php';
//sql code
while ($result = $list->fetch(PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE)) {
echo $class = get_class($result); //shows stdClass when autoload.php is included
echo $result->showAttributes();
}
The goal is to call the class based on first column from the query result. If I include the following each class instead of autoload.php, I get the desired result.
include 'Myclass1.php';
include 'Myclass2.php';
How can I avoid including bundles of classes and use autoload feature to make PDO::FETCH_CLASSTYPE work.
This is my autoload.php
class Autoloader
{
public static function register()
{
spl_autoload_register(function ($class) {
$file = str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
require $file;
});
}
}
Autoloader::register();
EDIT
Since there were comments about debugging the autoload, I used FETCH_CLASS, I can load respective classes. The issue is FETCH_CLASSTYPE is not able to identify the classes from autoload. I cannot use FETCH_CLASS as the class is decided dynamically. I have printed the result set and first column is the class names to be autoloaded.
while ($result = $list->fetch(PDO::FETCH_CLASS, 'MyClass1')) {
echo $class = get_class($result); //shows MyClass1, MyClass2 depending on what is passed to FETCH_CLASS
echo $result->showAttributes();
}
Posting the workaround based on comments as it might help someone looking for the same.
In my index.php file
include 'autoload.php';
$object = new Mainclass();
$list = $object->getList();
while ($row = $list->fetch()) {
$obj = FactoryClass::subClass($row['type'],$row); //read row and instantiate type object
echo $obj->showAttributes();
}
In my factoryclass where I create an instance
class FactoryClass
{
public static function subClass($obj = '', $values = [])
{
if (!empty($obj)) {
$class = $obj;
} else {
$class = 'sampleclass'; //fallback class
}
return new $class($values);
}
}
Updated my class to accept parameters in constructor and called setter in it, so in my MyClass1.php
class MyClass1 extends AbstractClass
{
public function __construct($values = [])
{
parent::__construct($values);
if (!empty($values)) {
//call setter functions of class attributes
}
}
public function showAttributes()
{
//define the display calling getters
}
}
What I wanted is to read a row, create an object based on row result,
send row result to set the properties and use those properties
to display the page content instead of dealing with the query result directly.

Using $config array in MVC classes

I've done quite a bit of reading on this and a lot of people are saying I should be using a singleton class. I was thinking of writing a "Config" class which would include a "config.php" file on __construct and loop through the $config array values and place them into $this->values...
But then I read more and more about how singletons should never be used. So my question is - what is the best approach for this? I want to have a configuration file, for organization purposes, that contains all of my $config variables. I do not want to be hardcoding things such as database usernames and passwords directly into methods, for the purpose of flexibility and whatnot.
For example, I use the following code in an MVC project I am working on:
public/index.php
<?php
include '../app/bootstrap.php';
?>
app/bootstrap.php
<?php
session_start();
function __autoload ($class) {
$file = '../app/'.str_replace('_', '/', strtolower($class)).'.php';
if (file_exists($file)) {
include $file;
}
else {
die($file.' not found');
}
}
$router = new lib_router();
?>
app/lib/router.php
<?php
class lib_router {
private $controller = 'controller_home'; // default controller
private $method = 'index'; // default method
private $params = array();
public function __construct () {
$this->getUrl()->route();
}
private function getUrl () {
if (isset($_GET['url'])) {
$url = trim($_GET['url'], '/');
$url = filter_var($url, FILTER_SANITIZE_URL);
$url = explode('/', $url);
$this->controller = isset($url[0]) ? 'controller_'.ucwords($url[0]) : $this->controller;
$this->method = isset($url[1]) ? $url[1] : $this->method;
unset($url[0], $url[1]);
$this->params = array_values($url);
}
return $this;
}
public function route () {
if (class_exists($this->controller)) {
if (method_exists($this->controller, $this->method)) {
call_user_func(array(new $this->controller, $this->method), $this->params);
}
else {
die('Method '.$this->method.' does not exist');
}
}
else {
die('Class '.$this->controller.' does not exist');
}
}
}
?>
Now let's say I visit http://localhost/myproject/lead/test
It is going to call the controller_lead class and the test method within.
Here is the code for app/controller/lead
<?php
class controller_lead extends controller_base {
public function test ($params) {
echo "lead test works!";
}
}
?>
app/controller/base
<?php
class controller_base {
private $db;
private $model;
public function __construct () {
$this->connect()->getModel();
}
private function connect () {
//$this->db = new PDO($config['db_type'] . ':host=' . $config['db_host'] . ';dbname=' . $config['db_name']. ', $config['db_user'], $config['db_pass'], $options);
return $this;
}
private function getModel () {
$model = str_replace('controller', 'model', get_class($this));
$this->model = new $model($this->db);
}
}
?>
This is where I run into the issue. As you can see, the connect method is going to try and create a new PDO object. Now how am I going to inject this $config variable, given all the other code I just provided?
My options appear to be:
Use a singleton (bad)
Use a global (worse)
Include config.php in bootstrap.php, and inject it throughout multiple classes (Why should I inject this into my lib_router class when lib_router has absolutely nothing to do with the database? This sounds like terrible practice.)
What other option do I have? I don't want to do any of those 3 things...
Any help would be greatly appreciated.
I ended up including a config file in my bootstrap which simply contained constants, and used the constants.
I ended up including a config file in my bootstrap which simply contained constants, and used the constants.
That was the same case for me - required the file in Model. The file based approach was the best fit since it has some benefits: you can .gitignore, set custom permission set, all your parameters are stored centrally, etc.
However, If the config file contains DB connection parameters only, I prefered to require the config file in Model only. Maybe, you could also break down into multiple, more specific config files and require them where necessary.
public function __construct()
{
if (self::$handle === FALSE)
{
$db = array();
require APP_DIR.DIR_SEP.'system'.DIR_SEP.'config'.DIR_SEP.'Database.php';
if (!empty($db))
{
$this->connect($db['db_host'], $db['db_user'], $db['db_password'], $db['db_name']);
}
else
{
Error::throw_error('Abimo Model : No database config found');
}
}
}

spl_autoloader not loading any classes

so i've started using namespaces and read some docs but I seem to be doing something wrong.
First off is my application structure which is build like this:
root
-dashboard(this is where i want to use the autoloader)
-index.php
--config(includes the autoloader)
--WePack(package)
---src(includes all my classes)
now in the src directory I included the classes with:
namespace WePack\src;
class Someclass(){
}
the content of config.php is:
<?php
// Start de sessie
ob_start();
session_start();
// Locate application path
define('ROOT', dirname(dirname(__FILE__)));
set_include_path(ROOT);
spl_autoload_extensions(".php"); // comma-separated list
spl_autoload_register();
echo get_include_path();
and I use it like this in my index.php
require_once ('config/config.php');
use WePack\src;
$someclass = new Someclass;
this is what the echo get_include_path(); returns:
/home/wepack/public_html/dashboard
which is what I want I guess. but the classes are not loaded and nothing is happening. I'm obviously missing something but I can't seem to figure it out. could you guys take a look at it and explain to me why this isn't working?
The problem here is, that you don't register a callback function with spl_autoload_register(). have a look at the official docs.
To be more flexible, you can write your own class to register and autoload classes like this:
class Autoloader
{
private $baseDir = null;
private function __construct($baseDir = null)
{
if ($baseDir === null) {
$this->baseDir = dirname(__FILE__);
} else {
$this->baseDir = rtrim($baseDir, '');
}
}
public static function register($baseDir = null)
{
//create an instance of the autoloader
$loader = new self($baseDir);
//register your own autoloader, which is contained in this class
spl_autoload_register(array($loader, 'autoload'));
return $loader;
}
private function autoload($class)
{
if ($class[0] === '\\') {
$class = substr($class, 1);
}
//if you want you can check if the autoloader is responsible for a specific namespace
if (strpos($class, 'yourNameSpace') !== 0) {
return;
}
//replace backslashes from the namespace with a normal directory separator
$file = sprintf('%s/%s.php', $this->baseDir, str_replace('\\', DIRECTORY_SEPARATOR, $class));
//include your file
if (is_file($file)) {
require_once($file);
}
}
}
after this you'll register your autoloader like this:
Autoloader::register("/your/path/to/your/libraries");
Isn't this what you mean:
spl_autoload_register(function( $class ) {
include_once ROOT.'/classes/'.$class.'.php';
});
That way you can just call a class like:
$user = new User(); // And loads it from "ROOT"/classes/User.php

This autoloader loads only the first class

I have the following autoloader, that for some reason is loading only the first class.
Here is the Autoloader class
<?php
class AutoloaderException extends Exception{}
class AutoLoader
{
private $classDir;
private $namespace;
public $dirSeparatorSymbol = '\\';
public function __construct($namespace, $classDir)
{
$this->classDir = $classDir;
$this->namespace = $namespace;
}
private function load($class)
{
$include_path = str_replace($this->dirSeparatorSymbol, DIRECTORY_SEPARATOR, $this->classDir);
$classFilename = strtolower(substr($class, strrpos($class, '\\') + 1) . '.php');
if(file_exists($include_path.$classFilename)){
require $include_path.$classFilename;
return true;
}
throw new AutoloaderException('Class '.$classFilename. ' could not be loaded');
}
public function register()
{
spl_autoload_register([$this, 'load']);
}
}
While the above works, but only loads one class at a time. So, in the below example.
For every registered path/class only first will only get loaded.
$b = new Autoloader('mercury\venus\earth', __DIR__.'/mercury/venus/earth/');
$b->register();
$a = new Autoloader('bar\tar', __DIR__.'/foo/bar/tar/');
$a->register();
$x = new bar\tar;
$y = new mercury\venus\earth;
What could be the problem for this?
If there must be multiple autoload functions, spl_autoload_register() allows for this. It effectively creates a queue of autoload functions, and runs through each of them in the order they are defined.
http://php.net/manual/en/function.spl-autoload-register.php
It's queue. Second autoload function would run if first autoload function did not define a class.
So, you do not need to throw an exception in your autoload method because it would prevent launching second autoload method in your autoload queue.

Autoload and namespace in the inheritance and composition

I wrote an Autoload:
<?php
function autoload($class)
{
try
{
global $path;
$elements = explode("\\", $class);
$name_class = end($elements);
foreach ($elements as $element)
{
if($element != $name_class)
{
$path .= $element."/";
echo "path:".$path."<br/>";
}
}
$file = strtolower(($path).$name_class.".php");
if(file_exists($file))
{
include($file);
return;
}
throw new Exception($class." not founded");
}
catch(Exception $e)
{
echo $e->getMessage();
return;
}
}
class Autoloader
{
public static function autoload($class)
{
autoload($class);
}
}
spl_autoload_register('autoload');
spl_autoload_register(array('autoloader', 'autoload'));
?>
The Autoload finds a relative path and change it in an absolute path.
Now I have problem when I call a class from another file with different namespace.
For example if I have this file Base.php in the directorynamed framework:
namespace Framework;
use Framework\Inspector as Inspector;
class Base
{
private $_inspector;
public function __construct($options = array())
{
$this->_inspector = new Inspector($this);
...
Now I have the file Inspector.php in the same directory with the same namespace:
<?php
namespace Framework;
use Framework\ArrayMethods as ArrayMethods;
use Framework\StringMethods as StringMethods;
use \Exception as Exception;
class Inspector
{
...
?>
When I try to instantiate the class Base, I get the exception message about missing Inspector class.
The same think if I try to extend Base class in other directories. I tried to add some echoes in my autoload function and it printed that he use local namespace of the child class or a class who uses an instance by composition of another class to create the string for finding the php file.
For example when I try the load Inspector in Base with echo I got:
path:Framework/
file:::C:\LightTPD\htdocs\mvc\framework\framework\Base.php (I add some ::: to make more readable)
framework/base.php
path:Framework/Framework/
file:::
framework/framework/inspector.php (trying to use the namespace of inspector after using the one of Base)
Framework\Inspector not foundedpath:Framework/Framework/Framework/
file:::
framework/framework/framework/inspector.php
Framework\Inspector not founded
When I tried to extend class Base by class Driver I got this:
path:Framework/
path:Framework/Configuration/
file:::C:\LightTPD\htdocs\mvc\framework\framework\configuration\Driver.php
framework/configuration/driver.php (everything ok)
path:Framework/Configuration/Framework/ (Framework is from Base; Configuration/Framework is from driver)
file:::
framework/configuration/framework/base.php
Framework\Base not foundedpath:Framework/Configuration/Framework/Framework/
file:::
framework/configuration/framework/framework/base.php
Framework\Base not founded
-###############################################################
The directory tree is:
It's:
-framework: ArrayMethods.php, Base.php,Configuration.php, Inspector.php, StingMethods.php
-configuration: Driver.php
-exception
-core
-exception
How can I fix it?
Sorry I noticed that the problem was the global $path.
Now I wrote the autoload.php in this way:
<?php
function autoload($class)
{
try
{ $path = NULL;
$elements = explode("\\", $class);
$name_class = end($elements);
foreach ($elements as $element)
{
if($element != $name_class)
{
$path .= $element."/";
$file = realpath(strtolower(($path).$name_class.".php")); //realpath is optional
if(file_exists($file))
{
include($file);
//echo $file."<br><br>"; testing in for debugging
return;
}
}
}
throw new Exception($class." not founded");
}
catch(Exception $e)
{
echo $e->getMessage();
return;
}
}
class Autoloader
{
public static function autoload($class)
{
autoload($class);
}
}
spl_autoload_register('autoload');
spl_autoload_register(array('autoloader', 'autoload'));
?>
Now even if it's less perfomant everything it's ok, he work whatever a class call another, it's based only the namespace of a class.
Make sure you include the files path of the class you want to extend(parent class) from to your child class file before the use namespaces of the parent/Base class.

Categories