ClosureCompiler.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. <?php
  2. /**
  3. * Class Minify_JS_ClosureCompiler
  4. * @package Minify
  5. */
  6. /**
  7. * Minify Javascript using Google's Closure Compiler API
  8. *
  9. * @link http://code.google.com/closure/compiler/
  10. * @package Minify
  11. * @author Stephen Clay <steve@mrclay.org>
  12. *
  13. * @todo can use a stream wrapper to unit test this?
  14. */
  15. class Minify_JS_ClosureCompiler {
  16. /**
  17. * @var string The option key for the maximum POST byte size
  18. */
  19. const OPTION_MAX_BYTES = 'maxBytes';
  20. /**
  21. * @var string The option key for additional params. @see __construct
  22. */
  23. const OPTION_ADDITIONAL_OPTIONS = 'additionalParams';
  24. /**
  25. * @var string The option key for the fallback Minifier
  26. */
  27. const OPTION_FALLBACK_FUNCTION = 'fallbackFunc';
  28. /**
  29. * @var string The option key for the service URL
  30. */
  31. const OPTION_COMPILER_URL = 'compilerUrl';
  32. /**
  33. * @var int The default maximum POST byte size according to https://developers.google.com/closure/compiler/docs/api-ref
  34. */
  35. const DEFAULT_MAX_BYTES = 200000;
  36. /**
  37. * @var string[] $DEFAULT_OPTIONS The default options to pass to the compiler service
  38. *
  39. * @note This would be a constant if PHP allowed it
  40. */
  41. private static $DEFAULT_OPTIONS = array(
  42. 'output_format' => 'text',
  43. 'compilation_level' => 'SIMPLE_OPTIMIZATIONS'
  44. );
  45. /**
  46. * @var string $url URL of compiler server. defaults to Google's
  47. */
  48. protected $serviceUrl = 'http://closure-compiler.appspot.com/compile';
  49. /**
  50. * @var int $maxBytes The maximum JS size that can be sent to the compiler server in bytes
  51. */
  52. protected $maxBytes = self::DEFAULT_MAX_BYTES;
  53. /**
  54. * @var string[] $additionalOptions Additional options to pass to the compiler service
  55. */
  56. protected $additionalOptions = array();
  57. /**
  58. * @var callable Function to minify JS if service fails. Default is JSMin
  59. */
  60. protected $fallbackMinifier = array('JSMin', 'minify');
  61. /**
  62. * Minify JavaScript code via HTTP request to a Closure Compiler API
  63. *
  64. * @param string $js input code
  65. * @param array $options Options passed to __construct(). @see __construct
  66. *
  67. * @return string
  68. */
  69. public static function minify($js, array $options = array())
  70. {
  71. $obj = new self($options);
  72. return $obj->min($js);
  73. }
  74. /**
  75. * @param array $options Options with keys available below:
  76. *
  77. * fallbackFunc : (callable) function to minify if service unavailable. Default is JSMin.
  78. *
  79. * compilerUrl : (string) URL to closure compiler server
  80. *
  81. * maxBytes : (int) The maximum amount of bytes to be sent as js_code in the POST request.
  82. * Defaults to 200000.
  83. *
  84. * additionalParams : (string[]) Additional parameters to pass to the compiler server. Can be anything named
  85. * in https://developers.google.com/closure/compiler/docs/api-ref except for js_code and
  86. * output_info
  87. */
  88. public function __construct(array $options = array())
  89. {
  90. if (isset($options[self::OPTION_FALLBACK_FUNCTION])) {
  91. $this->fallbackMinifier = $options[self::OPTION_FALLBACK_FUNCTION];
  92. }
  93. if (isset($options[self::OPTION_COMPILER_URL])) {
  94. $this->serviceUrl = $options[self::OPTION_COMPILER_URL];
  95. }
  96. if (isset($options[self::OPTION_ADDITIONAL_OPTIONS]) && is_array($options[self::OPTION_ADDITIONAL_OPTIONS])) {
  97. $this->additionalOptions = $options[self::OPTION_ADDITIONAL_OPTIONS];
  98. }
  99. if (isset($options[self::OPTION_MAX_BYTES])) {
  100. $this->maxBytes = (int) $options[self::OPTION_MAX_BYTES];
  101. }
  102. }
  103. /**
  104. * Call the service to perform the minification
  105. *
  106. * @param string $js JavaScript code
  107. * @return string
  108. * @throws Minify_JS_ClosureCompiler_Exception
  109. */
  110. public function min($js)
  111. {
  112. $postBody = $this->buildPostBody($js);
  113. if ($this->maxBytes > 0) {
  114. $bytes = (function_exists('mb_strlen') && ((int)ini_get('mbstring.func_overload') & 2))
  115. ? mb_strlen($postBody, '8bit')
  116. : strlen($postBody);
  117. if ($bytes > $this->maxBytes) {
  118. throw new Minify_JS_ClosureCompiler_Exception(
  119. 'POST content larger than ' . $this->maxBytes . ' bytes'
  120. );
  121. }
  122. }
  123. $response = $this->getResponse($postBody);
  124. if (preg_match('/^Error\(\d\d?\):/', $response)) {
  125. if (is_callable($this->fallbackMinifier)) {
  126. // use fallback
  127. $response = "/* Received errors from Closure Compiler API:\n$response"
  128. . "\n(Using fallback minifier)\n*/\n";
  129. $response .= call_user_func($this->fallbackMinifier, $js);
  130. } else {
  131. throw new Minify_JS_ClosureCompiler_Exception($response);
  132. }
  133. }
  134. if ($response === '') {
  135. $errors = $this->getResponse($this->buildPostBody($js, true));
  136. throw new Minify_JS_ClosureCompiler_Exception($errors);
  137. }
  138. return $response;
  139. }
  140. /**
  141. * Get the response for a given POST body
  142. *
  143. * @param string $postBody
  144. * @return string
  145. * @throws Minify_JS_ClosureCompiler_Exception
  146. */
  147. protected function getResponse($postBody)
  148. {
  149. $allowUrlFopen = preg_match('/1|yes|on|true/i', ini_get('allow_url_fopen'));
  150. if ($allowUrlFopen) {
  151. $contents = file_get_contents($this->serviceUrl, false, stream_context_create(array(
  152. 'http' => array(
  153. 'method' => 'POST',
  154. 'header' => "Content-type: application/x-www-form-urlencoded\r\nConnection: close\r\n",
  155. 'content' => $postBody,
  156. 'max_redirects' => 0,
  157. 'timeout' => 15,
  158. )
  159. )));
  160. } elseif (defined('CURLOPT_POST')) {
  161. $ch = curl_init($this->serviceUrl);
  162. curl_setopt($ch, CURLOPT_POST, true);
  163. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  164. curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-type: application/x-www-form-urlencoded'));
  165. curl_setopt($ch, CURLOPT_POSTFIELDS, $postBody);
  166. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  167. curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
  168. $contents = curl_exec($ch);
  169. curl_close($ch);
  170. } else {
  171. throw new Minify_JS_ClosureCompiler_Exception(
  172. "Could not make HTTP request: allow_url_open is false and cURL not available"
  173. );
  174. }
  175. if (false === $contents) {
  176. throw new Minify_JS_ClosureCompiler_Exception(
  177. "No HTTP response from server"
  178. );
  179. }
  180. return trim($contents);
  181. }
  182. /**
  183. * Build a POST request body
  184. *
  185. * @param string $js JavaScript code
  186. * @param bool $returnErrors
  187. * @return string
  188. */
  189. protected function buildPostBody($js, $returnErrors = false)
  190. {
  191. return http_build_query(
  192. array_merge(
  193. self::$DEFAULT_OPTIONS,
  194. $this->additionalOptions,
  195. array(
  196. 'js_code' => $js,
  197. 'output_info' => ($returnErrors ? 'errors' : 'compiled_code')
  198. )
  199. ),
  200. null,
  201. '&'
  202. );
  203. }
  204. }
  205. class Minify_JS_ClosureCompiler_Exception extends Exception {}