luyadev/luya-composer

View on GitHub
src/Installer.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

namespace luya\composer;

use Composer\Config;
use Composer\Installer\LibraryInstaller;
use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface;
use React\Promise\PromiseInterface;

/**
 * LUYA Package Installer.
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.1
 */
class Installer extends LibraryInstaller
{
    const LUYA_EXTRA = 'luya';
    
    const LUYA_FILE = 'luyadev/installer.php';

    const LUYA_TYPE_CORE = 'luya-core';

    const LUYA_TYPE_EXTENSION = 'luya-extension';

    const LUYA_TYPE_MODULE = 'luya-module';

    const LUYA_TYPE_THEME = 'luya-theme';
    
    /**
     * {@inheritDoc}
     */
    public function supports($packageType)
    {
        return $packageType == self::LUYA_TYPE_CORE || 
            $packageType == self::LUYA_TYPE_EXTENSION || 
            $packageType == self::LUYA_TYPE_MODULE || 
            $packageType == self::LUYA_TYPE_THEME;
    }
    
    /**
     * {@inheritDoc}
     */
    public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
    {
        // install the package the normal composer way
        $promise = parent::install($repo, $package);
        $this->addPackage($package);
        
        // Composer v2 might return a promise here
        if ($promise instanceof PromiseInterface) {
            return $promise->then();
        }
    }
    
    /**
     * {@inheritDoc}
     */
    public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
    {
        $promise = parent::update($repo, $initial, $target);
        $this->removePackage($initial);
        $this->addPackage($target);
        
        // Composer v2 might return a promise here
        if ($promise instanceof PromiseInterface) {
            return $promise->then();
        }
    }
    
    /**
     * {@inheritDoc}
     */
    public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
    {
        $promise = parent::uninstall($repo, $package);
        $this->removePackage($package);
        
        // Composer v2 might return a promise here
        if ($promise instanceof PromiseInterface) {
            return $promise->then();
        }
    }
    
    /**
     * Add a package to installer
     *
     * @param PackageInterface $package
     * @return void
     */
    protected function addPackage(PackageInterface $package)
    {
        if (!$this->isPackageInComposerConfig($package)) {
            return;
        }
        
        $this->writeInstaller($this->addConfig($package));
    }
    
    /**
     * Remove a package from installer
     *
     * @param PackageInterface $package
     * @return void
     */
    protected function removePackage(PackageInterface $package)
    {
        $this->writeInstaller($this->removeConfig($package));
    }
    
    /**
     * Get the installer array
     *
     * @return array
     */
    protected function getInstallers()
    {
        $file = $this->vendorDir . DIRECTORY_SEPARATOR . self::LUYA_FILE;
        
        if (!file_exists($file)) {
            return ['configs' => [], 'timestamp' => time()];
        }
        
        if (function_exists('opcache_invalidate')) {
            @opcache_invalidate($file, true);
        }
        
        $data = require($file);
        $data['timestamp'] = time();
        return $data;
    }
    
    /**
     * Check if the package is in the require or require-dev config.
     * 
     * If the package is not in the "root" composer.json available, the installer should not add those packages.
     *
     * @param PackageInterface $package
     * @return boolean
     * @since 1.1.0
     */
    public function isPackageInComposerConfig(PackageInterface $package)
    {
        if (array_key_exists($package->getName(), $this->composer->getPackage()->getRequires())) {
            return true;
        }

        if (array_key_exists($package->getName(), $this->composer->getPackage()->getDevRequires())) {
            return true;
        }

        $this->io->write("Package {$package->getName()} will be ignored by luyadev installer as its not part of the composer.json requirements.");

        return false;
    }

