AssetController.php 21.2 KB
Newer Older
Qiang Xue committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\console\controllers;

use Yii;
use yii\console\Exception;
use yii\console\Controller;

/**
15 16
 * This command allows you to combine and compress your JavaScript and CSS files.
 *
17 18 19 20 21 22 23 24 25 26 27 28 29 30
 * Usage:
 * 1. Create a configuration file using 'template' action:
 *    yii asset/template /path/to/myapp/config.php
 * 2. Edit the created config file, adjusting it for your web application needs.
 * 3. Run the 'compress' action, using created config:
 *    yii asset /path/to/myapp/config.php /path/to/myapp/config/assets_compressed.php
 * 4. Adjust your web application config to use compressed assets.
 *
 * Note: in the console environment some path aliases like '@wwwroot' and '@www' may not exist,
 * so corresponding paths inside the configuration should be specified directly.
 *
 * Note: by default this command relies on an external tools to perform actual files compression,
 * check [[jsCompressor]] and [[cssCompressor]] for more details.
 *
31 32
 * @property array|\yii\web\AssetManager $assetManager asset manager, which will be used for assets processing.
 *
Qiang Xue committed
33 34 35
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
36
class AssetController extends Controller
Qiang Xue committed
37
{
38 39 40
	/**
	 * @var string controller default action ID.
	 */
Qiang Xue committed
41
	public $defaultAction = 'compress';
42 43 44 45 46
	/**
	 * @var array list of asset bundles to be compressed.
	 * The keys are the bundle names, and the values are the configuration
	 * arrays for creating the [[yii\web\AssetBundle]] objects.
	 */
Qiang Xue committed
47
	public $bundles = array();
48 49 50 51
	/**
	 * @var array list of paths to the extensions, which assets should be also compressed.
	 * Each path should contain asset manifest file named "assets.php".
	 */
Qiang Xue committed
52 53
	public $extensions = array();
	/**
54 55 56
	 * @var array list of asset bundles, which represents output compressed files.
	 * You can specify the name of the output compressed file using 'css' and 'js' keys:
	 * For example:
Qiang Xue committed
57 58 59
	 * ~~~
	 * 'all' => array(
	 *     'css' => 'all.css',
60
	 *     'js' => 'all.js',
Qiang Xue committed
61 62 63
	 *     'depends' => array( ... ),
	 * )
	 * ~~~
64 65
	 * File names can contain placeholder "{ts}", which will be filled by current timestamp, while
	 * file creation.
Qiang Xue committed
66 67
	 */
	public $targets = array();
68
	/**
69 70
	 * @var array|\yii\web\AssetManager [[yii\web\AssetManager]] instance or its array configuration, which will be used
	 * for assets processing.
71
	 */
72
	private $_assetManager = array();
73
	/**
74
	 * @var string|callback JavaScript file compressor.
75 76
	 * If a string, it is treated as shell command template, which should contain
	 * placeholders {from} - source file name - and {to} - output file name.
77
	 * Otherwise, it is treated as PHP callback, which should perform the compression.
78 79 80 81
	 *
	 * Default value relies on usage of "Closure Compiler"
	 * @see https://developers.google.com/closure/compiler/
	 */
82
	public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}';
83 84 85 86
	/**
	 * @var string|callback CSS file compressor.
	 * If a string, it is treated as shell command template, which should contain
	 * placeholders {from} - source file name - and {to} - output file name.
87
	 * Otherwise, it is treated as PHP callback, which should perform the compression.
88 89 90 91
	 *
	 * Default value relies on usage of "YUI Compressor"
	 * @see https://github.com/yui/yuicompressor/
	 */
92
	public $cssCompressor = 'java -jar yuicompressor.jar {from} -o {to}';
Qiang Xue committed
93

