Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Basic autoload and namespace in php

Tags:

php

autoload

I'm in the process of updating a framework that I wrote a while ago. I would like to adopt new standards such as namespaces and using the autoload feature.

Right now my framework has a very rudimentary but functional autoload function that looks like this:

protected function runClasses()
{
    $itemHandler = opendir( V_APP_PATH );
    while( ( $item = readdir( $itemHandler ) ) !== false )
    {
        if ( substr( $item, 0, 1 ) != "." )
        {
            if( !is_dir( $item ) && substr( $item, -10 ) == ".class.php" )
            {
                if( !class_exists( $this->registry->$item ) )
                {
                    require_once( V_APP_PATH . $item );
                    $item  = str_replace( ".class.php", "", $item );
                    $this->registry->$item = new $item( $this->registry );
                }
            }
        }
    }   
}

As you can see in the code this function is limited to a single folder only, but the advantage to it is it loads the class into my registry allowing me to access that specific class in other files by doing something similar to $this->registry->Class->somevar which is the functionality I need.

What I'm needing/wanting to accomplish is to use the autoloader function but have that function not limited to a single folder, instead be able to navigate through multiple folders and instantiate the needed classes.

I have just some test files going and here is my current file structure:

File Structure

For MyClass2 I have:

namespace model;
Class MyClass2 {
    function __construct() {
        echo "MyClass2 is now loaded!";
    }
}

For MyClass1 I have:

Class MyClass1 {
function __construct() {
         echo "MyClass1 is now loaded!<br />";
    }
}

And for Autoload I have:

function __autoload( $className ) {
    $file = $className . ".php";
    printf( "%s <br />", $file );
    if(file_exists($file)) {
        require_once $file;
    }
}

$obj = new MyClass1();
$obj2 = new model\MyClass2(); 

My Question is the way that is set up it can't find the file for MyClass2 so I'm wondering what I've done wrong and secondly, is there a way like my first "autoload" function to not need to specify the namespace in the autoload file and assign it to my registry?

Sorry for such a lengthy question but any help is greatly appreciated.

like image 260
Peter Avatar asked Jan 15 '23 12:01

Peter


2 Answers

I see two things here.

The first one makes your problem a bit complicated. You want to make use of namespaces, but your current configuration is via the file-system. The file-names of the class definition files does not contain the namespace so far. So you can not just continue as you actually do.

The second is that you do not have what's covered by PHP autoloading, you just load a defined set of classes and register it with the registry.

I'm not really sure if you need PHP autoloading here. Sure it might look promising for you to bring both together. Solving the first point will probably help you to solve the later, so I suggest to start with it first.

Let's make the hidden dependencies more visible. In your current design you have got three things:

  1. The name under which an object is registered in the registry.
  2. The filename which contains the class definition.
  3. The name of the class itself.

The values of 2. and 3. are in one, you parse the name of the class itself from the filename. As written, namespaces make this complicated now. The solution is easy, instead of reading from a directory listing, you can read from a file that contains this information. A lightweight configuration file format is json:

{
    "Service": {
        "file":  "test.class.php",
        "class": "Library\\Of\\Something\\ConcreteService"
    }
}

This contains now the three needed dependencies to register a class by a name into the registry because the filename is known as well.

You then allow to register classes in the registry:

class Registry
{
    public function registerClass($name, $class) {
        $this->$name = new $class($this);
    }
}

And add a loader class for the json format:

interface Register
{
    public function register(Registry $registry);
}

class JsonClassmapLoader implements Register
{
    private $file;

    public function __construct($file) {

        $this->file = $file;
    }

    public function register(Registry $registry) {

        $definitions = $this->loadDefinitionsFromFile();

        foreach ($definitions as $name => $definition) {
            $class = $definition->class;
            $path  = dirname($this->file) . '/' . $definition->file;

            $this->define($class, $path);

            $registry->registerClass($name, $class);
        }
    }

    protected function define($class, $path) {

        if (!class_exists($class)) {
            require($path);
        }
    }

    protected function loadDefinitionsFromFile() {

        $json = file_get_contents($this->file);
        return json_decode($json);
    }
}

There is not much magic here, file-names in the json file are relative to the directory of it. If a class is not yet defined (here with triggering PHP autoloading), the file of the class is being required. After that is done, the class is registered by it's name:

$registry = new Registry();
$json     = new JsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke();  # Done.

This example as well is pretty straight forward and it works. So the first problem is solved.

The second problem is the autoloading. This current variant and your previous system did hide something else, too. There are two central things to do. The one is to actually load class definitions and the other is to instantiate the object.

In your original example, autoloading technically was not necessary because the moment an object is registered within the registry, it is instantiate as well. You do this to assign the registry to it as well. I don't know if you do it only because of that or if this just happened that way to you. You write in your question that you need that.

So if you want to bring autoloading into your registry (or lazy loading), this will vary a bit. As your design is already screwed, let's continue to add more magic on top. You want to defer the instantiation of a registry component to the moment it's used the first time.

As in the registry the name of the component is more important than it's actual type, this is already pretty dynamic and a string only. To defer component creation, the class is not created when registered but when accessed. That is possible by making use of the __get function which requires a new type of Registry:

class LazyRegistry extends Registry
{
    private $defines = [];

    public function registerClass($name, $class)
    {
        $this->defines[$name] = $class;
    }

    public function __get($name) {
        $class = $this->defines[$name];
        return $this->$name = new $class($this);
    }
}

