Basic startup and namespace in php

I am currently updating the framework that I wrote some time ago. I would like to use new standards, such as namespaces, and use the autoload function.

At the moment, my structure has a very rudimentary, but functional startup function, which 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 only one folder, but the advantage is that it loads the class into my registry, allowing me to access this particular class in other files by doing something similar to $this->registry->Class->somevar which is necessary functionality.

What I need / need to do is use the autoloader function, but this function is not limited to one folder, instead it can move across several folders and create instances of the necessary classes.

I have only a few test files, 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 it is configured, it cannot find the file for MyClass2, so I wonder what I did wrong, and secondly, there is a way, like my first “startup” function, which does not need to specify the space names in the startup file and assign it to my registry?

Sorry for such a long question, but any help is much appreciated.

+4
source share
2 answers

Here I see two things.

The first makes your problem a little complicated. You want to use namespaces, but your current configuration is done through the file system. Class definition file names do not yet contain a namespace. Thus, you cannot just continue as you really are.

Secondly, you do not have what is covered by PHP autoload, you just load a specific set of classes and register it in the registry.

I'm not sure if you need PHP autoload here. Of course, it may seem promising to you that you both come together. Solving the first point will probably help you solve the problem later, so I suggest starting with it first.

Let the hidden dependencies be more visible. In your current design, you have three things:

  • The name in which the object is registered in the registry.
  • The name of the file containing the class definition.
  • The name of the class itself.

The values ​​2. and 3. are in one, you analyze the name of the class itself from the file name. As written, namespaces make this difficult now. The solution is easy, instead of reading from a list of directories, you can read from a file that contains this information. Easy configuration file format: json:

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

This contains three necessary dependencies for registering a class by name in the registry, since the file name is also known.

Then you can register the classes in the registry:

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

And add a bootloader class for 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, the file names in the json file refer to its directory. If the class has not yet been defined (here with starting PHP startup), a class file is required. After that, the class is registered by name:

 $registry = new Registry(); $json = new JsonClassmapLoader('path/registry.json'); $json->register($registry); echo $registry->Service->invoke(); # Done. 

This example is also pretty straight forward and it works. So, the first problem is solved.

The second problem is startup. This current option and your previous system also hid something else. There are two main things to do. One is to actually load the class definitions, and the other is to instantiate the object.

In your original example, autoload was not technically necessary, since the moment the object is registered in the registry, it also creates an instance. You do this to assign a registry to it. I don’t know if you will do it just because of this, or if it happened that way. You write in your question that you need it.

So, if you want to include autoload in your registry (or lazy boot), this will be slightly different. As your design is already screwed up, let's continue to add more magic from above. You want to delay the creation of a registry component until it is used for the first time.

As in the registry, the name of the component is more important than its actual type, it is already quite dynamic and only a string. To delay the creation of a component, the class is not created at registration, but at access. This is possible with the __get function, which requires a new registry type:

 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 is the same again, however the registry type has changed:

 $registry = new LazyRegistry(); $json = new JsonClassmapLoader('path/registry.json'); $json->register($registry); echo $registry->Service->invoke(); # Done. 

So, now the creation of specific service objects has been delayed until the first access. However, this is not autoload yet. Class definitions are already loaded inside the json loader. This would not be a consequence of the fact that he had already made everything very dynamic and magical, but not that. We need an autoloader for each class, which should be triggered when objects are accessed for the first time. For instance. we really want to have rotten code in the application, which can remain there forever unnoticed, because we do not care if it is used or not. But we do not want to load it into memory.

For autoload, you must know spl_autoload_register , which allows you to have more than one autoloader function. There are many reasons why this is usually useful (for example, imagine that you are using third-party packages), however this dynamic magic field called your Registry is just the perfect tool to work with. A direct solution (and not early optimization) is to register one autoloader function for each class that we have in the registry definition. This requires a new type of loader, and the autoloader function requires only two lines of code:

 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 has not changed much again, just the bootloader type:

 $registry = new LazyRegistry(); $json = new LazyJsonClassmapLoader('path/registry.json'); $json->register($registry); echo $registry->Service->invoke(); # Done. 

Now you can be lazy like hell. And that will mean to really change the code again. Since you want to remove the need to actually place these files in this particular directory. Ah wait, that’s what you asked, so we leave it here.

Otherwise, consider setting up the registry using calls that return an instance on first access. This usually makes things more flexible. Autoload - as shown - no matter if you can really leave your directory-based approach, you don't care where the code is packed into a specific one (http://www.getcomposer.org/).

The entire sample code is complete (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. 

Hope this is useful, but it is mostly played in the sandbox and you will crush it sooner or later. What you really want to know is inversion of control, dependency injection, and then dependency injection containers.

You have some kind of smell. All this is completely filled with magic and dynamics. You might think that this is cool for development or for “plugins” in your system (it is easy to expand it), however you should keep the number of objects in it.

The magic can be difficult to debug, so you may need to check the json file format, if in your case it makes sense to prevent problems with the initial settings in the first place.

Also note that the registry object passed to each constructor is not a single parameter, but a dynamic number of parameters. Sooner or later, it will start to create side effects. If you use the registry too much, the more the faster. These side effects will cost you a lot of maintenance because the design is already incorrect, so you can only control it with the help of hard work, heavy integration tests for regressions, etc.

However, make your own experience, it's just some kind of look not at what you will tell me later, I did not notice this.

+7
source

For your second question: using __autoload is not recommended and should be replaced with spl_autoload_register. What the autoloader should do is separate the 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 your file structure, I would suggest creating an absolute path based on the namespace and class name:

 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 took the PSR0 approach, where the namespace is part of the path.

+2
source

Source: https://habr.com/ru/post/1435327/


All Articles