94
	/**
95 96
	 * Returns the asset manager instance.
	 * @throws \yii\console\Exception on invalid configuration.
97 98 99 100 101 102 103 104 105
	 * @return \yii\web\AssetManager asset manager instance.
	 */
	public function getAssetManager()
	{
		if (!is_object($this->_assetManager)) {
			$options = $this->_assetManager;
			if (!isset($options['class'])) {
				$options['class'] = 'yii\\web\\AssetManager';
			}
106 107 108 109 110 111
			if (!isset($options['basePath'])) {
				throw new Exception("Please specify 'basePath' for the 'assetManager' option.");
			}
			if (!isset($options['baseUrl'])) {
				throw new Exception("Please specify 'baseUrl' for the 'assetManager' option.");
			}
112 113 114 115 116 117
			$this->_assetManager = Yii::createObject($options);
		}
		return $this->_assetManager;
	}

	/**
118
	 * Sets asset manager instance or configuration.
119 120 121 122 123 124 125 126 127 128 129
	 * @param \yii\web\AssetManager|array $assetManager asset manager instance or its array configuration.
	 * @throws \yii\console\Exception on invalid argument type.
	 */
	public function setAssetManager($assetManager)
	{
		if (is_scalar($assetManager)) {
			throw new Exception('"' . get_class($this) . '::assetManager" should be either object or array - "' . gettype($assetManager) . '" given.');
		}
		$this->_assetManager = $assetManager;
	}

130
	/**
131
	 * Combines and compresses the asset files according to the given configuration.
132 133
	 * During the process new asset bundle configuration file will be created.
	 * You should replace your original asset bundle configuration with this file in order to use compressed files.
134
	 * @param string $configFile configuration file name.
135
	 * @param string $bundleFile output asset bundles configuration file name.
136
	 */
Qiang Xue committed
137 138 139 140
	public function actionCompress($configFile, $bundleFile)
	{
		$this->loadConfiguration($configFile);
		$bundles = $this->loadBundles($this->bundles, $this->extensions);
Qiang Xue committed
141
		$targets = $this->loadTargets($this->targets, $bundles);
142
		$this->publishBundles($bundles, $this->assetManager);
Qiang Xue committed
143
		$timestamp = time();
144 145
		foreach ($targets as $name => $target) {
			echo "Creating output bundle '{$name}':\n";
Qiang Xue committed
146
			if (!empty($target->js)) {
Qiang Xue committed
147 148
				$this->buildTarget($target, 'js', $bundles, $timestamp);
			}
Qiang Xue committed
149
			if (!empty($target->css)) {
Qiang Xue committed
150 151
				$this->buildTarget($target, 'css', $bundles, $timestamp);
			}
152
			echo "\n";
Qiang Xue committed
153 154 155
		}

		$targets = $this->adjustDependency($targets, $bundles);
Qiang Xue committed
156
		$this->saveTargets($targets, $bundleFile);
Qiang Xue committed
157 158
	}

159 160 161 162 163
	/**
	 * Applies configuration from the given file to self instance.
	 * @param string $configFile configuration file name.
	 * @throws \yii\console\Exception on failure.
	 */
Qiang Xue committed
164 165
	protected function loadConfiguration($configFile)
	{
166
		echo "Loading configuration from '{$configFile}'...\n";
Qiang Xue committed
167
		foreach (require($configFile) as $name => $value) {
168
			if (property_exists($this, $name) || $this->canSetProperty($name)) {
Qiang Xue committed
169 170
				$this->$name = $value;
			} else {
Qiang Xue committed
171
				throw new Exception("Unknown configuration option: $name");
Qiang Xue committed
172 173 174
			}
		}

175
		$this->getAssetManager(); // check if asset manager configuration is correct
Qiang Xue committed
176 177
	}

178
	/**
179
	 * Creates full list of source asset bundles.
180 181
	 * @param array[] $bundles list of asset bundle configurations.
	 * @param array $extensions list of the extension paths.
182
	 * @return \yii\web\AssetBundle[] list of source asset bundles.
183
	 */
Qiang Xue committed
184 185
	protected function loadBundles($bundles, $extensions)
	{
186
		echo "Collecting source bundles information...\n";
187 188

		$assetManager = $this->getAssetManager();
Qiang Xue committed
189
		$result = array();
190 191 192 193

		$assetManager->bundles = $bundles;
		foreach ($assetManager->bundles as $name => $bundle) {
			$result[$name] = $assetManager->getBundle($name);
Qiang Xue committed
194
		}
195

Qiang Xue committed
196 197 198 199 200
		foreach ($extensions as $path) {
			$manifest = $path . '/assets.php';
			if (!is_file($manifest)) {
				continue;
			}
201 202
			$assetManager->bundles = require($manifest);
			foreach ($assetManager->bundles as $name => $bundle) {
Qiang Xue committed
203
				if (!isset($result[$name])) {
204
					$result[$name] = $assetManager->getBundle($name);
Qiang Xue committed
205 206 207
				}
			}
		}
208 209 210 211 212

		foreach ($result as $name => $bundle) {
			$this->loadBundleDependency($name, $bundle, $result);
		}

Qiang Xue committed
213 214 215
		return $result;
	}