The usage example again is quite the same, however, the type of the registry has changed:

$registry = new LazyRegistry();
$json     = new JsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke();  # Done.

So now the creation of the concrete service objects has been deferred until first accessed. However this still yet is not autoloading. The loading of the class definitions is already done inside the json loader. It would not be consequent to already make verything dynamic and magic, but not that. We need an autoloader for each class that should kick in in the moment the objects is accessed the first time. E.g. we actually want to be able to have rotten code in the application that could stay there forever unnoticed because we don't care if it is used or not. But we don't want to load it into memory then.

For autoloading you should be aware of spl_autoload_register which allows you to have more than one autoloader function. There are many reasons why this is generally useful (e.g. imagine you make use of third-party packages), however this dynamic magic box called Registry of yours, it's just the perfect tool for the job. A straight forward solution (and not doing any premature optimization) is to register one autoloader function for each class we have in the registry definition. This then needs a new type of loader and the autoloader function is just two lines of code or so:

class LazyJsonClassmapLoader extends JsonClassmapLoader
{
    protected function define($class, $path) {

        $autoloader = function ($classname) use ($class, $path) {

            if ($classname === $class) {
                require($path);
            }
        };

        spl_autoload_register($autoloader);
    }
}

The usage example again didn't change much, just the type of the loader:

$registry = new LazyRegistry();
$json     = new LazyJsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke(); # Done.

Now you can be lazy as hell. And that would mean, to actually change the code again. Because you want to remote the necessity to actually put those files into that specific directory. Ah wait, that is what you asked for, so we leave it here.

Otherwise consider to configure the registry with callables that would return the instance on first access. That does normally make things more flexible. Autoloading is - as shown - independent to that if you actually can leave your directory based approach, you don't care any longer where the code is packaged in concrete (http://www.getcomposer.org/).

The whole code-example in full (without registry.json and test.class.php):

class Registry
{
    public function registerClass($name, $class) {
        $this->$name = new $class($this);
    }
}

class LazyRegistry extends Registry
{
    private $defines = [];

    public function registerClass($name, $class) {
        $this->defines[$name] = $class;
    }

    public function __get($name) {
        $class = $this->defines[$name];
        return $this->$name = new $class($this);
    }
}

interface Register
{
    public function register(Registry $registry);
}

class JsonClassmapLoader implements Register
{
    private $file;

    public function __construct($file) {

        $this->file = $file;
    }

    public function register(Registry $registry) {

        $definitions = $this->loadDefinitionsFromFile();

        foreach ($definitions as $name => $definition) {
            $class = $definition->class;
            $path  = dirname($this->file) . '/' . $definition->file;

            $this->define($class, $path);

            $registry->registerClass($name, $class);
        }
    }

    protected function define($class, $path) {

        if (!class_exists($class)) {
            require($path);
        }
    }

    protected function loadDefinitionsFromFile() {

        $json = file_get_contents($this->file);
        return json_decode($json);
    }
}

class LazyJsonClassmapLoader extends JsonClassmapLoader
{
    protected function define($class, $path) {

        $autoloader = function ($classname) use ($class, $path) {

            if ($classname === $class) {
                require($path);
            }
        };

        spl_autoload_register($autoloader);
    }
}

$registry = new LazyRegistry();
$json     = new LazyJsonClassmapLoader('path/registry.json');
$json->register($registry);

echo $registry->Service->invoke(); # Done.

I hope this is helpful, however this is mainly playing in the sandbox and you will crush that the sooner or later. What you're actually want to learn about is Inversion of Control, Dependency Injection and then about Dependency Injection containers.

The Registry you have is some sort of smell. It's all totally full of magic and dynamic. You might think this is cool for development or for having "plugins" in your system (it's easy to extend), however you should keep the amount of objects therein low.

Magic can be hard to debug, so you might want to check the format of the json file if it makes sense in your case first to prevent first-hand configuration issues.

Also consider that the registry object passed to each constructor is not one parameter but represents a dynamic amount of parameters. This will start to create side-effects the sooner or later. If you are using the registry too much, then more the sooner. These kind of side-effects will cost you maintenance a lot because by design this is already flawed, so you can only control it with hard work, heavy integration tests for the regressions etc..

However, make your own experiences, it's just some outlook not that you tell me later I didn't notice it.

like image 155
hakre Avatar answered Jan 18 '23 01:01

hakre


For your second question: the use of __autoload is discouraged and should be replaced with a spl_autoload_register. What the autoloader should do is split namespace and class:

function __autoload( $classname )
{    
  if( class_exists( $classname, false ))
    return true;

  $classparts = explode( '\\', $classname );
  $classfile = '/' . strtolower( array_pop( $classparts )) . '.php';
  $namespace = implode( '\\', $classparts );

  // at this point you have to decide how to process further

}

Depending on you file structure I would suggest building a absolute path based on the namespace and classname:

define('ROOT_PATH', __DIR__);

function __autoload( $classname )
{    
  if( class_exists( $classname, false ))
    return true;

  $classparts = explode( '\\', $classname );
  $classfile = '/' . strtolower( array_pop( $classparts )) . '.php';
  $namespace = implode( '\\', $classparts );

  $filename = ROOT_PATH . '/' . $namespace . $classfile;
  if( is_readble($filename))
    include_once $filename;
}

I've took the PSR0 approach, where the namespace is part of the path.

like image 28
JvdBerg Avatar answered Jan 18 '23 00:01

JvdBerg