Cannot find dynamic class reference PHP - php

My routing.php (auto-prepend file)
<?php
if (preg_match('/\.(?:png|jpg|jpeg|gif)$/', $_SERVER["REQUEST_URI"])) {
return false;
} else {
include 'index.php';
}
My index.php
<?php
namespace MVC;
require_once "controllers/AdminController.php";
use MVC\controllers\AdminController as AdminController;
/*
* Break the requested URI down into an array
*/
$request = trim($_SERVER["REQUEST_URI"]);
$request = explode("/", $request);
$request = array_filter($request);
$additional = array();
$singles = array();
/*
* Get the method/controller from the requested URL
*/
foreach($request as $key=>$item) {
switch($key) {
case 1:
$controller = $item;
break;
case 2:
$method = $item;
break;
default:
if($key > 2) {
$item = explode("=", $item);
if(isset($item[1])) {
//key item pair, data
$pairs[$item[0]] = $item[1];
/*
* Make the assumption that any value passed in
* as an assigned value is data and not a route
*
* Append this to the pairs array as its own key/item
*/
} else if(isset($item[0])) {
echo "<b>WARNING: \"" . $item[0] . "\" has no key/value!</b>";
}
}
break;
}
}
/*
* Handle the fact that these might not have been requested
*/
if(!isset($controller))
$controller = 'index';
$controller = ucfirst($controller) . "Controller";
if(!isset($method))
$method = 'index';
$controller = new $controller();
My AdminController.php (found in controllers/AdminController.php)
<?php
namespace MVC\controllers;
require_once "libs/Controller.php";
use MVC\libs\Controller;
class AdminController extends Controller {
}
Finally my Controller superclass found in libs/Controller.php
<?php
namespace MVC\libs;
class Controller {
}
My issue is this line
$controller = new $controller();
I'm aware that this is a variable - that's the intention, I'm trying to dynamically load this class based on the URI request.
Gives this error:
127.0.0.1:50342 [500]: /admin/users/id=555/action=ban - Class 'AdminController' not found in /home/jack/PhpstormProjects/mvc/index.php on line 64
I've checked that it's requiring the class, using it as "AdminController", yet it still can't find a reference to it in the relevant namespace - I'm thinking that because it's dynamic it might have some issues?
Cheers all!
EDIT: My folder structure
controllers
AdminController.php
libs
Controller.php
index.php
routing.php

This is a namespace problem. You can't use variable class name and use statement at the same time like you do. Your variable gets its value at runtime, while use imports are done at compile time (use can't be used in block scope for the same reason). See:
use foo as bar;
class foo {
public function __construct() {
echo "OK\n";
}
}
new foo(); // OK
new bar(); // OK
$foo = 'foo';
$bar = 'bar';
new $foo(); // OK
new $bar(); // Fatal error: Class 'bar' not found
Solution: use full class name (with absolute namespace) in your variable.

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.

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

Confusion understanding PHP namespaces

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

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.

Reflection class PHP from file?

I wanna get value from Class PHP without initialize this Class.
For this I give the file path where this class, for it to be reviewed, but not initialized.
My Idea:
<?php
$reflection = new ReflectionClass( '/var/www/classes/Base.php' );
$version = $reflection->getProperty('version')->getValue( );
if( $version >= 1 )
{
return true;
}
return false;
?>
BASE.PHP
<?php
class Base
{
private $version = 2;
}
?>
whats about static? its much simpler:
<?php
class Base
{
public static $version = 2; // or $version = array(1,2,3);
}
if(is_array(Base::$version)) {
print_r(Base::$version);
} else {
echo Base::$version;
}
?>
How about a protected variable with a getter.
class Base {
protected $version = array(2,3,4,5,6);
public function __version() { return $this->version; }
}
You can instantiate this anywhere you like, or extend it to add functions to it. The version will be constant across any extensions, so bear that in mind.
Usage is as simple as $yourClass->__version(). Named it similar to a magic method's name in order to prevent function name collision. It can be redefined by extensions if needed.

Categories