216 217 218 219 220 221 222
	/**
	 * Loads asset bundle dependencies recursively.
	 * @param string $name bundle name
	 * @param \yii\web\AssetBundle $bundle bundle instance
	 * @param array $result already loaded bundles list.
	 * @throws \yii\console\Exception on failure.
	 */
resurtm committed
223 224
	protected function loadBundleDependency($name, $bundle, &$result)
	{
225 226 227 228 229 230 231 232
		if (!empty($bundle->depends)) {
			$assetManager = $this->getAssetManager();
			foreach ($bundle->depends as $dependencyName) {
				if (!array_key_exists($dependencyName, $result)) {
					$dependencyBundle = $assetManager->getBundle($dependencyName);
					if ($dependencyBundle === null) {
						throw new Exception("Unable to load dependency bundle '{$dependencyName}' for bundle '{$name}'.");
					} else {
233
						$result[$dependencyName] = false;
234
						$this->loadBundleDependency($dependencyName, $dependencyBundle, $result);
235
						$result[$dependencyName] = $dependencyBundle;
236
					}
237 238 239 240
				} else {
					if ($result[$dependencyName] === false) {
						throw new Exception("A circular dependency is detected for target '{$dependencyName}'.");
					}
241 242 243 244 245
				}
			}
		}
	}

246
	/**
247 248 249 250
	 * Creates full list of output asset bundles.
	 * @param array $targets output asset bundles configuration.
	 * @param \yii\web\AssetBundle[] $bundles list of source asset bundles.
	 * @return \yii\web\AssetBundle[] list of output asset bundles.
251 252
	 * @throws \yii\console\Exception on failure.
	 */
Qiang Xue committed
253 254
	protected function loadTargets($targets, $bundles)
	{
255
		// build the dependency order of bundles
Qiang Xue committed
256 257 258 259 260
		$registered = array();
		foreach ($bundles as $name => $bundle) {
			$this->registerBundle($bundles, $name, $registered);
		}
		$bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1));
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286

		// fill up the target which has empty 'depends'.
		$referenced = array();
		foreach ($targets as $name => $target) {
			if (empty($target['depends'])) {
				if (!isset($all)) {
					$all = $name;
				} else {
					throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name");
				}
			} else {
				foreach ($target['depends'] as $bundle) {
					if (!isset($referenced[$bundle])) {
						$referenced[$bundle] = $name;
					} else {
						throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time.");
					}
				}
			}
		}
		if (isset($all)) {
			$targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced));
		}

		// adjust the 'depends' order for each target according to the dependency order of bundles
		// create an AssetBundle object for each target
Qiang Xue committed
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
		foreach ($targets as $name => $target) {
			if (!isset($target['basePath'])) {
				throw new Exception("Please specify 'basePath' for the '$name' target.");
			}
			if (!isset($target['baseUrl'])) {
				throw new Exception("Please specify 'baseUrl' for the '$name' target.");
			}
			usort($target['depends'], function ($a, $b) use ($bundleOrders) {
				if ($bundleOrders[$a] == $bundleOrders[$b]) {
					return 0;
				} else {
					return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1;
				}
			});
			$target['class'] = 'yii\\web\\AssetBundle';
			$targets[$name] = Yii::createObject($target);
		}
		return $targets;
	}

Qiang Xue committed
307
	/**
308 309
	 * Publishes given asset bundles.
	 * @param \yii\web\AssetBundle[] $bundles asset bundles to be published.
Qiang Xue committed
310
	 */
311
	protected function publishBundles($bundles)
