ischenko/yii2-jsloader

View on GitHub
src/base/Loader.php

Summary

Maintainability
A
2 hrs
Test Coverage
<?php
/**
 * @copyright Copyright (c) 2016 Roman Ishchenko
 * @license https://github.com/ischenko/yii2-jsloader/blob/master/LICENSE
 * @link https://github.com/ischenko/yii2-jsloader#readme
 */

namespace ischenko\yii2\jsloader\base;

use ischenko\yii2\jsloader\ConfigInterface;
use ischenko\yii2\jsloader\filters\Chain as ChainFilter;
use ischenko\yii2\jsloader\filters\ClassName as ClassNameFilter;
use ischenko\yii2\jsloader\filters\NotEmptyFiles as NotEmptyFilesFilter;
use ischenko\yii2\jsloader\filters\Position as PositionFilter;
use ischenko\yii2\jsloader\helpers\JsExpression;
use ischenko\yii2\jsloader\LoaderInterface;
use ischenko\yii2\jsloader\ModuleInterface;
use Yii;
use yii\base\BaseObject;
use yii\base\InvalidConfigException;
use yii\helpers\FileHelper;
use yii\web\AssetBundle;
use yii\web\View;

/**
 * Base class for JS loaders
 *
 * @author Roman Ishchenko <roman@ishchenko.ck.ua>
 * @since 1.0
 */
abstract class Loader extends BaseObject implements LoaderInterface
{
    /**
     * @var string
     */
    public $runtimePath = '@runtime/jsloader';

    /**
     * @var View
     */
    private $view;

    /**
     * @var ClassNameFilter
     */
    private $ignoredBundles;

    /**
     * @var PositionFilter
     */
    private $ignoredPositions;

    /**
     * @var array
     */
    private $seenAssetBundles = [];

    /**
     * Loader constructor.
     *
     * @param View $view
     * @param array $config
     */
    public function __construct(View $view, array $config = [])
    {
        $this->view = $view;
        $this->setIgnoreBundles([]);
        $this->setIgnorePositions([View::POS_HEAD]);

        parent::__construct($config);
    }

    /**
     * @return \ischenko\yii2\jsloader\ConfigInterface an object that implements configuration interface
     */
    abstract public function getConfig(): ConfigInterface;

    /**
     * @return \yii\web\View the view object associated with the loader
     */
    public function getView()
    {
        return $this->view;
    }

    /**
     * @param array $bundles a list of asset bundles names which should be ignored by the loader
     */
    public function setIgnoreBundles(array $bundles)
    {
        $this->ignoredBundles = new ClassNameFilter($bundles);
    }

    /**
     * @param array $positions a list of positions which should be skipped by the loader
     */
    public function setIgnorePositions(array $positions)
    {
        $this->ignoredPositions = new PositionFilter($positions);
    }

    /**
     * Sets new configuration for the loader
     *
     * @param ConfigInterface|array $config
     * @return $this
     */
    public function setConfig($config): LoaderInterface
    {
        $configObject = $this->getConfig();

        if (is_object($config)) {
            $config = get_object_vars($config);
        }

        foreach ((array)$config as $key => $value) {
            $configObject->$key = $value;
        }

        $this->seenAssetBundles = [];

        return $this;
    }

    /**
     * Performs processing of assets registered in the view
     *
     * @return void
     */
    public function processAssets(): void
    {
        $expressions = [];

        foreach ([
                     View::POS_HEAD,
                     View::POS_BEGIN,
                     View::POS_END,
                     View::POS_LOAD,
                     View::POS_READY
                 ] as $position
        ) {
            if ($this->ignoredPositions->match($position)) {
                continue;
            }

            if (($jsExpression = $this->createJsExpression($position)) === null) {
                continue;
            }

            $expressions[$position] = $jsExpression;
        }

        $this->renderJs($expressions);
    }

    /**
     * Start processing asset bundles with further publish
     * @since 1.3
     */
    public function processBundles(): void
    {
        $view = $this->getView();

        foreach (array_keys($view->assetBundles) as $name) {
            $this->registerAssetBundle($name);
        }
    }

    /**
     * Adds a new JS expressions to loader
     *
     * @param array $expressions
     *
     * @return void
     */
    abstract protected function renderJs(array $expressions): void;

