diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index d953889..90c1b60 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -22,6 +22,7 @@ Yii Framework 2 Change Log - Enh #13702: Added support for PSR-3 'Logger' (klimov-paul) - Enh #13706: 'Profiler' layer extracted (klimov-paul) - Enh #15410: Added serialization abstraction layer under `yii\serialize\*` namespace (klimov-paul) +- Enh #608: Added `yii\web\AssetConverter::$isOutdatedCallback` allowing custom check for outdated asset conversion result (klimov-paul) - Enh: Objects `yii\helpers\ReplaceArrayValue`, `yii\helpers\UnsetArrayValue` now support restoring after being exported with `var_export()` function (silverfire) - Chg: Removed methods marked as deprecated in 2.0.x (samdark) - Chg #8452: Packages 'captcha', 'jquery', 'rest', 'mssql' and 'oracle' have been extracted into extensions (klimov-paul) diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php index bcc50e7..5057f21 100644 --- a/framework/web/AssetConverter.php +++ b/framework/web/AssetConverter.php @@ -49,6 +49,42 @@ class AssetConverter extends Component implements AssetConverterInterface * significantly degrade the performance. */ public $forceConvert = false; + /** + * @var callable a PHP callback, which should be invoked to check whether asset conversion result is outdated. + * It will be invoked only if conversion target file exists and its modification time is older then the one of source file. + * Callback should match following signature: + * + * ```php + * function (string $basePath, string $sourceFile, string $targetFile, string $sourceExtension, string $targetExtension) : bool + * ``` + * + * where $basePath is the asset source directory; $sourceFile is the asset source file path, relative to $basePath; + * $targetFile is the asset target file path, relative to $basePath; $sourceExtension is the source asset file extension + * and $targetExtension is the target asset file extension, respectively. + * + * It should return `true` is case asset should be reconverted. + * For example: + * + * ```php + * function ($basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension) { + * if (YII_ENV !== 'dev') { + * return false; + * } + * + * $resultModificationTime = @filemtime("$basePath/$result"); + * foreach (FileHelper::findFiles($basePath, ['only' => ["*.{$sourceExtension}"]]) as $filename) { + * if ($resultModificationTime < @filemtime($filename)) { + * return true; + * } + * } + * + * return false; + * } + * ``` + * + * @since 2.1.0 + */ + public $isOutdatedCallback; /** @@ -61,11 +97,11 @@ class AssetConverter extends Component implements AssetConverterInterface { $pos = strrpos($asset, '.'); if ($pos !== false) { - $ext = substr($asset, $pos + 1); - if (isset($this->commands[$ext])) { - [$ext, $command] = $this->commands[$ext]; + $srcExt = substr($asset, $pos + 1); + if (isset($this->commands[$srcExt])) { + [$ext, $command] = $this->commands[$srcExt]; $result = substr($asset, 0, $pos + 1) . $ext; - if ($this->forceConvert || @filemtime("$basePath/$result") < @filemtime("$basePath/$asset")) { + if ($this->forceConvert || $this->isOutdated($basePath, $asset, $result, $srcExt, $ext)) { $this->runCommand($command, $basePath, $asset, $result); } @@ -77,6 +113,34 @@ class AssetConverter extends Component implements AssetConverterInterface } /** + * Checks whether asset convert result is outdated, and thus should be reconverted. + * @param string $basePath the directory the $asset is relative to. + * @param string $sourceFile the asset source file path, relative to [[$basePath]]. + * @param string $targetFile the converted asset file path, relative to [[$basePath]]. + * @param string $sourceExtension source asset file extension. + * @param string $targetExtension target asset file extension. + * @return bool whether asset is outdated or not. + * @since 2.1.0 + */ + protected function isOutdated($basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension) + { + $resultModificationTime = @filemtime("$basePath/$targetFile"); + if ($resultModificationTime === false || $resultModificationTime === null) { + return true; + } + + if ($resultModificationTime < @filemtime("$basePath/$sourceFile")) { + return true; + } + + if ($this->isOutdatedCallback === null) { + return false; + } + + return call_user_func($this->isOutdatedCallback, $basePath, $sourceFile, $targetFile, $sourceExtension, $targetExtension); + } + + /** * Runs a command to convert asset files. * @param string $command the command to run. If prefixed with an `@` it will be treated as a [path alias](guide:concept-aliases). * @param string $basePath asset base path and command working directory diff --git a/tests/framework/web/AssetConverterTest.php b/tests/framework/web/AssetConverterTest.php index 49bcd3e..5e9b509 100644 --- a/tests/framework/web/AssetConverterTest.php +++ b/tests/framework/web/AssetConverterTest.php @@ -20,6 +20,9 @@ class AssetConverterTest extends \yiiunit\TestCase */ protected $tmpPath; + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); @@ -30,6 +33,9 @@ class AssetConverterTest extends \yiiunit\TestCase } } + /** + * {@inheritdoc} + */ protected function tearDown() { if (is_dir($this->tmpPath)) { @@ -62,6 +68,35 @@ EOF /** * @depends testConvert */ + public function testConvertOutdated() + { + $tmpPath = $this->tmpPath; + $srcFilename = $tmpPath . '/test.php'; + file_put_contents($srcFilename, <<<'EOF' +commands['php'] = ['txt', 'php {from} > {to}']; + + $converter->convert('test.php', $tmpPath); + $initialConvertTime = file_get_contents($tmpPath . '/test.txt'); + + usleep(1); + $converter->convert('test.php', $tmpPath); + $this->assertStringEqualsFile($tmpPath . '/test.txt', $initialConvertTime); + + touch($srcFilename, time() + 1000); + $converter->convert('test.php', $tmpPath); + $this->assertNotEquals($initialConvertTime, file_get_contents($tmpPath . '/test.txt')); + } + + /** + * @depends testConvertOutdated + */ public function testForceConvert() { $tmpPath = $this->tmpPath; @@ -86,4 +121,37 @@ EOF $converter->convert('test.php', $tmpPath); $this->assertNotEquals($initialConvertTime, file_get_contents($tmpPath . '/test.txt')); } + + /** + * @depends testConvertOutdated + */ + public function testCheckOutdatedCallback() + { + $tmpPath = $this->tmpPath; + $srcFilename = $tmpPath . '/test.php'; + file_put_contents($srcFilename, <<<'EOF' +commands['php'] = ['txt', 'php {from} > {to}']; + + $converter->convert('test.php', $tmpPath); + $initialConvertTime = file_get_contents($tmpPath . '/test.txt'); + + $converter->isOutdatedCallback = function() { + return false; + }; + $converter->convert('test.php', $tmpPath); + $this->assertStringEqualsFile($tmpPath . '/test.txt', $initialConvertTime); + + $converter->isOutdatedCallback = function() { + return true; + }; + $converter->convert('test.php', $tmpPath); + $this->assertNotEquals($initialConvertTime, file_get_contents($tmpPath . '/test.txt')); + } }