Qiang Xue committed
312
	{
313
		echo "\nPublishing bundles:\n";
314
		$assetManager = $this->getAssetManager();
315
		foreach ($bundles as $name => $bundle) {
316
			$bundle->publish($assetManager);
317
			echo "  '".$name."' published.\n";
Qiang Xue committed
318
		}
319
		echo "\n";
Qiang Xue committed
320 321 322
	}

	/**
323 324
	 * Builds output asset bundle.
	 * @param \yii\web\AssetBundle $target output asset bundle
325
	 * @param string $type either 'js' or 'css'.
326 327 328
	 * @param \yii\web\AssetBundle[] $bundles source asset bundles.
	 * @param integer $timestamp current timestamp.
	 * @throws Exception on failure.
Qiang Xue committed
329
	 */
Qiang Xue committed
330
	protected function buildTarget($target, $type, $bundles, $timestamp)
Qiang Xue committed
331
	{
Qiang Xue committed
332
		$outputFile = strtr($target->$type, array(
Qiang Xue committed
333 334 335
			'{ts}' => $timestamp,
		));
		$inputFiles = array();
Qiang Xue committed
336 337

		foreach ($target->depends as $name) {
Qiang Xue committed
338 339
			if (isset($bundles[$name])) {
				foreach ($bundles[$name]->$type as $file) {
340
					$inputFiles[] = $bundles[$name]->basePath . '/' . $file;
Qiang Xue committed
341 342
				}
			} else {
343
				throw new Exception("Unknown bundle: '{$name}'");
Qiang Xue committed
344 345 346
			}
		}
		if ($type === 'js') {
Qiang Xue committed
347
			$this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile);
Qiang Xue committed
348
		} else {
Qiang Xue committed
349
			$this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile);
Qiang Xue committed
350
		}
Qiang Xue committed
351
		$target->$type = array($outputFile);
Qiang Xue committed
352 353
	}

354
	/**
355 356 357 358
	 * Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones.
	 * @param \yii\web\AssetBundle[] $targets output asset bundles.
	 * @param \yii\web\AssetBundle[] $bundles source asset bundles.
	 * @return \yii\web\AssetBundle[] output asset bundles.
359
	 */
Qiang Xue committed
360 361
	protected function adjustDependency($targets, $bundles)
	{
362 363
		echo "Creating new bundle configuration...\n";

Qiang Xue committed
364 365 366
		$map = array();
		foreach ($targets as $name => $target) {
			foreach ($target->depends as $bundle) {
367
				$map[$bundle] = $name;
Qiang Xue committed
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
			}
		}

		foreach ($targets as $name => $target) {
			$depends = array();
			foreach ($target->depends as $bn) {
				foreach ($bundles[$bn]->depends as $bundle) {
					$depends[$map[$bundle]] = true;
				}
			}
			unset($depends[$name]);
			$target->depends = array_keys($depends);
		}

		// detect possible circular dependencies
		foreach ($targets as $name => $target) {
			$registered = array();
			$this->registerBundle($targets, $name, $registered);
		}

		foreach ($map as $bundle => $target) {
			$targets[$bundle] = Yii::createObject(array(
				'class' => 'yii\\web\\AssetBundle',
				'depends' => array($target),
			));
		}
Qiang Xue committed
394 395 396
		return $targets;
	}

397 398 399 400 401 402 403
	/**
	 * Registers asset bundles including their dependencies.
	 * @param \yii\web\AssetBundle[] $bundles asset bundles list.
	 * @param string $name bundle name.
	 * @param array $registered stores already registered names.
	 * @throws \yii\console\Exception if circular dependency is detected.
	 */
Qiang Xue committed
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
	protected function registerBundle($bundles, $name, &$registered)
	{
		if (!isset($registered[$name])) {
			$registered[$name] = false;
			$bundle = $bundles[$name];
			foreach ($bundle->depends as $depend) {
				$this->registerBundle($bundles, $depend, $registered);
			}
			unset($registered[$name]);
			$registered[$name] = true;
		} elseif ($registered[$name] === false) {
			throw new Exception("A circular dependency is detected for target '$name'.");
		}
	}

419 420 421 422
	/**
	 * Saves new asset bundles configuration.
	 * @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved.
	 * @param string $bundleFile output file name.
423
	 * @throws \yii\console\Exception on failure.
424
	 */
