PageRenderTime 55ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/View.php

https://gitlab.com/NecroMan/yii2-minify
PHP | 473 lines | 295 code | 89 blank | 89 comment | 49 complexity | 2cf8388038b768cb59f898a240136fcb MD5 | raw file
  1. <?php
  2. namespace milano\minify;
  3. use yii\base\Exception;
  4. use yii\helpers;
  5. /**
  6. * Class View
  7. * @package rmrevin\yii\minify
  8. */
  9. class View extends \yii\web\View
  10. {
  11. /** @var bool */
  12. public $enableMinify = true;
  13. /** @var bool */
  14. public $minifyCss = true;
  15. /** @var bool */
  16. public $compressCss = true;
  17. /** @var bool */
  18. public $minifyJs = true;
  19. /** @var bool */
  20. public $compressJs = true;
  21. /** @var string path alias to web base (in url) */
  22. public $webPath = '@web';
  23. /** @var string path alias to web base (absolute) */
  24. public $basePath = '@webroot';
  25. /** @var string path alias to save minify result */
  26. public $minifyPath = '@webroot/minify';
  27. /** @var array positions of js files to be minified */
  28. public $jsPosition = [self::POS_END, self::POS_HEAD];
  29. /** @var bool|string charset forcibly assign, otherwise will use all of the files found charset */
  30. public $forceCharset = false;
  31. /** @var bool whether to change @import on content */
  32. public $expandImports = true;
  33. /** @var int */
  34. public $cssLinebreakPos = 2048;
  35. /** @var int|bool chmod of minified file. If false chmod not set */
  36. public $fileMode = 0664;
  37. /** @var array schemes that will be ignored during normalization url */
  38. public $schemas = ['//', 'http://', 'https://', 'ftp://'];
  39. /** @var bool do I need to compress the result html page. */
  40. public $compressOutput = false;
  41. /** @var string Method of file updated checking: 'sha' - with sha1_file (slower, better for debug with assets force
  42. * copy) or 'time' - with filemtime (faster, better for production) */
  43. public $hashMethod = 'sha';
  44. /** @var string Regex for ignoring CSS and JS files like TinyMCE and ElFinder */
  45. public $ignore = '';
  46. /**
  47. * @throws Exception
  48. */
  49. public function init()
  50. {
  51. parent::init();
  52. $minify_path = $this->minifyPath = (string)\Yii::getAlias($this->minifyPath);
  53. if (!file_exists($minify_path)) {
  54. helpers\FileHelper::createDirectory($minify_path);
  55. }
  56. if (!is_readable($minify_path)) {
  57. throw new Exception('Directory for compressed assets is not readable.');
  58. }
  59. if (!is_writable($minify_path)) {
  60. throw new Exception('Directory for compressed assets is not writable.');
  61. }
  62. if (true === $this->compressOutput) {
  63. \Yii::$app->response->on(\yii\web\Response::EVENT_BEFORE_SEND, function (\yii\base\Event $Event) {
  64. /** @var \yii\web\Response $Response */
  65. $Response = $Event->sender;
  66. if ($Response->format === \yii\web\Response::FORMAT_HTML) {
  67. if (!empty($Response->data)) {
  68. $Response->data = HtmlCompressor::compress($Response->data, ['extra' => true]);
  69. }
  70. if (!empty($Response->content)) {
  71. $Response->content = HtmlCompressor::compress($Response->content, ['extra' => true]);
  72. }
  73. }
  74. });
  75. }
  76. }
  77. /**
  78. * @inheritdoc
  79. */
  80. public function endPage($ajaxMode = false)
  81. {
  82. $this->trigger(self::EVENT_END_PAGE);
  83. $content = ob_get_clean();
  84. foreach (array_keys($this->assetBundles) as $bundle) {
  85. $this->registerAssetFiles($bundle);
  86. }
  87. if (true === $this->enableMinify) {
  88. if (true === $this->minifyCss) {
  89. $this->minifyCSS();
  90. }
  91. if (true === $this->minifyJs) {
  92. $this->minifyJS();
  93. }
  94. }
  95. echo strtr(
  96. $content,
  97. [
  98. self::PH_HEAD => $this->renderHeadHtml(),
  99. self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
  100. self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
  101. ]
  102. );
  103. $this->clear();
  104. }
  105. /**
  106. * @return self
  107. */
  108. protected function minifyCSS()
  109. {
  110. if (!empty($this->cssFiles)) {
  111. $cssFiles = $this->cssFiles;
  112. $this->cssFiles = [];
  113. $toMinify = [];
  114. foreach ($cssFiles as $file => $html) {
  115. if ($this->thisFileNeedMinify($file, $html)) {
  116. $toMinify[$file] = $html;
  117. } else {
  118. if (!empty($toMinify)) {
  119. $this->processMinifyCss($toMinify);
  120. $toMinify = [];
  121. }
  122. $this->cssFiles[$file] = $html;
  123. }
  124. }
  125. if (!empty($toMinify)) {
  126. $this->processMinifyCss($toMinify);
  127. }
  128. unset($toMinify);
  129. }
  130. return $this;
  131. }
  132. /**
  133. * @param array $files
  134. */
  135. protected function processMinifyCss($files)
  136. {
  137. $resultFile = $this->minifyPath . '/' . $this->_getSummaryFilesHash($files) . '.css';
  138. if (!file_exists($resultFile)) {
  139. $css = '';
  140. foreach ($files as $file => $html) {
  141. $file = str_replace(\Yii::getAlias($this->webPath), '', $file);
  142. $content = file_get_contents(\Yii::getAlias($this->basePath) . $file);
  143. preg_match_all('|url\(([^)]+)\)|is', $content, $m);
  144. if (!empty($m[0])) {
  145. $path = dirname($file);
  146. $result = [];
  147. foreach ($m[0] as $k => $v) {
  148. if (in_array(strpos($m[1][$k], 'data:'), [0, 1], true)) {
  149. continue;
  150. }
  151. $url = str_replace(['\'', '"'], '', $m[1][$k]);
  152. if ($this->isUrl($url)) {
  153. $result[$m[1][$k]] = '\'' . $url . '\'';
  154. } else {
  155. $result[$m[1][$k]] = '\'' . \Yii::getAlias($this->webPath) . $path . '/' . $url . '\'';
  156. }
  157. }
  158. $content = str_replace(array_keys($result), array_values($result), $content);
  159. }
  160. $css .= $content;
  161. }
  162. $this->expandImports($css);
  163. if ($this->compressCss) {
  164. $css = (new \CSSmin())
  165. ->run($css, $this->cssLinebreakPos);
  166. }
  167. if (false !== $this->forceCharset) {
  168. $charsets = '@charset "' . (string)$this->forceCharset . '";' . "\n";
  169. } else {
  170. $charsets = $this->collectCharsets($css);
  171. }
  172. $imports = $this->collectImports($css);
  173. $fonts = $this->collectFonts($css);
  174. file_put_contents($resultFile, $charsets . $imports . $fonts . $css);
  175. if (false !== $this->fileMode) {
  176. @chmod($resultFile, $this->fileMode);
  177. }
  178. }
  179. $file = sprintf('%s%s', \Yii::getAlias($this->webPath), str_replace(\Yii::getAlias($this->basePath), '', $resultFile));
  180. $this->cssFiles[$file] = helpers\Html::cssFile($file);
  181. }
  182. /**
  183. * @return self
  184. */
  185. protected function minifyJS()
  186. {
  187. if (!empty($this->jsFiles)) {
  188. $jsFiles = $this->jsFiles;
  189. foreach ($jsFiles as $position => $files) {
  190. if (false === in_array($position, $this->jsPosition, true)) {
  191. $this->jsFiles[$position] = [];
  192. foreach ($files as $file => $html) {
  193. $this->jsFiles[$position][$file] = $html;
  194. }
  195. } else {
  196. $this->jsFiles[$position] = [];
  197. $toMinify = [];
  198. foreach ($files as $file => $html) {
  199. if ($this->thisFileNeedMinify($file, $html)) {
  200. $toMinify[$file] = $html;
  201. } else {
  202. if (!empty($toMinify)) {
  203. $this->processMinifyJs($position, $toMinify);
  204. $toMinify = [];
  205. }
  206. $this->jsFiles[$position][$file] = $html;
  207. }
  208. }
  209. if (!empty($toMinify)) {
  210. $this->processMinifyJs($position, $toMinify);
  211. }
  212. unset($toMinify);
  213. }
  214. }
  215. }
  216. return $this;
  217. }
  218. /**
  219. * @param integer $position
  220. * @param array $files
  221. */
  222. protected function processMinifyJs($position, $files)
  223. {
  224. $resultFile = sprintf('%s/%s.js', $this->minifyPath, $this->_getSummaryFilesHash($files));
  225. if (!file_exists($resultFile)) {
  226. $js = '';
  227. foreach ($files as $file => $html) {
  228. $file = \Yii::getAlias($this->basePath) . str_replace(\Yii::getAlias($this->webPath), '', $file);
  229. $js .= file_get_contents($file) . ';' . PHP_EOL;
  230. }
  231. if ($this->compressJs) {
  232. $js = (new \JSMin($js))
  233. ->min();
  234. }
  235. file_put_contents($resultFile, $js);
  236. if (false !== $this->fileMode) {
  237. @chmod($resultFile, $this->fileMode);
  238. }
  239. }
  240. $file = sprintf('%s%s', \Yii::getAlias($this->webPath), str_replace(\Yii::getAlias($this->basePath), '', $resultFile));
  241. $this->jsFiles[$position][$file] = helpers\Html::jsFile($file);
  242. }
  243. /**
  244. * @param string $url
  245. * @param boolean $checkSlash
  246. * @return bool
  247. */
  248. protected function isUrl($url, $checkSlash = true)
  249. {
  250. $regexp = '#^(' . implode('|', $this->schemas) . ')#is';
  251. if ($checkSlash) {
  252. $regexp = '#^(/|\\\\|' . implode('|', $this->schemas) . ')#is';
  253. }
  254. return (bool)preg_match($regexp, $url);
  255. }
  256. /**
  257. * @param string $string
  258. * @return bool
  259. */
  260. protected function isContainsConditionalComment($string)
  261. {
  262. return strpos($string, '<![endif]-->') !== false;
  263. }
  264. /**
  265. * @param string $text
  266. * @return int
  267. */
  268. protected function isIgnored($text) {
  269. return $text && preg_match('#' . $this->ignore . '#i', $text);
  270. }
  271. /**
  272. * @param string $file
  273. * @param string $html
  274. * @return bool
  275. */
  276. protected function thisFileNeedMinify($file, $html)
  277. {
  278. return !$this->isUrl($file, false) && !$this->isContainsConditionalComment($html) && !$this->isIgnored($file);
  279. }
  280. /**
  281. * @param string $css
  282. * @return string
  283. */
  284. protected function collectCharsets(&$css)
  285. {
  286. return $this->_collect($css, '|\@charset[^;]+|is', function ($string) {
  287. return $string . ';';
  288. });
  289. }
  290. /**
  291. * @param string $css
  292. * @return string
  293. */
  294. protected function collectImports(&$css)
  295. {
  296. return $this->_collect($css, '|\@import[^;]+|is', function ($string) {
  297. return $string . ';';
  298. });
  299. }
  300. /**
  301. * @param string $css
  302. * @return string
  303. */
  304. protected function collectFonts(&$css)
  305. {
  306. return $this->_collect($css, '|\@font-face\{[^}]+\}|is', function ($string) {
  307. return $string;
  308. });
  309. }
  310. /**
  311. * @param string $css
  312. */
  313. protected function expandImports(&$css)
  314. {
  315. if (true === $this->expandImports) {
  316. preg_match_all('|\@import\s([^;]+);|is', str_replace('&amp;', '&', $css), $m);
  317. if (!empty($m[0])) {
  318. foreach ($m[0] as $k => $v) {
  319. $import_url = $m[1][$k];
  320. if (!empty($import_url)) {
  321. $import_content = $this->_getImportContent($import_url);
  322. if (!empty($import_content)) {
  323. $css = str_replace($m[0][$k], $import_content, $css);
  324. }
  325. }
  326. }
  327. }
  328. }
  329. }
  330. /**
  331. * @param string $url
  332. * @return null|string
  333. */
  334. protected function _getImportContent($url)
  335. {
  336. $result = null;
  337. if ('url(' === helpers\StringHelper::byteSubstr($url, 0, 4)) {
  338. $url = str_replace(['url(\'', 'url("', 'url(', '\')', '")', ')'], '', $url);
  339. if (helpers\StringHelper::byteSubstr($url, 0, 2) === '//') {
  340. $url = preg_replace('|^//|', 'http://', $url, 1);
  341. }
  342. if (!empty($url)) {
  343. $result = file_get_contents($url);
  344. }
  345. }
  346. return $result;
  347. }
  348. /**
  349. * @param string $css
  350. * @param string $pattern
  351. * @param callable $handler
  352. * @return string
  353. */
  354. protected function _collect(&$css, $pattern, $handler)
  355. {
  356. $result = '';
  357. preg_match_all($pattern, $css, $m);
  358. foreach ($m[0] as $string) {
  359. $string = $handler($string);
  360. $css = str_replace($string, '', $css);
  361. $result .= $string . PHP_EOL;
  362. }
  363. return $result;
  364. }
  365. /**
  366. * @param array $files
  367. * @return string
  368. */
  369. protected function _getSummaryFilesHash($files)
  370. {
  371. $result = '';
  372. foreach ($files as $file => $html) {
  373. $path = \Yii::getAlias($this->basePath) . $file;
  374. if ($this->thisFileNeedMinify($file, $html) && file_exists($path)) {
  375. if ($this->hashMethod === 'time') {
  376. $result .= $path . '?' . filemtime($path);
  377. } else {
  378. $result .= sha1_file($path);
  379. }
  380. }
  381. }
  382. return sha1($result);
  383. }
  384. }