From e8ac1dc8d02dcda52f23a38df271efd8f30bef11 Mon Sep 17 00:00:00 2001 From: Steve Clay Date: Mon, 3 Mar 2008 16:23:39 +0000 Subject: [PATCH] Perf update for JSmin (Minify_Javascript), + phpDocs for public HTTP_* class APIs, minor Minify fix, private members changed to protected to allow easier subclassing. --- lib/HTTP/ConditionalGet.php | 174 ++++++++++++++++++++---------- lib/HTTP/ConditionalGet/Build.php | 2 +- lib/HTTP/Encoder.php | 170 +++++++++++++++++++++-------- lib/Minify.php | 35 +++--- lib/Minify/CSS.php | 12 +-- lib/Minify/Controller/Page.php | 2 +- lib/Minify/HTML.php | 24 ++--- lib/Minify/Javascript.php | 40 ++++--- lib/Minify/Source.php | 6 +- 9 files changed, 304 insertions(+), 161 deletions(-) diff --git a/lib/HTTP/ConditionalGet.php b/lib/HTTP/ConditionalGet.php index d41879c..ade3245 100644 --- a/lib/HTTP/ConditionalGet.php +++ b/lib/HTTP/ConditionalGet.php @@ -32,49 +32,43 @@ */ class HTTP_ConditionalGet { - private $headers = array(); - private $lmTime = null; - private $etag = null; + /** + * Does the client have a valid copy of the requested resource? + * + * You'll want to check this after instantiating the object. If true, do + * not send content, just call sendHeaders() if you haven't already. + * + * @var bool + */ public $cacheIsValid = null; - public function getHeaders() { - return $this->headers; - } - /** - * Depending on the PHP config, PHP will buffer all output and set - * Content-Length for you. If it doesn't, or you flush() while sending data, - * you'll want to call this to let the client know up front. + * @param array $spec options + * + * 'isPublic': (bool) if true, the Cache-Control header will contain + * "public", allowing proxy caches to cache the content. Otherwise + * "private" will be sent, allowing only browsers to cache. (default false) + * + * 'lastModifiedTime': (int) if given, both ETag AND Last-Modified headers + * will be sent with content. This is recommended. + * + * 'contentHash': (string) if given, only the ETag header can be sent with + * content (only HTTP1.1 clients can conditionally GET). The given string + * should be short with no quote characters and always change when the + * resource changes (recommend md5()). This is not needed/used if + * lastModifiedTime is given. + * + * 'invalidate': (bool) if true, the client cache will be considered invalid + * without testing. Effectively this disables conditional GET. + * (default false) + * + * 'setExpires': (mixed) set this to a timestamp or GMT date to send an + * Expires header with the content instead of ETag/Last-Modified. If given, + * Conditional GETs will not be performed, but the public/private + * Cache-Control header will still be sent. (default null) + * + * @return null */ - public function setContentLength($bytes) { - return $this->headers['Content-Length'] = $bytes; - } - - public function sendHeaders() { - $headers = $this->headers; - if (array_key_exists('_responseCode', $headers)) { - header($headers['_responseCode']); - unset($headers['_responseCode']); - } - foreach ($headers as $name => $val) { - header($name . ': ' . $val); - } - } - - private function setEtag($hash, $scope) { - $this->etag = '"' . $hash - . substr($scope, 0, 3) - . '"'; - $this->headers['ETag'] = $this->etag; - } - - private function setLastModified($time) { - $this->lmTime = (int)$time; - $this->headers['Last-Modified'] = self::gmtdate($time); - } - - // TODO: allow custom Cache-Control directives, but offer pre-configured - // "modes" for common cache models public function __construct($spec) { $scope = (isset($spec['isPublic']) && $spec['isPublic']) ? 'public' @@ -84,7 +78,7 @@ class HTTP_ConditionalGet { if (is_numeric($spec['setExpires'])) { $spec['setExpires'] = self::gmtdate($spec['setExpires']); } - $this->headers = array( + $this->_headers = array( 'Cache-Control' => $scope ,'Expires' => $spec['setExpires'] ); @@ -93,41 +87,114 @@ class HTTP_ConditionalGet { } if (isset($spec['lastModifiedTime'])) { // base both headers on time - $this->setLastModified($spec['lastModifiedTime']); - $this->setEtag($spec['lastModifiedTime'], $scope); + $this->_setLastModified($spec['lastModifiedTime']); + $this->_setEtag($spec['lastModifiedTime'], $scope); } else { // hope to use ETag if (isset($spec['contentHash'])) { - $this->setEtag($spec['contentHash'], $scope); + $this->_setEtag($spec['contentHash'], $scope); } } - $this->headers['Cache-Control'] = "max-age=0, {$scope}, must-revalidate"; + $this->_headers['Cache-Control'] = "max-age=0, {$scope}, must-revalidate"; // invalidate cache if disabled, otherwise check $this->cacheIsValid = (isset($spec['invalidate']) && $spec['invalidate']) ? false - : $this->isCacheValid(); + : $this->_isCacheValid(); + } + + /** + * Get array of output headers to be sent + * + * In the case of 304 responses, this array will only contain the response + * code header: array('_responseCode' => 'HTTP/1.0 304 Not Modified') + * + * Otherwise something like: + * + * array( + * 'Cache-Control' => 'max-age=0, public, must-revalidate' + * ,'ETag' => '"foobar"' + * ) + * + * + * @return array + */ + public function getHeaders() { + return $this->_headers; + } + + /** + * Set the Content-Length header in bytes + * + * With most PHP configs, as long as you don't flush() output, this method + * is not needed and PHP will buffer all output and set Content-Length for + * you. Otherwise you'll want to call this to let the client know up front. + * + * @param int $bytes + * + * @return int copy of input $bytes + */ + public function setContentLength($bytes) { + return $this->_headers['Content-Length'] = $bytes; + } + + /** + * Send headers + * + * @see getHeaders() + * + * Note this doesn't "clear" the headers. Calling sendHeaders() will + * call header() again (but probably have not effect) and getHeaders() will + * still return the headers. + * + * @return null + */ + public function sendHeaders() { + $headers = $this->_headers; + if (array_key_exists('_responseCode', $headers)) { + header($headers['_responseCode']); + unset($headers['_responseCode']); + } + foreach ($headers as $name => $val) { + header($name . ': ' . $val); + } + } + + protected $_headers = array(); + protected $_lmTime = null; + protected $_etag = null; + + protected function _setEtag($hash, $scope) { + $this->_etag = '"' . $hash + . substr($scope, 0, 3) + . '"'; + $this->_headers['ETag'] = $this->_etag; + } + + protected function _setLastModified($time) { + $this->_lmTime = (int)$time; + $this->_headers['Last-Modified'] = self::gmtdate($time); } /** * Determine validity of client cache and queue 304 header if valid */ - private function isCacheValid() + protected function _isCacheValid() { - if (null === $this->etag) { + if (null === $this->_etag) { // ETag was our backup, so we know we don't have lmTime either return false; } $isValid = ($this->resourceMatchedEtag() || $this->resourceNotModified()); if ($isValid) { // overwrite headers, only need 304 - $this->headers = array( + $this->_headers = array( '_responseCode' => 'HTTP/1.0 304 Not Modified' ); } return $isValid; } - private function resourceMatchedEtag() { + protected function resourceMatchedEtag() { if (!isset($_SERVER['HTTP_IF_NONE_MATCH'])) { return false; } @@ -136,14 +203,14 @@ class HTTP_ConditionalGet { : $_SERVER['HTTP_IF_NONE_MATCH']; $cachedEtags = split(',', $cachedEtagList); foreach ($cachedEtags as $cachedEtag) { - if (trim($cachedEtag) == $this->etag) { + if (trim($cachedEtag) == $this->_etag) { return true; } } return false; } - private function resourceNotModified() { + protected function resourceNotModified() { if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { return false; } @@ -154,11 +221,10 @@ class HTTP_ConditionalGet { // IE has tacked on extra data to this header, strip it $ifModifiedSince = substr($ifModifiedSince, 0, $semicolon); } - return ($ifModifiedSince == self::gmtdate($this->lmTime)); + return ($ifModifiedSince == self::gmtdate($this->_lmTime)); } - private static function gmtdate($ts) { + protected static function gmtdate($ts) { return gmdate('D, d M Y H:i:s \G\M\T', $ts); } } - diff --git a/lib/HTTP/ConditionalGet/Build.php b/lib/HTTP/ConditionalGet/Build.php index c4d89e5..d13da54 100644 --- a/lib/HTTP/ConditionalGet/Build.php +++ b/lib/HTTP/ConditionalGet/Build.php @@ -104,7 +104,7 @@ class HTTP_ConditionalGet_Build { file_put_contents($file, "{$this->lastModified}|{$nextCheck}"); } - private static function _scan($max, $path, $options) + protected static function _scan($max, $path, $options) { $d = dir($path); while (false !== ($entry = $d->read())) { diff --git a/lib/HTTP/Encoder.php b/lib/HTTP/Encoder.php index 7fc4f05..0565b6f 100644 --- a/lib/HTTP/Encoder.php +++ b/lib/HTTP/Encoder.php @@ -23,70 +23,134 @@ */ class HTTP_Encoder { + /** + * Default compression level for zlib operations + * + * This level is used if encode() is not given a $compressionLevel + * + * @var int + */ public static $compressionLevel = 6; - private static $clientEncodeMethod = null; - - private $content = ''; - private $headers = array(); - - private $encodeMethod = array('', ''); + /** + * Get an HTTP Encoder object + * + * @param array $spec options + * + * 'content': (string required) content to be encoded + * + * 'type': (string) if set, the Content-Type header will have this value. + * + * 'method: (string) only set this if you are forcing a particular encoding + * method. If not set, the best method will be chosen by getAcceptedEncoding() + * The available methods are 'gzip', 'deflate', 'compress', and '' (no + * encoding) + * + * @return null + */ public function __construct($spec) { - if (isset($spec['content'])) { - $this->content = $spec['content']; - } - $this->headers['Content-Length'] = strlen($this->content); + $this->_content = $spec['content']; + $this->_headers['Content-Length'] = (string)strlen($this->_content); if (isset($spec['type'])) { - $this->headers['Content-Type'] = $spec['type']; + $this->_headers['Content-Type'] = $spec['type']; } - if (self::$clientEncodeMethod === null) { - self::$clientEncodeMethod = self::getAcceptedEncoding(); + if (self::$_clientEncodeMethod === null) { + self::$_clientEncodeMethod = self::getAcceptedEncoding(); } if (isset($spec['method']) && in_array($spec['method'], array('gzip', 'deflate', 'compress', ''))) { - $this->encodeMethod = array($spec['method'], $spec['method']); + $this->_encodeMethod = array($spec['method'], $spec['method']); } else { - $this->encodeMethod = self::$clientEncodeMethod; + $this->_encodeMethod = self::$_clientEncodeMethod; } } + /** + * Get content in current form + * + * Call after encode() for encoded content. + * + * return string + */ public function getContent() { - return $this->content; + return $this->_content; } + + /** + * Get array of output headers to be sent + * + * E.g. + * + * array( + * 'Content-Length' => '615' + * ,'Content-Encoding' => 'x-gzip' + * ,'Vary' => 'Accept-Encoding' + * ) + * + * + * @return array + */ public function getHeaders() { - return $this->headers; + return $this->_headers; } /** - * Send the file and headers (encoded or not) + * Send output headers + * + * You must call this before headers are sent and it probably cannot be + * used in conjunction with zlib output buffering / mod_gzip. Errors are + * not handled purposefully. + * + * @see getHeaders() + * + * @return null + */ + public function sendHeaders() { + foreach ($this->_headers as $name => $val) { + header($name . ': ' . $val); + } + } + + /** + * Send output headers and content + * + * A shortcut for sendHeaders() and echo getContent() * * You must call this before headers are sent and it probably cannot be * used in conjunction with zlib output buffering / mod_gzip. Errors are * not handled purposefully. + * + * @return null */ public function sendAll() { $this->sendHeaders(); - echo $this->content; + echo $this->_content; } /** - * Send just the headers + * Determine the client's best encoding method from the HTTP Accept-Encoding + * header. + * + * If no Accept-Encoding header is set, or the browser is IE before v6 SP2, + * this will return ('', ''), the "identity" encoding. + * + * A syntax-aware scan is done of the Accept-Encoding, so the method must + * be non 0. The methods are favored in order of gzip, deflate, then + * compress. + * + * Note: this value is cached internally for the entire PHP execution + * + * @return array two values, 1st is the actual encoding method, 2nd is the + * alias of that method to use in the Content-Encoding header (some browsers + * call gzip "x-gzip" etc.) */ - public function sendHeaders() { - foreach ($this->headers as $name => $val) { - header($name . ': ' . $val); - } - } - - // returns array(encoding, encoding to use in Content-Encoding header) - // eg. array('gzip', 'x-gzip') public static function getAcceptedEncoding() { - if (self::$clientEncodeMethod !== null) { - return self::$clientEncodeMethod; + if (self::$_clientEncodeMethod !== null) { + return self::$_clientEncodeMethod; } if (! isset($_SERVER['HTTP_ACCEPT_ENCODING']) - || self::isBuggyIe()) + || self::_isBuggyIe()) { return array('', ''); } @@ -104,37 +168,55 @@ class HTTP_Encoder { } /** - * If conditionsEncode the content - * @return bool success + * Encode (compress) the content + * + * If the encode method is '' (none) or compression level is 0, or the 'zlib' + * extension isn't loaded, we return false. + * + * Then the appropriate gz_* function is called to compress the content. If + * this fails, false is returned. + * + * If successful, the Content-Length header is updated, and Content-Encoding + * and Vary headers are added. + * + * @param int $compressionLevel given to zlib functions. If not given, the + * class default will be used. + * + * @return bool success true if the content was actually compressed */ public function encode($compressionLevel = null) { if (null === $compressionLevel) { $compressionLevel = self::$compressionLevel; } - if ('' === $this->encodeMethod[0] + if ('' === $this->_encodeMethod[0] || ($compressionLevel == 0) || !extension_loaded('zlib')) { return false; } - if ($this->encodeMethod[0] === 'gzip') { - $encoded = gzencode($this->content, $compressionLevel); - } elseif ($this->encodeMethod[0] === 'deflate') { - $encoded = gzdeflate($this->content, $compressionLevel); + if ($this->_encodeMethod[0] === 'gzip') { + $encoded = gzencode($this->_content, $compressionLevel); + } elseif ($this->_encodeMethod[0] === 'deflate') { + $encoded = gzdeflate($this->_content, $compressionLevel); } else { - $encoded = gzcompress($this->content, $compressionLevel); + $encoded = gzcompress($this->_content, $compressionLevel); } if (false === $encoded) { return false; } - $this->headers['Content-Length'] = strlen($encoded); - $this->headers['Content-Encoding'] = $this->encodeMethod[1]; - $this->headers['Vary'] = 'Accept-Encoding'; - $this->content = $encoded; + $this->_headers['Content-Length'] = strlen($encoded); + $this->_headers['Content-Encoding'] = $this->_encodeMethod[1]; + $this->_headers['Vary'] = 'Accept-Encoding'; + $this->_content = $encoded; return true; } - private static function isBuggyIe() + protected static $_clientEncodeMethod = null; + protected $content = ''; + protected $headers = array(); + protected $encodeMethod = array('', ''); + + protected static function _isBuggyIe() { if (strstr($_SERVER['HTTP_USER_AGENT'], 'Opera') || !preg_match('/^Mozilla\/4\.0 \(compatible; MSIE ([0-9]\.[0-9])/i', $_SERVER['HTTP_USER_AGENT'], $m)) diff --git a/lib/Minify.php b/lib/Minify.php index 2f9adec..c89aefa 100644 --- a/lib/Minify.php +++ b/lib/Minify.php @@ -146,16 +146,15 @@ class Minify { $cg = new HTTP_ConditionalGet($cgOptions); if ($cg->cacheIsValid) { // client's cache is valid - if (self::$_options['quiet']) { - return array( - 'success' => true - ,'statusCode' => 304 - ,'content' => '' - ,'headers' => array() - ); - } else { + if (! self::$_options['quiet']) { $cg->sendHeaders(); - } + } + return array( + 'success' => true + ,'statusCode' => 304 + ,'content' => '' + ,'headers' => array() + ); } // client will need output $headers = $cg->getHeaders(); @@ -216,22 +215,22 @@ class Minify { /** * @var mixed null if disk cache is not to be used */ - private static $_cachePath = null; + protected static $_cachePath = null; /** * @var Minify_Controller active controller for current request */ - private static $_controller = null; + protected static $_controller = null; /** * @var array options for current request */ - private static $_options = null; + protected static $_options = null; /** * @var Cache_Lite_File cache obj for current request */ - private static $_cache = null; + protected static $_cache = null; @@ -247,7 +246,7 @@ class Minify { * * @return string minified, encoded content */ - private static function _fetchContent($encodeMethod) + protected static function _fetchContent($encodeMethod) { $cacheId = self::_getCacheId(self::$_controller->sources, self::$_options) . $encodeMethod; @@ -278,7 +277,7 @@ class Minify { * * @return null */ - private static function _setupCache() { + protected static function _setupCache() { // until the patch is rolled into PEAR, we'll provide the // class in our package require_once dirname(__FILE__) . '/Cache/Lite/File.php'; @@ -297,7 +296,7 @@ class Minify { * * @return string */ - private static function _combineMinify() { + protected static function _combineMinify() { $type = self::$_options['contentType']; // ease readability // when combining scripts, make sure all statements separated @@ -366,7 +365,7 @@ class Minify { * * @return string */ - private static function _encode($content) + protected static function _encode($content) { if (self::$_options['encodeMethod'] === '' || ! self::$_options['encodeOutput']) { @@ -389,7 +388,7 @@ class Minify { * * @return string */ - private static function _getCacheId() { + protected static function _getCacheId() { return md5(serialize(array( Minify_Source::getDigest(self::$_controller->sources) ,self::$_options['minifiers'] diff --git a/lib/Minify/CSS.php b/lib/Minify/CSS.php index fb930e5..05c35ca 100644 --- a/lib/Minify/CSS.php +++ b/lib/Minify/CSS.php @@ -87,17 +87,17 @@ class Minify_CSS { * * I.e. are some browsers targetted until the next comment? */ - private static $_inHack = false; + protected static $_inHack = false; /** * @var string string to be prepended to relative URIs */ - private static $_tempPrepend = ''; + protected static $_tempPrepend = ''; /** * @var string path of this stylesheet for rewriting purposes */ - private static $_tempCurrentPath = ''; + protected static $_tempCurrentPath = ''; /** * Process what looks like a comment and return a replacement @@ -106,7 +106,7 @@ class Minify_CSS { * * @return string */ - private static function _commentCB($m) + protected static function _commentCB($m) { $m = $m[1]; // $m is everything after the opening tokens and before the closing tokens @@ -148,12 +148,12 @@ class Minify_CSS { * * @return string */ - private static function _selectorsCB($m) + protected static function _selectorsCB($m) { return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]); } - private static function _urlCB($m) + protected static function _urlCB($m) { $isImport = (0 === strpos($m[0], '@import')); if ($isImport) { diff --git a/lib/Minify/Controller/Page.php b/lib/Minify/Controller/Page.php index b9ebaed..0fdf82f 100644 --- a/lib/Minify/Controller/Page.php +++ b/lib/Minify/Controller/Page.php @@ -54,7 +54,7 @@ class Minify_Controller_Page extends Minify_Controller_Base { return $options; } - private $_loadCssJsMinifiers = false; + protected $_loadCssJsMinifiers = false; /** * @see Minify_Controller_Base::loadMinifier() diff --git a/lib/Minify/HTML.php b/lib/Minify/HTML.php index 6f03ce8..4e26114 100644 --- a/lib/Minify/HTML.php +++ b/lib/Minify/HTML.php @@ -84,21 +84,21 @@ class Minify_HTML { return $html; } - private static $_isXhtml = false; - private static $_replacementHash = null; - private static $_pres = array(); - private static $_scripts = array(); - private static $_styles = array(); - private static $_cssMinifier = null; - private static $_jsMinifier = null; + protected static $_isXhtml = false; + protected static $_replacementHash = null; + protected static $_pres = array(); + protected static $_scripts = array(); + protected static $_styles = array(); + protected static $_cssMinifier = null; + protected static $_jsMinifier = null; - private static function _removePreCB($m) + protected static function _removePreCB($m) { self::$_pres[] = $m[1]; return self::$_replacementHash . 'PRE' . count(self::$_pres); } - private static function _removeStyleCB($m) + protected static function _removeStyleCB($m) { $openStyle = $m[1]; $css = $m[2]; @@ -123,7 +123,7 @@ class Minify_HTML { return self::$_replacementHash . 'STYLE' . count(self::$_styles); } - private static function _removeScriptCB($m) + protected static function _removeScriptCB($m) { $openScript = $m[1]; $js = $m[2]; @@ -147,14 +147,14 @@ class Minify_HTML { return self::$_replacementHash . 'SCRIPT' . count(self::$_scripts); } - private static function _removeCdata($str) + protected static function _removeCdata($str) { return (false !== strpos($str, ''), '', $str) : $str; } - private static function _needsCdata($str) + protected static function _needsCdata($str) { return (self::$_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str)); } diff --git a/lib/Minify/Javascript.php b/lib/Minify/Javascript.php index 047b9d6..d2ff810 100644 --- a/lib/Minify/Javascript.php +++ b/lib/Minify/Javascript.php @@ -39,9 +39,9 @@ * @package Minify_Javascript * @author Ryan Grove * @copyright 2002 Douglas Crockford (JSMin.c) - * @copyright 2007 Ryan Grove (PHP port) + * @copyright 2008 Ryan Grove (PHP port) * @license http://opensource.org/licenses/mit-license.php MIT License - * @version 1.1.0 (2007-06-01) + * @version 1.1.1 (2008-03-03) * @link http://code.google.com/p/jsmin-php/ */ @@ -55,7 +55,7 @@ class Minify_Javascript { private $inputIndex = 0; private $inputLength = 0; private $lookAhead = null; - private $output = array(); + private $output = ''; // -- Public Static Methods -------------------------------------------------- @@ -74,15 +74,15 @@ class Minify_Javascript { private function action($d) { switch($d) { case 1: - $this->output[] = $this->a; + $this->output .= $this->a; case 2: $this->a = $this->b; if ($this->a === "'" || $this->a === '"') { for (;;) { - $this->output[] = $this->a; - $this->a = $this->get(); + $this->output .= $this->a; + $this->a = $this->get(); if ($this->a === $this->b) { break; @@ -93,8 +93,8 @@ class Minify_Javascript { } if ($this->a === '\\') { - $this->output[] = $this->a; - $this->a = $this->get(); + $this->output .= $this->a; + $this->a = $this->get(); } } } @@ -107,25 +107,23 @@ class Minify_Javascript { $this->a === ':' || $this->a === '[' || $this->a === '!' || $this->a === '&' || $this->a === '|' || $this->a === '?')) { - $this->output[] = $this->a; - $this->output[] = $this->b; + $this->output .= $this->a; + $this->output .= $this->b; for (;;) { $this->a = $this->get(); if ($this->a === '/') { break; - } - elseif ($this->a === '\\') { - $this->output[] = $this->a; - $this->a = $this->get(); - } - elseif (ord($this->a) <= self::ORD_LF) { + } elseif ($this->a === '\\') { + $this->output .= $this->a; + $this->a = $this->get(); + } elseif (ord($this->a) <= self::ORD_LF) { throw new Minify_JavascriptException('Unterminated regular expression '. 'literal.'); } - $this->output[] = $this->a; + $this->output .= $this->a; } $this->b = $this->next(); @@ -141,8 +139,7 @@ class Minify_Javascript { if ($this->inputIndex < $this->inputLength) { $c = $this->input[$this->inputIndex]; $this->inputIndex += 1; - } - else { + } else { $c = null; } } @@ -167,8 +164,7 @@ class Minify_Javascript { case ' ': if (self::isAlphaNum($this->b)) { $this->action(1); - } - else { + } else { $this->action(2); } break; @@ -237,7 +233,7 @@ class Minify_Javascript { } } - return implode('', $this->output); + return $this->output; } private function next() { diff --git a/lib/Minify/Source.php b/lib/Minify/Source.php index b37f3b2..203b410 100644 --- a/lib/Minify/Source.php +++ b/lib/Minify/Source.php @@ -129,8 +129,8 @@ class Minify_Source { return 'text/plain'; } - private $_content = null; - private $_filepath = null; - private $_id = null; + protected $_content = null; + protected $_filepath = null; + protected $_id = null; }