Qiang Xue committed
425 426 427 428 429 430 431 432 433 434 435 436
	protected function saveTargets($targets, $bundleFile)
	{
		$array = array();
		foreach ($targets as $name => $target) {
			foreach (array('js', 'css', 'depends', 'basePath', 'baseUrl') as $prop) {
				if (!empty($target->$prop)) {
					$array[$name][$prop] = $target->$prop;
				}
			}
		}
		$array = var_export($array, true);
		$version = date('Y-m-d H:i:s', time());
437
		$bundleFileContent = <<<EOD
Qiang Xue committed
438 439
<?php
/**
440
 * This file is generated by the "yii {$this->id}" command.
441
 * DO NOT MODIFY THIS FILE DIRECTLY.
442
 * @version {$version}
Qiang Xue committed
443
 */
444
return {$array};
445 446
EOD;
		if (!file_put_contents($bundleFile, $bundleFileContent)) {
447 448 449
			throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'.");
		}
		echo "Output bundle configuration created at '{$bundleFile}'.\n";
Qiang Xue committed
450 451
	}

452
	/**
453
	 * Compresses given JavaScript files and combines them into the single one.
454 455
	 * @param array $inputFiles list of source file names.
	 * @param string $outputFile output file name.
456
	 * @throws \yii\console\Exception on failure
457
	 */
Qiang Xue committed
458 459
	protected function compressJsFiles($inputFiles, $outputFile)
	{
460 461 462 463
		if (empty($inputFiles)) {
			return;
		}
		echo "  Compressing JavaScript files...\n";
464 465 466
		if (is_string($this->jsCompressor)) {
			$tmpFile = $outputFile . '.tmp';
			$this->combineJsFiles($inputFiles, $tmpFile);
467
			echo shell_exec(strtr($this->jsCompressor, array(
468 469
				'{from}' => escapeshellarg($tmpFile),
				'{to}' => escapeshellarg($outputFile),
470 471 472
			)));
			@unlink($tmpFile);
		} else {
473
			call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile);
474
		}
475 476 477 478
		if (!file_exists($outputFile)) {
			throw new Exception("Unable to compress JavaScript files into '{$outputFile}'.");
		}
		echo "  JavaScript files compressed into '{$outputFile}'.\n";
Qiang Xue committed
479 480
	}

481 482 483 484
	/**
	 * Compresses given CSS files and combines them into the single one.
	 * @param array $inputFiles list of source file names.
	 * @param string $outputFile output file name.
485
	 * @throws \yii\console\Exception on failure
486
	 */
Qiang Xue committed
487 488
	protected function compressCssFiles($inputFiles, $outputFile)
	{
489 490 491 492
		if (empty($inputFiles)) {
			return;
		}
		echo "  Compressing CSS files...\n";
493 494 495
		if (is_string($this->cssCompressor)) {
			$tmpFile = $outputFile . '.tmp';
			$this->combineCssFiles($inputFiles, $tmpFile);
496
			echo shell_exec(strtr($this->cssCompressor, array(
497 498
				'{from}' => escapeshellarg($tmpFile),
				'{to}' => escapeshellarg($outputFile),
499
			)));
500
			@unlink($tmpFile);
501
		} else {
502
			call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile);
503
		}
504 505 506 507
		if (!file_exists($outputFile)) {
			throw new Exception("Unable to compress CSS files into '{$outputFile}'.");
		}
		echo "  CSS files compressed into '{$outputFile}'.\n";
508 509
	}

510
	/**
511
	 * Combines JavaScript files into a single one.
512 513
	 * @param array $inputFiles source file names.
	 * @param string $outputFile output file name.
514
	 * @throws \yii\console\Exception on failure.
515 516
	 */
	public function combineJsFiles($inputFiles, $outputFile)
517 518
	{
		$content = '';
519
		foreach ($inputFiles as $file) {
520 521 522 523
			$content .= "/*** BEGIN FILE: $file ***/\n"
				. file_get_contents($file)
				. "/*** END FILE: $file ***/\n";
		}
524
		if (!file_put_contents($outputFile, $content)) {
525
			throw new Exception("Unable to write output JavaScript file '{$outputFile}'.");
526
		}
527 528
	}

529 530 531 532
	/**
	 * Combines CSS files into a single one.
	 * @param array $inputFiles source file names.
	 * @param string $outputFile output file name.
533
	 * @throws \yii\console\Exception on failure.
534 535
	 */
	public function combineCssFiles($inputFiles, $outputFile)
