Confusion understanding PHP namespaces - php

I am learning about and implementing PHP namespaces.
Please refer to the following code:
<?php
namespace Xicor\core;
class App {
public function __construct() {
$registry = Registry::getInstance();
//TODO: init router
//TODO: call controller#action
$controllerName = 'Xicor\app\controllers\\'.'Index';
$action = 'show';
$controller = new $controllerName();
$controller->$action();
}
}
The above code works perfectly.
If I add throw new Exception('Lorem Ipsum') within the constructor, I'll get an error as expected. To make it work, I must use throw new \Exception('Lorem Ipsum') so that we are referring to the global namespace.
But, why does
$controllerName = 'Xicor\app\controllers\\'.'Index'; successfully import the right class.
Why do I not have to use
$controllerName = '\Xicor\app\controllers\\'.'Index'; (with \ prefixed)?
If it affects anything, here's my autoloader:
<?php
spl_autoload_register(function($name) {
//replace \ with DIRECTORY_SEPARATOR
$path = str_replace('\\', DS, $name);
//replace Xicor with root
$path = str_replace('Xicor', __DIR__, $path); // __DIR__ doesn't return a trailing slash
//add .php at end
$path .= '.php';
if(file_exists($path)) {
require_once($path);
}
});

As I understand it. PHP will work in the current namespace within a class unless specified otherwise (a preceding \).
Example
namespace Bar;
class Foo {
function __construct()
{
// references current namespace, looks for Bar\Baz;
$baz = new Baz();
}
}
class Baz {
function __construct()
{
try {
// do stuff
// references global namespace
} catch(\Exception $e) {
var_dump($e->getMessage());
}
}
function foo() {
// This will prepend the current namespace to the class, in actual fact it will first look for "Bar\Bar\Foo"
// When it isnt found, it will move on and look at spl_autoload_register to try to resolve this class,
// Failing that you will get a ClassNotFoundException
$foo = new Bar\Foo();
}
}
Please see. https://www.php.net/manual/en/language.namespaces.rules.php
and https://www.php.net/manual/en/language.namespaces.faq.php#language.namespaces.faq.full for reference

Related

PHP - Autoload only needed classes

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);
}
}
}
}

Class not found when successfully included