    /**
     * Ensure a config for a package.
     *
     * @param PackageInterface $package
     * @param array $config
     * @return void
     */
    protected function ensureConfig(PackageInterface $package, array $config)
    {

        // generate the package folder, which is actually the name but with os based directory seperator
        $packageFolder = str_replace("/", DIRECTORY_SEPARATOR, $package->getPrettyName());
        
        $packageConfig = [
            'package' => [
                'isDev' => $package->isDev(),
                'name' => $package->getName(),
                'prettyName' => $package->getPrettyName(),
                'version' => $package->getVersion(),
                'targetDir' => $package->getTargetDir(),
                'installSource' => $package->getInstallationSource(),
                'sourceUrl' => $package->getSourceUrl(),
                'packageFolder' => $packageFolder,
            ],
            'blocks' => [],
            'bootstrap' => (isset($config['bootstrap'])) ? ComposerHelper::parseDirectorySeperator($config['bootstrap']) : [],
            'themes' => [],
        ];
        
        $blocks = isset($config['blocks']) ? $config['blocks'] : [];
    
        foreach ($blocks as $blockFolder) {
            $packageConfig['blocks'][] = $this->getRelativeVendorDir() . DIRECTORY_SEPARATOR . $packageFolder . DIRECTORY_SEPARATOR . ComposerHelper::parseDirectorySeperator(ltrim($blockFolder, DIRECTORY_SEPARATOR));
        }
    
        $themes = isset($config['themes']) ? $config['themes'] : [];
    
        foreach ($themes as $themeFolder) {
            $packageConfig['themes'][] = $this->getRelativeVendorDir() . DIRECTORY_SEPARATOR . $package->getPrettyName() . DIRECTORY_SEPARATOR . ComposerHelper::parseDirectorySeperator(ltrim($themeFolder, '/'));
        }
        
        return $packageConfig;
    }
     
    /**
     * Remove a package from the config
     *
     * @param PackageInterface $package
     * @return array
     */
    protected function removeConfig(PackageInterface $package)
    {
        $data = $this->getInstallers();
        
        if (isset($data['configs'][$package->getName()])) {
            unset($data['configs'][$package->getName()]);
        }
        
        return $data;
    }
    
    /**
     * Get the LUYA extra binary data.
     *
     * @param PackageInterface $package
     * @return array
     */
    protected function getPackageExtraData(PackageInterface $package)
    {
        if (empty($package->getExtra())) {
            return [];
        }
        
        return isset($package->getExtra()[self::LUYA_EXTRA]) ? $package->getExtra()[self::LUYA_EXTRA] : [];
    }
    
    /**
     * Add a package to the config
     *
     * @param PackageInterface $package
     * @return array
     */
    protected function addConfig(PackageInterface $package)
    {
        $data = $this->getInstallers();
        $data['configs'][$package->getName()] = $this->ensureConfig($package, $this->getPackageExtraData($package));
        
        return $data;
    }
    
    /**
     * Write the installer.php file in vendor folder
     *
     * @param array $data
     * @return void
     */
    protected function writeInstaller(array $data)
    {
        $file = $this->vendorDir . DIRECTORY_SEPARATOR . self::LUYA_FILE;
        
        if (!file_exists(dirname($file))) {
            mkdir(dirname($file), 0777, true);
        }
        
        $array = str_replace("'<vendor-dir>", '$vendorDir . \'', var_export($data, true));
        if (file_put_contents($file, "<?php\n\n\$vendorDir = dirname(__DIR__);\n\nreturn $array;\n") === false) {
            $this->io->writeError("Unable to create luya installer file.");
        }
        
        // Invalidate opcache of plugins.php if it exists
        if (function_exists('opcache_invalidate')) {
            @opcache_invalidate($file, true);
        }
    }

    private $_relativeVendorDir;
    
    /**
     * Read the relative vendor-dir from composer config.
     *
     * @return string
     * @since 1.0.4
     */
    public function getRelativeVendorDir()
    {
        if ($this->_relativeVendorDir === null) {
            $this->_relativeVendorDir = rtrim($this->composer->getConfig()->get('vendor-dir', Config::RELATIVE_PATHS), DIRECTORY_SEPARATOR);
        }
        
        return $this->_relativeVendorDir;
    }
}