Search code examples
phpinstallationcomposer-phpautoload

How to dump autoload in custom installer plugin


I have been trying to write a custom installer for my composer package. But can't get it to work. What I need and How I'm doing it now:

  • I need my package to be installed on root directory. My package name is rootdata21/hati, so I moved the hati folder there on the project root folder.

  • Now I updated the composer.json by addeding an entry to autoload psr4 properties such as:

    { "autoload": { "psr-4": { "hati\": "hati/" } } }

But I actually don't know how I can get the composer to re dump autoloader to reflect this new autload entry in composer.json file. Here is my Installer class.

<?php

namespace hati\installer;

use Composer\Installer\LibraryInstaller;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\PartialComposer;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Script\ScriptEvents;
use hati\config\ConfigWriter;
use React\Promise\PromiseInterface;

class Installer extends LibraryInstaller {

    private string $root;
    private string $hatiDir;
    protected $composer;

    public function __construct(IOInterface $io, PartialComposer $composer, $root) {
        $this -> composer = $composer;
        $this -> root = $root . DIRECTORY_SEPARATOR;
        $this -> hatiDir = $root . DIRECTORY_SEPARATOR . 'hati' . DIRECTORY_SEPARATOR;

        parent::__construct($io, $composer);
    }

    public function getInstallPath(PackageInterface $package): string {
        return 'rootdata21';
    }

    public function install(InstalledRepositoryInterface $repo, PackageInterface $package): ?PromiseInterface {

        if (file_exists($this -> hatiDir)) {
            $choice = $this -> io -> ask('Existing hati folder found. Do you want to delete it? [y/n]: ', 'n');
            if ($choice === 'y') {
                self::rmdir($this -> hatiDir);
            } else {
                $this -> io -> critical('Hati installation has been cancelled. Please delete hati folder manually.');
                return null;
            }
        }

        return parent::install($repo, $package)->then(function () {

            // Move hati folder to project root directory
            $old = $this -> root . 'rootdata21'. DIRECTORY_SEPARATOR .'hati';
            rename($old, $this -> hatiDir);

            // delete the rootdata21 folder
            self::rmdir($this -> root . 'rootdata21');

            // generate/update the hati.json file on the project root directory
            $createNewConfig = true;
            if (file_exists($this -> root . 'hati.json')) {

                while(true) {
                    $ans = $this -> io -> ask('Existing hati.json found. Do you want to merge it with new config? [y/n]: ');
                    if ($ans !== 'y' && $ans !== 'n') continue;
                    break;
                }
                $createNewConfig = $ans == 'n';
            }

            require_once "{$this -> hatiDir}config" . DIRECTORY_SEPARATOR . "ConfigWriter.php";
            $result = ConfigWriter::write($this->root, $createNewConfig);

            // show the result to the user
            if ($result['success']) {
                $this -> io -> info($result['msg']);

                $welcomeFile = $this -> hatiDir . 'page/welcome.txt';
                if (file_exists($welcomeFile)) include($welcomeFile);

            } else {
                $this -> io -> error($result['msg']);
            }

            $this -> dumpAutoload();
        });
    }

    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) {
        return parent::update($repo, $initial, $target) -> then(function () {
            require_once "{$this -> hatiDir}config" . DIRECTORY_SEPARATOR . "ConfigWriter.php";
            $result = ConfigWriter::write($this->root);

            // show the result to the user
            if ($result['success']) {
                $this -> io -> info('Hati has been updated successfully');
            } else {
                $this -> io -> error($result['msg']);
            }
        });
    }

    public function supports($packageType): bool {
        return 'hati-installer' === $packageType;
    }

    private function dumpAutoload(): void {
        $composerJsonPath = $this -> root . 'composer.json';
        $composerJson = json_decode(file_get_contents($composerJsonPath), true);
        $composerJson['autoload']['psr-4']['hati\\'] = 'hati/';
        file_put_contents($composerJsonPath, json_encode($composerJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

        // Regenerate the Composer autoload files to include your classes
        $this -> composer -> getEventDispatcher() -> dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP);
    }

    public static function rmdir($dir): bool {
        if (!file_exists($dir)) return true;

        if (!is_dir($dir)) return unlink($dir);

        foreach (scandir($dir) as $item) {
            if ($item == '.' || $item == '..') continue;

            if (!self::rmdir($dir . DIRECTORY_SEPARATOR . $item)) return false;
        }

        return rmdir($dir);
    }

}

Solution

  • I have been able to achieve what I wanted to do. So here, I will explain what help I asked for.

    Normally without using any custom installer plugin, composer installs my package in the vendor directory under folder "rootdata21/hati". But for some reasons, my entire package source code needs to be on project root directory. And I also don't want that rootdata21 as parent folder.

    So I wrote a plugin for it. The plugin returns "rootdata21" as installation path. It gets my package on root directory but the folder structure is now "rootdata21/hati". So I had to override the install method to modify that. However, even when I get my desired folder location and structure by copying/renaming/deleting folder from "rootdata21/hati" to "hati", the autoloader doesn't work for my relocated source code. I then have to manually update the composer.json file to dump the autoloader which defeats the purpose of having the installer. And this is what I was trying to achieve, so that after moving my package folder to project root, the autoloader should still work.

    Here is my final updated installer code which works they way I want.

    public function getInstallPath(PackageInterface $package): string { return 'hati'; }
    
    public function install(InstalledRepositoryInterface $repo, PackageInterface $package): ?PromiseInterface {
    
        // Setting custom psr-4 entry for hati folder being on project root
        $autoload = $package -> getAutoload();
        if (isset($autoload['psr-4'])) {
            $customPSR4 = ['hati\\' => '/',];
            $autoload['psr-4'] = array_merge($autoload['psr-4'], $customPSR4);
    
            // Let the composer know about this
            $package -> setAutoload($autoload);
        }
    
        return parent::install($repo, $package) -> then(function () {
    
            // Manipulate the hati/hati folder to hati on project root
            self::copy($this -> root . 'hati' . DIRECTORY_SEPARATOR . 'hati', $this -> root . '_temp');
            self::rmdir($this -> root . 'hati');
            rename($this -> root . '_temp',$this -> root . 'hati');
    
            // rest of the installation code goes here...
        });
    }
    

    After all of these, vendor/composer/autoload_psr4.php sets classpath correctly as you can see in the screenshot. 2023-07-31_211426

    I had to return "hati" as installation path, because returning "rootdata21" and the installation code above gets me the following as autoload_psr4.php record which doesn't work.

    'hati\\' => array($baseDir . '/rootdata')