ischenko/yii2-jsloader-requirejs

View on GitHub
src/RequireJs.php

Summary

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

namespace ischenko\yii2\jsloader;

use ischenko\yii2\jsloader\base\Loader;
use ischenko\yii2\jsloader\helpers\JsExpression;
use ischenko\yii2\jsloader\requirejs\Config;
use ischenko\yii2\jsloader\requirejs\JsRenderer;
use RuntimeException;
use yii\di\Instance;
use yii\helpers\Json;
use yii\web\View;

/**
 * RequireJS implementation
 *
 * @author Roman Ishchenko <roman@ishchenko.ck.ua>
 * @since 1.0
 */
class RequireJs extends Loader
{
    /**
     * @var string URL to be used to load the RequireJS library. If value is empty the loader will publish library from the bower package
     */
    public $libraryUrl;

    /**
     * @var string path to the RequireJS library
     */
    public $libraryPath = '@bower/requirejs/require.js';

    /**
     * @see http://requirejs.org/docs/api.html#data-main
     *
     * @var string|callable|false URL of script file that will be used as value for the data-main entry. FALSE means do not use the data-main entry.
     */
    public $main;

    /**
     * @var string|array|JsRendererInterface
     */
    public $renderer = JsRenderer::class;

    /**
     * @var Config
     */
    private $config;

    public function init()
    {
        parent::init();

        $this->renderer = Instance::ensure($this->renderer, JsRendererInterface::class);
    }

    /**
     * @return Config
     */
    public function getConfig(): ConfigInterface
    {
        if (!$this->config) {
            $this->config = new Config();
        }

        return $this->config;
    }

    /**
     * Performs actual rendering of the JS loader
     *
     * @param JsExpression[] $jsExpressions a list of js expressions indexed by position
     */
    protected function doRender(array $jsExpressions)
    {
        $resultJsCode = '';

        krsort($jsExpressions);

        foreach ($jsExpressions as $position => $jsExpression) {
            if (!empty($resultJsCode)) {
                $expression = $jsExpression;

                while (($code = $expression->getExpression()) instanceof JsExpression) {
                    $expression = $code;
                }

                if ($position === View::POS_READY && !empty($code)) {
                    $code = $this->encloseJqueryReady($code);
                }

                $expression->setExpression("{$code}\n{$resultJsCode}");
            }

            $resultJsCode = $jsExpression->render($this->renderer);
        }

        $this->publishRequireJs($resultJsCode);
    }

    /**
     * @param string $code
     */
    protected function publishRequireJs($code)
    {
        $view = $this->getView();
        $assetManager = $view->getAssetManager();

        if (empty($this->libraryUrl)) {
            list(, $this->libraryUrl) = $assetManager->publish($this->libraryPath);
        }

        $requireConfig = $this->renderRequireConfig();
        $requireOptions = ['position' => View::POS_HEAD];

        if ($this->main === false) {
            $view->registerJs($code, View::POS_END);
            $view->registerJs($requireConfig, View::POS_HEAD);
        } else {
            $requireOptions['async'] = 'async';
            $requireOptions['defer'] = 'defer';

            $main = $this->resolveMainScript($this->main, "{$requireConfig};\n{$code}");

            list(, $requireOptions['data-main']) = $assetManager->publish($main);
        }

        $view->registerJsFile($this->libraryUrl, $requireOptions);
    }

    /**
     * Performs rendering of configuration block for RequireJS
     *
     * @return string
     */
    protected function renderRequireConfig()
    {
        $jsonOptions = 320;

        if (YII_DEBUG) {
            $jsonOptions |= JSON_PRETTY_PRINT;
        }

        $config = $this->getConfig()->toArray();
        $config = Json::encode((object)array_filter($config), $jsonOptions);

        return "require.config({$config});";
    }

    /**
     * @param string $code
     *
     * @return string
     */
    private function encloseJqueryReady($code)
    {
        return "jQuery(function() {\n{$code}\n});";
    }

    /**
     * @param string $filePath
     * @param string $content
     *
     * @return string full path to a file
     *
     * @throws RuntimeException
     */
    private function writeFileContent($filePath, $content)
    {
        if (!file_exists($filePath)) {
            if (@file_put_contents($filePath, $content, LOCK_EX) === false) {
                throw new RuntimeException("Failed to write data into a file \"$filePath\"");
            }
        }

        return $filePath;
    }

    /**
     * @param mixed $main
     * @param string $code
     * @return string
     */
    private function resolveMainScript($main, $code): string
    {
        if (is_callable($main)) {
            $main = call_user_func($main, $code, $this);
        }

        if (empty($main = trim($main))) {
            $main = md5($code) . '.js';
        }

        $path = $this->getRuntimePath()
            . DIRECTORY_SEPARATOR . ltrim($main, DIRECTORY_SEPARATOR);

        return $this->writeFileContent($path, $code);
    }
}