536 537
	{
		$content = '';
538
		foreach ($inputFiles as $file) {
539
			$content .= "/*** BEGIN FILE: $file ***/\n"
540
				. $this->adjustCssUrl(file_get_contents($file), dirname($file), dirname($outputFile))
541 542
				. "/*** END FILE: $file ***/\n";
		}
543 544 545
		if (!file_put_contents($outputFile, $content)) {
			throw new Exception("Unable to write output CSS file '{$outputFile}'.");
		}
546 547
	}

548 549 550 551 552 553 554 555 556
	/**
	 * Adjusts CSS content allowing URL references pointing to the original resources.
	 * @param string $cssContent source CSS content.
	 * @param string $inputFilePath input CSS file name.
	 * @param string $outputFilePath output CSS file name.
	 * @return string adjusted CSS content.
	 */
	protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath)
	{
557 558 559 560 561 562 563 564 565
		$sharedPathParts = array();
		$inputFilePathParts = explode('/', $inputFilePath);
		$inputFilePathPartsCount = count($inputFilePathParts);
		$outputFilePathParts = explode('/', $outputFilePath);
		$outputFilePathPartsCount = count($outputFilePathParts);
		for ($i =0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) {
			if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) {
				$sharedPathParts[] = $inputFilePathParts[$i];
			} else {
566 567 568
				break;
			}
		}
569 570
		$sharedPath = implode('/', $sharedPathParts);

571 572 573 574 575
		$inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/');
		$outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/');
		$inputFileRelativePathParts = explode('/', $inputFileRelativePath);
		$outputFileRelativePathParts = explode('/', $outputFileRelativePath);

resurtm committed
576
		$callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) {
577 578 579
			$fullMatch = $matches[0];
			$inputUrl = $matches[1];

580 581 582 583
			if (preg_match('/https?:\/\//is', $inputUrl)) {
				return $fullMatch;
			}

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
			$outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..');
			$outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts);

			if (strpos($inputUrl, '/') !== false) {
				$inputUrlParts = explode('/', $inputUrl);
				foreach ($inputUrlParts as $key => $inputUrlPart) {
					if ($inputUrlPart == '..') {
						array_pop($outputUrlParts);
						unset($inputUrlParts[$key]);
					}
				}
				$outputUrlParts[] = implode('/', $inputUrlParts);
			} else {
				$outputUrlParts[] = $inputUrl;
			}
			$outputUrl = implode('/', $outputUrlParts);
600 601

			return str_replace($inputUrl, $outputUrl, $fullMatch);
602
		};
603

604
		$cssContent = preg_replace_callback('/url\(["\']?([^"]*)["\']?\)/is', $callback, $cssContent);
605 606 607 608

		return $cssContent;
	}

609 610 611
	/**
	 * Creates template of configuration file for [[actionCompress]].
	 * @param string $configFile output file name.
612
	 * @throws \yii\console\Exception on failure.
613
	 */
614 615 616 617
	public function actionTemplate($configFile)
	{
		$template = <<<EOD
<?php
618 619
/**
 * Configuration file for the "yii asset" console command.
620
 * Note: in the console environment some path aliases like '@wwwroot' and '@www' may not exist,
621 622
 * so corresponding paths should be specified directly.
 */
623
return array(
624
	// The list of asset bundles to compress:
625
	'bundles' => require('path/to/bundles.php'),
626
	// The list of extensions to compress:
627
	'extensions' => require('path/to/namespaces.php'),
628
	// Asset bundle for compression output:
629 630 631 632 633 634 635 636
	'targets' => array(
		'all' => array(
			'basePath' => __DIR__,
			'baseUrl' => '/test',
			'js' => 'all-{ts}.js',
			'css' => 'all-{ts}.css',
		),
	),
637
	// Asset manager configuration:
638 639 640 641 642 643
	'assetManager' => array(
		'basePath' => __DIR__,
		'baseUrl' => '/test',
	),
);
EOD;
644 645 646 647 648
		if (file_exists($configFile)) {
			if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) {
				return;
			}
		}
649 650
		if (!file_put_contents($configFile, $template)) {
			throw new Exception("Unable to write template file '{$configFile}'.");
651 652 653
		} else {
			echo "Configuration file template created at '{$configFile}'.\n\n";
		}
Qiang Xue committed
654
	}
Zander Baldwin committed
655
}