Another bike: writing your own class autoloader for Bitrix

No matter what anyone says, but I think that the invention of the bicycle is a useful thing. Using ready-made libraries and frameworks, of course, is good, but sometimes you should put them off and create something of your own. This is how we keep the brain in good shape and realize our creative potential.



The article is going to be long, so sit back as I begin.





UPD: As it turned out, the method described in this article does not help in all cases - when it comes to ORM, where the naming of classes and files is different (for example, the ConfigTable class, which is in the config.php file), problems and errors begin. Therefore, it is still better to use Composer.




So, Bitrix, or rather, Bitrix Framework. Despite the presence of a rich API, from time to time there is a need to create your own classes / libraries, as well as connect third-party ones. Therefore, to begin with, let's look at the existing autoload methods.



Good old include / require. I added it purely for historical reference. Although at the dawn of my programming path, I put the necessary classes and libraries in a separate folder, created a separate file where I included all these classes and only then included the file with the inclusions (I apologize for the tautology).



Composer.Allows you to connect both your own classes and third-party libraries. However, when adding new classes, it requires a manual update. In addition, the classes themselves, files and namespaces also need to be written manually. You can read about how to make Bitrix friends with a composer here



Bitrix loader . It is used both for connecting modules and for autoloading classes. However, before connecting the necessary classes, you will have to form an array, where the keys will be the names of the classes, and the values ​​of the path to them. And it will all look something like this:



$classes = [
    'Namespace\\Package\\ClassName' => '/path/to/class.php'
];

Loader::registerAutloadClasses(null, $classes);


Custom modules. They say that this is the most recommended way - you create a module, install it in the admin area, then connect it anywhere and use it for your pleasure. It looks simple, but in reality we have the following:



  • In addition to writing classes, you also need to register the procedure for installing and removing the module. There are a number of required parameters and methods, without which the module may not work (although I don’t know, I haven’t tested it)
  • Classes will not work without connecting the module
  • It doesn't always make sense to move a class into a separate module


Nevertheless, if you wrote your local module and then decided to add a couple more classes to it, then to use them you no longer need to reinstall the module - just call the necessary methods in the right place, and that's it!



Well, now, in fact, the bike itself ...



After analyzing all the above methods, I thought about what to come up with so that it would be enough to simply add new classes to a certain place, and they would then be loaded automatically, without adding new elements to the array of namespaces and file paths.



As a result, it was decided to write a special module - as strange as it may sound, but this idea seemed to me more successful than adding several functions to init.php - which will automatically load all classes from the required directory.



I will omit the process of writing the installation / removal of a module - whoever needs it, they will look in the source, and go straight to the main functionality.



Because initially, the number of levels of nesting of folders is unknown, then the methods need to be recursive. We will also use the Bitrix \ Main \ Loader class, which will load the classes.



Let's imagine that we decided to put all our classes in the / local / php_interface / lib directory:



image



We may also have files that do not contain classes and, accordingly, should not be included in the autoloader, so this point should also be taken into account.



So let's go.



namespace Ramapriya\LoadManager;

use Bitrix\Main\Loader;

class Autoload
{
}


First of all, we need to get all the contents of our folder. To do this, let's write the scanDirectory method:



    public static function scanDirectory(string $dir) : array
    {
        $result = [];
        $scanner = scandir($dir); //   
        foreach ($scanner as $scan) {
            switch ($scan) {
                // 
                case '.': 
                case '..':
                    break;
                default:
//                          
                    $item = $dir . '/' . $scan; 
                    $SplFileInfo = new \SplFileInfo($item);
    
                    if($SplFileInfo->isFile()) {
//    ,        
                        $result[] = $scan; 
                        
                    } elseif ($SplFileInfo->isDir()) {
//    ,                                 
                        $result[$scan] = self::scanDirectory($item, $result[$scan]); 
    
                    }
            }
        }
    
        return $result;
    }


The output should be the following:







As we can see, the file structure is respected, so you can start forming an array for autoloading:



/*     $defaultNamespace,        . 
   php-,      
*/
    public static function prepareAutoloadClassesArray(string $directory, string $defaultNamespace, array $excludeFiles) : array
    {
        $result = [];
//   
        $scanner = self::scanDirectory($directory); 
    
        foreach ($scanner as $key => $value) {
    
            $sep = '\\';
            
            switch(gettype($key)) {
                
                case 'string':
//     ,    
                    $SplFileInfo = new \SplFileInfo($directory . '/' . $key);
                    $classNamespace = $defaultNamespace . $sep . $key;
    
                    if($SplFileInfo->isDir()) {
//   ,    ,   ,    ,      
                        $tempResult = self::prepareAutoloadClassesArray($directory . '/' . $key, $classNamespace, $excludeFiles);
                        foreach($tempResult as $class => $file) {
//         
                            $result[$class] = $file; 
                        }
                    }
    
                    break;
    
                case 'integer':
//    - ,        
                    $SplFileInfo = new \SplFileInfo($directory . '/' . $value);
//      (           ,    )
                    $classNamespace = $defaultNamespace . $sep . str_ireplace('.php', '', $SplFileInfo->getBasename()); 

//      php-
                    if(
                        $SplFileInfo->isFile() &&
                        $SplFileInfo->getExtension() === 'php'
                    ) {
 //      ,      
                        foreach($excludeFiles as $excludeFile) {
                            if($SplFileInfo->getBasename() !== $excludeFile) {
//        
                                $result[$classNamespace] = str_ireplace($_SERVER['DOCUMENT_ROOT'], '', $directory . '/' . $value); 
                            }
                        }                        
                        
                    }
    
                    break;
                    
            }
    
        }
    
        return $result;
    }


If everything is done correctly, then in the end we will get a generated array for autoloading using a bitrix loader:







To check the functionality, add the MainException.php file to the folder with exceptions, containing the following class:



<?php

namespace Ramapriya\Exceptions;

class MainException extends \Exception
{
    public function __construct($message = null, $code = 0, Exception $previous = null)
    {
        parent::__construct($message, $code, $previous);
    }
}


As we can see, our file has been loaded into an array of classes:







Looking ahead, let's try to call our new exception:



throw new Ramapriya\Exceptions\MainException('test exception');


As a result, we will see:



[Ramapriya\Exceptions\MainException] 
test exception (0)


So, it remains for us to implement the autoloading method for the resulting array. For this purpose, we will write a method with the most banal name loadClasses, where we will pass the resulting array:




    public static function loadClasses(array $classes, $moduleId = null)
    {
        Loader::registerAutoloadClasses($moduleId, $classes);
    }


This method uses a bitrix loader, which registers an array with our classes.



Now there is very little left - to form an array with classes and load them using the class we have written. To do this, in our lib folder, create an include.php file:



<?php

use Bitrix\Main\Loader;
use Bitrix\Main\Application;
use Ramapriya\LoadManager\Autoload;

//    -      ,     
Loader::includeModule('ramapriya.loadmanager');

$defaultNamespace = 'Ramapriya';
$excludeFiles = ['include.php'];

$libDir = Application::getDocumentRoot() . '/local/php_interface/lib';

$autoloadClasses = Autoload::prepareAutoloadClassesArray($libDir, $defaultNamespace, $excludeFiles);

Autoload::loadClasses($autoloadClasses);


Next, let's include this file in init.php:



// init.php

$includeFile = $_SERVER['DOCUMENT_ROOT'] . '/local/php_interface/lib/include.php';

if(file_exists($includeFile)) {
    require_once $includeFile;
}


Instead of a conclusion



Well, congratulations, our bike is ready and does an excellent job with its function.

The sources are, as always, on the github .



Thanks for your attention.



All Articles