    /**
     * Registers asset bundle in the loader
     *
     * @param string $name
     *
     * @return ModuleInterface|false an instance of registered module or false if asset bundle was not registered
     */
    protected function registerAssetBundle(string $name)
    {
        if (($bundle = $this->getAssetBundleFromView($name)) === false) {
            return $bundle;
        }

        $config = $this->getConfig();

        if (!($module = $config->getModule($name))) {
            $module = $config->addModule($name);
        } elseif (isset($this->seenAssetBundles[$name])) {
            return $module;
        }

        $this->seenAssetBundles[$name] = true;

        $options = ['baseUrl' => $bundle->baseUrl];

        $options += $bundle->jsOptions ?: [];
        $options += ['position' => View::POS_END];

        $module->setOptions($options);

        foreach ($bundle->depends as $dependency) {
            if (($dependency = $this->registerAssetBundle($dependency)) !== false) {
                $module->addDependency($dependency);
            }
        }

        $this->importJsFilesFromBundle($bundle, $module);

        return $module;
    }

    /**
     * @return string a path to runtime folder
     */
    protected function getRuntimePath()
    {
        static $runtimePath = [];

        if (!isset($runtimePath[$this->runtimePath])) {
            $rtPath = Yii::getAlias($this->runtimePath);

            if (!FileHelper::createDirectory($rtPath)) {
                throw new InvalidConfigException('Cannot create runtime folder "' . $rtPath . '"');
            }

            $runtimePath[$this->runtimePath] = $rtPath;
        }

        return $runtimePath[$this->runtimePath];
    }

    /**
     * @param integer $position
     *
     * @return JsExpression
     */
    private function createJsExpression($position)
    {
        $chainFilter = new ChainFilter(
            [
                new PositionFilter($position),
                new NotEmptyFilesFilter()
            ]
            , ChainFilter::LOGICAL_AND
        );

        $modules = $this->getConfig()->getModules($chainFilter);
        $code = $this->importJsCodeFromView($position);
        $depends = $this->importJsFilesFromView($position);

        $jsExpression = null;

        if (!empty($code) || !empty($depends)) {
            $jsExpression = new JsExpression($code, $depends);
        }

        if ($modules !== []) {
            $jsExpression = new JsExpression($jsExpression, $modules);
        }

        return $jsExpression;
    }

    /**
     * @param integer $position
     *
     * @return array
     */
    private function importJsFilesFromView($position)
    {
        $modules = [];
        $view = $this->getView();
        $config = $this->getConfig();

        if (!empty($view->jsFiles[$position])) {
            foreach ($view->jsFiles[$position] as $jsFile) {
                if (preg_match('/src=(["\\\'])(.*?)\1/', $jsFile, $matches)) {
                    $modules[] = $config->addModule(md5($matches[2]))
                        ->addFile($matches[2], ['position' => $position]);
                }
            }

            unset($view->jsFiles[$position]);
        }

        return $modules;
    }

    /**
     * @param integer $position
     *
     * @return string
     */
    private function importJsCodeFromView($position)
    {
        $code = '';
        $view = $this->getView();

        if (!empty($view->js[$position])) {
            $code = implode("\n", $view->js[$position]);
            unset($view->js[$position]);
        }

        return $code;
    }

    /**
     * @param string $name
     *
     * @return AssetBundle|false an asset bundle from the view or false if asset bundle not found
     */
    private function getAssetBundleFromView($name)
    {
        $view = $this->getView();

        if (!isset($view->assetBundles[$name])) {
            return false;
        }

        $bundle = $view->assetBundles[$name];

        if (!($bundle instanceof AssetBundle)) {
            return false;
        }

        if ($this->ignoredBundles->match($name)
            || $this->ignoredPositions->match($bundle->jsOptions)
        ) {
            return false;
        }

        return $bundle;
    }

    /**
     * @param AssetBundle $bundle
     * @param ModuleInterface $module
     */
    private function importJsFilesFromBundle(AssetBundle $bundle, ModuleInterface $module)
    {
        $ignoredJs = [];
        $assetManager = $this->getView()->getAssetManager();

        foreach ($bundle->js as $js) {
            $file = $js;
            $options = [];

            if (is_array($js)) {
                if ($this->ignoredPositions->match($js)) {
                    $ignoredJs[] = $js;
                    continue;
                }

                $file = array_shift($js);
                $options = $js;
            }

            $module->addFile($assetManager->getAssetUrl($bundle, $file), $options);
        }

        $bundle->js = $ignoredJs;
    }
}