I am migrating my PHP web application to use namespaces in preparation for putting it up on GitHub. Most of this is straightforward, but I have one spot where the code must obtain static information from a class when the name of the class is provided by a string, and I cannot find the correct syntax for this situation:
if ($className == '')
{
$className = 'Record';
$information['classname'] = 'Record';
}
if (!class_exists(__NAMESPACE__ . "\\" . $className))
{
print "<p>include '". __NAMESPACE__ . "/" . $className . ".inc'</p>\n";
include __NAMESPACE__ . "/" . $className . ".inc";
}
// BOTH of the following statements report Class 'Blog' not found
// when $className == "Blog"
$order = $className::$defaultOrder;
$order = __NAMESPACE__ . "\\" . $className::$defaultOrder;
What is the correct syntax for referencing a static member of a class within a namespace when the name of the class is provided in a variable? The output of a web page that invokes the above code is:
include 'Genealogy/Blog.inc'
Blog.inc included
Fatal error: Uncaught Error: Class 'Blog' not found in /home/jcobban/includes/Genealogy/Record.inc:2043 Stack trace: #0 /home/jcobban/includes/Genealogy/RecordSet.inc(418): Genealogy\Record::getInformation('Blogs') #1 /home/jcobban/includes/Genealogy/Template.inc(2898): Genealogy\RecordSet->__construct('Blogs', Array) #2 /home/jcobban/includes/Genealogy/Template.inc(764): Genealogy\FtTemplate->customization() #3 /home/jcobban/public_html/Genealogy/genealogy.php(82): Genealogy\Template->__construct('/home/jcobban/p...') #4 {main} thrown in /home/jcobban/includes/Genealogy/Record.inc on line 2043
The above code is in the base class Genealogy\Record which is implemented in "Genealogy/Record.inc". This base class is defined:
<?php
namespace Genealogy;
use \PDO;
use \Exception;
use \ArrayAccess;
use \Countable;
use \Iterator;
class Record implements Iterator, ArrayAccess
{
...
protected static $defaultOrder = '';
The derived class Genealogy\Blog is implemented in "Genealogy/Blog.inc" as follows:
<?php
namespace Genealogy;
use \PDO;
use \Exception;
require_once __NAMESPACE__ . '/Record.inc';
print "<p>Blog.inc included</p>\n";
class Blog extends Record
{
...
The class Blog does not override the base definition of static member $defaultOrder.
FYI I converted the code of the library to use namespaces by running the following PERL script:
use strict;
use warnings;
use 5.010;
use File::Find;
use File::Slurp;
my #content;
find( \&wanted, '/home/jcobban/includes/');
exit;
sub wanted {
if ((substr $File::Find::dir, -9) ne "Genealogy" && -f)
{
print "wanted: ", $File::Find::name, "\n";
my #lines = read_file($File::Find::name);
my $first = shift #lines;
if ((substr $first, 0, 5) eq '<?php')
{
foreach my $line (#lines){
$line =~ s#require_once(\s*)(['"])#require_once$1__NAMESPACE__ . $2/#;
$line =~ s#require(\s*)(['"])#require$1__NAMESPACE__ . $2/#;
$line =~ s#include(\s*)(['"])#include$1__NAMESPACE__ . $2/#;
$line =~ s#include(\s*)\$#include$1__NAMESPACE__ . "/" . \$#;
$line =~ s#class_exists\((['"])#class_exists(__NAMESPACE__ . $1\\\\#;
$line =~ s#class_exists\((\$)#class_exists(__NAMESPACE__ . "\\\\" . $1#;
}
my $newfile = $File::Find::dir . "/Genealogy/" . $_;
print "add namespace and write to $newfile\n";
unshift #lines, "use \\Iterator;\n";
unshift #lines, "use \\Countable;\n";
unshift #lines, "use \\ArrayAccess;\n";
unshift #lines, "use \\Exception;\n";
unshift #lines, "use \\PDO;\n";
unshift #lines, "namespace Genealogy;\n";
unshift #lines, $first;
write_file($newfile, #lines);
}
}
return;
}
I do not believe it is significant but the web site is running PHP Version 7.2.10.
The classes in this library implement an object-oriented interface to an SQL database. That is the application code accesses fields within the record using sub-script notation and updates the record by $record->save() which determines whether or not to use INSERT or UPDATE to apply the change and uses the proper syntax for the SQL server. The RecordSet class that appears in the exception encapsulates an SQL query and presents the appearance of an array of instances of Record and permits performing updates and deletes of all of the members of the set. These two classes, and the classes that are derived from them to support individual tables, completely insulate the application code from SQL.
The problem I was having would appear to be that the :: operator was processed before the concatenation. When I changed the code to:
$nsclass = __NAMESPACE__ . "\\" . $className;
if (!class_exists($nsclass))
{
include __NAMESPACE__ . "/" . $className . ".inc";
}
$order = $nsclass::$defaultOrder;
it worked.
Related
Using spl_autoload_register() with a class that uses namespace, causes this error message: Fatal error: Uncaught Error: Class "MyClass" not found...
Before this error message, the autoloader does echo "...MyClass.php IS found".
Two files are used: "/index.php" (in the root directory) and "/classes/MyClass.php".
Index.php:
declare(strict_types=1);
define ('ROOT_DIR', __DIR__ . '/');
define ('CLASS_DIR', ROOT_DIR . 'classes/');
spl_autoload_register(function($class) {
$filepath = CLASS_DIR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
if (file_exists($filepath)) {
require_once $filepath;
echo('<br /><b>' . $filepath . '</b> IS found.<br />');
} else {
echo('<br /><b>' . $filepath . '</b> not found.<br />');
exit;
}
});
$myclass = new MyClass();
MyClass.php:
declare(strict_types=1);
namespace App\Classes
class MyClass
{
// ...
}
Does anyone know how to solve this?
According to https://www.php-fig.org/psr/psr-4/, a vendor name is required, so I am using "App" as a top-level namespace name, even though it is not a directory (because my website uses the root of the server).
I also tried adding "use App/classes/Myclass" to index.php before "$myclass = new MyClass()" but this causes even more problems, because the autoloader will look for the directory "/classes/App/Classes"...
With the namespace removed from the class, everything works fine, and I can use the functions of the class through $myclass. But I would like to start using namespaces... Any help would be really appreciated! <3
Conclusion: Either the line "$myclass = new App\Classes\MyClass();"
or "use App\Classes\MyClass;" should be used.
So it is not possible to use the root of the server while also having a
top-level namespace name ("App") with this autoload function. The
function has to be expanded to allow for this possibility. And the "classes" directory will be renamed to "Classes". I will post my solution when it is ready!
For more details, read the comments below the answer by #IMSoP (Thank
you very much for your help!)
Solution:
declare(strict_types=1);
namespace App;
define ('ROOT_DIR', $_SERVER['DOCUMENT_ROOT'] . '/');
define ('BASE_DIR', __DIR__ . '/');
define ('TOP_LEVEL_NAMESPACE_NAME', __NAMESPACE__ . '/');
spl_autoload_register(function($class) {
if (BASE_DIR == ROOT_DIR . TOP_LEVEL_NAMESPACE_NAME) {
$filepath = ROOT_DIR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
} else {
$filepath = BASE_DIR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
$filepath = str_replace(TOP_LEVEL_NAMESPACE_NAME, '', $filepath);
}
if (file_exists($filepath)) {
require_once $filepath;
} else {
echo('Class <b>' . end(explode('\\', $class)) . '.php</b> was not found.');
exit;
}
});
use App\Classes\MyClass;
$myclass = new MyClass();
This solution works whether the application is in the directory with the same name as the top-level namespace name, or anywhere else!
You might want to read through the manual pages on autoloading and namespaces to make sure you understand the key concepts.
You have declared a class called MyClass inside the namespace App\Classes; that means that its fully qualified name is App\Classes\MyClass - that's the name you need to call it by from outside that namespace. There could simultaneously be a different class whose fully-qualified name was just MyClass, because it wasn't in any namespace, and any number of others in other namespaces, like App\Security\MyClass, App\UI\MyClass, etc.
Then you've attempted to reference a class in index.php called MyClass, which triggers the autoloader. The autoloader translates it to a path like .../classes/MyClass.php, and loads the right file; but that file defines your namespaced class. So after the autoloader has finished, there is no class called MyClass, only App\Classes\MyClass and the code fails.
If instead you write new App\Classes\MyClass, you'll get the opposite problem: the string passed to your autoloader is 'App\Classes\MyClass' and you translate that to a file path like '.../classes/App/Classes/MyClass.php' - but that's not where your file is. (Adding use App\ClassesMyClass does the same thing - use statements are just compiler assistance to avoid writing out the fully-qualified name as often.)
What you need to do is both:
Consistently use fully-qualified class names (or alias them with use)
Lay out your files to match your namespace structure, so that your autoloader can find them, which generally means a directory per namespace
I have seen these,
How to autoload class with a different filename? PHP
Load a class with a different name than the one passed to the autoloader as argument
I can change but in my MV* structure I have:
/models
customer.class.php
order.class.php
/controllers
customer.controller.php
order.controller.php
/views
...
In the actually classes they are,
class CustomerController {}
class OrderController{}
class CustomerModel{}
class OrderModel{}
I was trying to be consistent with the names. If I do not put the class name suffix (Controller, Model), I cannot load the class because that is redeclaring.
If I keep the names of my classes, autoload fails because it will look for a class file named
CustomerController
when the file name is really,
customer.controller.php
Are my only ways to (in no order):
use create_alias
rename my files (customer.model.php to customermodel.php)
rename my classes
use regular expressions
use a bootstrap with included files (include,
require_once, etc.)
?
Example code,
function model_autoloader($class) {
include MODEL_PATH . $class . '.model.php';
}
spl_autoload_register('model_autoloader');
It seems I have to rename files,
http://www.php-fig.org/psr/psr-4/
"The terminating class name corresponds to a file name ending in .php. The file name MUST match the case of the terminating class name."
Looks to me this can be handled with some basic string manipulation and some conventions.
define('CLASS_PATH_ROOT', '/');
function splitCamelCase($str) {
return preg_split('/(?<=\\w)(?=[A-Z])/', $str);
}
function makeFileName($segments) {
if(count($segments) === 1) { // a "model"
return CLASS_PATH_ROOT . 'models/' . strtolower($segments[0]) . '.php';
}
// else get type/folder name from last segment
$type = strtolower(array_pop($segments));
if($type === 'controller') {
$folderName = 'controllers';
}
else {
$folderName = $type;
}
$fileName = strtolower(join($segments, '.'));
return CLASS_PATH_ROOT . $folderName . '/' . $fileName . '.' . $type . '.php';
}
$classNames = array('Customer', 'CustomerController');
foreach($classNames as $className) {
$parts = splitCamelCase($className);
$fileName = makeFileName($parts);
echo $className . ' -> '. $fileName . PHP_EOL;
}
The output is
Customer -> /models/customer.php
CustomerController -> /controllers/customer.controller.php
You now need to use makeFileName inside the autoloader function.
I myself am strongly against stuff like this. I'd use namespaces and file names that reflect the namespace and class name. I'd also use Composer.
(I found splitCamelCase here.)
Currently, I have a pretty average php auto loader loading in my classes. I've come to a point in development where I will need to override a class with another class based on a variable. I'm running a custom SaaS application, and we have the occasional organization that will demand some weird change to the way the system functions. In the past, we've filled up our code with garbage by massive IF statements for orgs, such as
if(ORGID == 'ABCD'){
//do this insane thing
}else{
//Normal code here.
}
So, I've been toying with the idea of a dynamic auto loader. ORGID is one of the very first defines in the application. The entire application is running under a fixed namespace of COMPANY\PRODUCT; Here's a code sample of what I was thinking I could do.
class MyLoader {
static public function load($name) {
$temp = explode('\\',$name);
$class = array_pop($temp);
$name = str_replace('_',DIRECTORY_SEPARATOR,$class);
if(file_exists(ROOT.'includes/_classes/' . $name . '.php')){
include(ROOT.'includes/_classes/' . $name . '.php');
}
}
}
spl_autoload_register(__NAMESPACE__ .'\MyLoader::load');
Since ROOT and ORGID are defined before the autoloader comes into play, I thought about doing this
class MyLoader {
static public function load($name) {
$temp = explode('\\',$name);
$class = array_pop($temp);
$name = str_replace('_',DIRECTORY_SEPARATOR,$class);
if(file_exists(ROOT.'includes/_classes/' . ORGID . '/' . $name . '.php')){
include(ROOT.'includes/_classes/' . ORGID . '/' . $name . '.php');
}elseif(file_exists(ROOT.'includes/_classes/' . $name . '.php')){
include(ROOT.'includes/_classes/' . $name . '.php');
}
}
}
spl_autoload_register(__NAMESPACE__ .'\MyLoader::load');
While this works, I have to copy/paste my entire class into the org specific class file, then make changes. I can't extend the primary class, because the classes share the same name. The only option I've been able to come up with that would allow me to extend my classes in such a way is to never load the base class.
Instead of
$myObj = new myClass();
I call
$myObj = new customMyClass();
and I have a file called customMyClass(); which simply extends myClass without making any changes to it. This way, the auto loader will load customMyClass and then load myClass. If an organization has their own customMyClass in their organization folder, then it will load in that class, which will then properly load in myClass.
While this works, we have hundreds of class files, which would double if we had a custom file for each.
I've seen a couple of examples that use eval to handle similar situations. Is that really the only way to do this type of thing?
UPDATE:
Just so I'm clear, the end goal is so that the thousands of places we've called $myObj = new myClass(); doesn't need to be rewritten.
For MVC reasons, I want to be able to trigger a function to find when the function has been called, since Codeigniter has functions around their core, I want to hook a function such as setcookie and create a file when it's been called (from the triggered function) for example:
function call_me()
{
$file = fopen('setcookie.txt', 'a+');
fwrite($file, 'Called at ' . __CLASS__);
fclose();
}
So when setcookie is called, it should trigger the call_me function. Is there any specific function or method to do this? I know about debug_backtrace but that's not the purpose I want.
What you basically need to have a look at is Observers.
The observer pattern (aka. Dependents, publish/subscribe) is a
software design pattern in which an object, called the subject,
maintains a list of its dependents, called observers, and notifies
them automatically of any state changes, usually by calling one of
their methods. It is mainly used to implement distributed event
handling systems. Observer is also a key part in the familiar MVC
architectural pattern. In fact the observer pattern was first
implemented in Smalltalk's MVC based user interface framework.1
Why don't you try what is described here :
http://devzone.zend.com/1384/observer-pattern-in-php/
I know about debug_backtrace but that's not the purpose I want.
I see that you insist not to use backtrack function, but still I believe that when you want to log when a function is called backtrack can come in handy.
The idea is that you have a predifined piece pf code stored in a constant, whenever you want to debug an if condition evaluates this code.
If you are under prouction the if statement will prevent from evaluating anything so your code's speed is not affected. If it works for you you can modify it to your needs, to track down even more levels.
To make my point this is a full example, if I have not understood right and this is not what you're looking for, my apologies!
To check the example you have a file: test.php
<?php
define ('__SITE_PATH',realpath(dirname(__FILE__)).'/');
ini_set('log_errors', 1);
ini_set('error_log', __SITE_PATH.'my_error_log.log');
include 'test1.php';
include 'test2.php';
define (__DEBUG_EVAL, '
$dbt = debug_backtrace();
error_log(
"\n".
"Parent function file: " . $dbt[1]["file"] . "\n" .
"Parent function class: " . $dbt[2]["class"] . "\n" .
"Parent fiunction name: " . $dbt[2]["function"] . "\n" .
"Par. fiunc. called from line: " . $dbt[2]["line"] . "\n" .
"Child function file: " . $dbt[0]["file"] . "\n" .
"Child function class: " . $dbt[1]["class"] . "\n" .
"Child fiunction name: " . $dbt[1]["function"] . "\n" .
"Child fiunc. called from line: " . $dbt[1]["line"] . "\n" .
"\n"
);
');
test1::a();
?>
This is test1.php
<?PHP
class test1
{
public static function a()
{
test2::b();
}
}
?>
The last is test2.php
<?PHP
class test2
{
public static function b()
{
if(defined('__DEBUG_EVAL')) eval(__DEBUG_EVAL);
echo 'Hello!';
}
}
?>
This is the result:
[13-Apr-2012 14:37:18]
Parent function file: C:\PHP-GTK\MyProjects\Electre\test1.php
Parent function class: test1
Parent fiunction name: a
Par. fiunc. called from line: 29
Child function file: C:\PHP-GTK\MyProjects\Electre\test2.php
Child function class: test2
Child fiunction name: b
Child fiunc. called from line: 7
I'd like to instanciate my class doing :
use somedir\http as Http;
$S_bodyWriterType = 'Http\\' . strtolower($S_requestBodyPayloadType) . '\\RequestBodyWriter';
$this->_O_requestBodyWriter = new $S_bodyWriterType;
It says the class does not exist. However THAT would work (no string involved here) :
$this->_O_requestBodyWriter = new Http\xml\RequestBodyWriter;
And that would also work of course (the namespace is fully qualified) :
$S_bodyWriterType = 'somedir\http\\' . strtolower($S_requestBodyPayloadType) . '\\' . 'RequestBodyWriter';
$this->_O_requestBodyWriter = new $S_bodyWriterType;
i'd definitely prefer to use shortened namespaces instead of having to write long, fully-qualified namespaces in different places of the codebase and having to change them all in case the directory location moves. I've been pulling my hair off for a while now over this.
Thanks for help !
OK, you provided the Bug report yourself ;) But thats the fact: If you define a classname in a string, its not said, that the object is created in the same context.
namespace y {
use a\b as B;
$GLOBALS['class'] = 'B\\MyClass';
}
namespace z {
use k\l as B;
$classname = $GLOBALS['class'];
$a = new $classname;
}
Thus you need to define classnames in string full qualifed. I suggest to use (namespace-/class-)constants
use a\b as B;
const NAMESPACE_B = '\\a\\b';
$classname = NAMESPACE_B . '\\MyClass';
If the class you want to instanciate is in a subnamespace, remember, that the pseudo-constant __NAMESPACE__ always exists;
namespace a;
use a\b as B;
$classname = __NAMESPACE__ . '\\b\\MyClass';
Additional in your case I suggest to create a factory
use somedir\http as Http;
class RequestBodyWriterFactory {
public function create($type) {
$classname = __NAMESPACE__ . "\\$type\\RequestBodyWriter";
return new $classname;
}
}
// somewere else
$this->_O_requestBodyWriter = $this->factory->create(strtolower($S_requestBodyPayloadType));
This way you have more control on what is created and how its created.