I get this fatal error for class not being found:
Fatal error: Uncaught Error: Class 'HomeController' not found
But the file is included.
require_once self::CONTROLLERS_PATH . $this->controller . '.php';
var_dump(file_exists(self::CONTROLLERS_PATH . $this->controller . '.php'));
new $this->controller;
I debugged it by var_dumping file_exists which returns true. I checked the namespaces and everything looks good. If I var dump $this->controller, its value is as intended - string(14) "HomeController". If I hard code it like new HomeController();, the class is initialized. Why is it not working from the variable, though? You can see my code below. This is my file structure:
App.php
<?php
namespace App\Core;
class App
{
protected $controller = 'HomeController';
protected $method = 'index';
protected $params = [];
const CONTROLLERS_PATH = '../App/Controllers/';
public function __construct ()
{
$url = $this->parseUrl();
if(file_exists(self::CONTROLLERS_PATH . ucfirst($url[0]) . '.php'))
{
$this->controller = ucfirst($url[0]); //Update $this->controller value
unset($url[0]);
echo 'file exists';
}
require_once self::CONTROLLERS_PATH . $this->controller . '.php';
var_dump(file_exists(self::CONTROLLERS_PATH . $this->controller . '.php'));
var_dump($this->controller);
new $this->controller;
if(isset($url[1]))
{
if(method_exists($this->controller, $url[1]))
{
$this->method = $url[1];
unset($url[1]);
echo 'method exists';
}
}
$this->params = $url ? array_values($url): [];
call_user_func_array([$this->controller, $this->method], $this->params);
var_dump($url);
}
public function parseUrl()
{
if(isset($_GET['url']))
{
return $url = explode('/', filter_var(rtrim($_GET['url'], '/'), FILTER_SANITIZE_URL));
}
}
}
HomeController.php
<?php
namespace App\Controllers;
class HomeController
{
public function index()
{
echo 'asd';
}
}
When instantiating a class via a variable name you always have to include the whole namespace. And this has to be stored as a string first.
An example:
namespace Api;
Class test {
public function __construct() {
echo "test";
}
}
$className = 'test';
$classNameNamespaced = "\\Api\\".$className;
$a = new $classNameNamespaced; // echoes "test"
in your case:
$className = "\\App\\Controllers\\".$this->controller;
new $className;
Because the code is in a namespace, new $this->controller is different than new HomeController even if the value of $this->controller is HomeController.
The expression new HomeController is evaluated at the compile time and, because of the namespace, it is expanded to new \App\Controllers\HomeController (rule #6 on the Name resolution rules). The interpreter needs the fully qualified class name in order to find it.
The expression new $this->controller is evaluated during runtime and it leads to new HomeController which is the same as new \HomeController. There isn't any class HomeController defined in the root namespace, hence the error you get.
You can make it easily work by building the full class name into a string:
$class = 'App\\Controllers\\'.$this->controller;
new $class;
Find more examples in the documentation page "Namespaces and dynamic language features".
But the best way to load classes is to use the autoloader generated by Composer.

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

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.

How can I call a static method on a variable class?

I'm trying to make some kind of function that loads and instantiates a class from a given variable. Something like this:
<?php
function loadClass($class) {
$sClassPath = SYSPATH."/classes/{$class}.php";
if (file_exists($sClassPath)) {
require_once($sClassPath);
$class = $class::getInstance();
}
}
?>
If I use it like this:
<?php
loadClass('session');
?>
It should include and instantiate the session class.
BTW: the static getInstance function comes from this code:
<?php
function getCallingClass() {
$backtrace = debug_backtrace();
$method = $backtrace[1]['function'];
$file = file($backtrace[1]['file']);
$line = $file[($backtrace[1]['line'] - 1)];
$class = trim(preg_replace("/^.+?([A-Za-z0-9_]*)::{$method}\(.*$/s", "\\1\\2", $line));
if(! class_exists($class)) {
return false;
} return $class;
}
class Core {
protected static $instances = array();
public static function getInstance() {
$class = getCallingClass();
if (!isset(self::$instances[$class])) {
self::$instances[$class] = new $class();
} return self::$instances[$class];
}
}
?>
The thing is that right now the way to use the functions in a class is this:
<?php
$session = session::getInstance();
?>
But now I want to build that into a function so that I never again have to use that line of code.
I just say loadClass('session');
and than I can use $session->blablablafunction();
Calling static functions on a variable class name is apparently available in PHP 5.3:
Foo::aStaticMethod();
$classname = 'Foo';
$classname::aStaticMethod(); // As of PHP 5.3.0
http://php.net/manual/en/language.oop5.static.php
Could definitely use that right now myself.
Until then you can't really assume that every class you are loading is designed to be a singleton. So long as you are using < 5.3 you'll have to just load the class and instantiate via the constructor:
function loadClass($class) {
$sClassPath = SYSPATH."/classes/{$class}.php";
if (file_exists($sClassPath)) {
require_once($sClassPath);
$class = new $class;
}
}
OR
Just load the class without creating an object from it. Then call "::getInstance()" on those meant to be singletons, and "new" on those that are not, from outside of the loadClass() function.
Although, as others have pointed out earlier, an __autoload() would probably work well for you.
You can use call_user_func():
$class = call_user_func(array($class, 'getInstance'));
The first argument is a callback type containing the classname and method name in this case.
Why not use __autoload() function?
http://www.php.net/autoload
then you just instantiate object when needed.
It looks like you are fighting PHP's current implementation static binding, which is why you are jumping through hoops with getCallingClass. I can tell you from experience, you should probably abandon trying to put instantiation in a parent class through a static method. It will cause you more problems in the end. PHP 5.3 will implement "late static binding" and should solve your problem, but that obviously doesn't help now.
You are probably better off using the autoload functionality mentioned by kodisha combined with a solid Singleton implementation. I'm not sure if your goal is syntactic sugar or not, but it think you will do better in the long run to steer clear of trying to save a few characters.
Off the top of my head, needs testing, validation etc:
<?php
function loadClass($className) {
if (is_object($GLOBALS[$className]))
return;
$sClassPath = SYSPATH."/classes/{$className}.php";
if (file_exists($sClassPath)) {
require_once($sClassPath);
$reflect = new ReflectionClass($className);
$classObj = $reflect->newInstanceArgs();
$GLOBALS[$className] = $classObj;
}
}
?>
Late static bindings will work for you I think. In the construct of each class do:
class ClassName
{
public static $instances = array();
public function __construct()
{
self::$instances[] = $this;
}
}
Then...
Here is an autoloader I created. See if this solves your dilemma.
// Shorten constants for convenience
define ('DS', DIRECTORY_SEPARATOR);
define ('PS', PATH_SEPARATOR);
$template = "default";
// Define an application path constants
define ('APP_ROOT', realpath('.').DS);
define ('VIEW', APP_ROOT . 'Views' . DS);
define ('MODEL', APP_ROOT . 'Models' . DS);
define ('CONTROLLER', APP_ROOT . 'Controllers' . DS);
define ('TEMPLATE', VIEW."templates".DS.$template.DS);
define ('CONTENT', VIEW."content".DS);
define ('HELPERS', MODEL."helpers".DS);
// Check if application is in development stage and set error reporting and
// logging accordingly
error_reporting(E_ALL);
if (defined('DEVELOPMENT')) {
ini_set('display_errors', 1);
} else {
ini_set('display_errors', 0);
ini_set('log_errors', 'On');
ini_set('error_log', APP_ROOT.'error.log');
}
$paths = array(APP_ROOT, VIEW, MODEL, CONTROLLER, TEMPLATE, CONTENT, HELPERS);
// Set the include path from Config Object
set_include_path(implode(PS, $paths));
// Autoloader
function __autoload($class)
{
require_once $class.'.php';
return;
}
Then all you have to do is
$var = new ClassName();
but you have to have a php file in the path with the name
ClassName.php
where ClassName is the same as the name of the class you want to instantiate.

Categories