diff --git a/README b/README index d6bdd31..56398c7 100644 --- a/README +++ b/README @@ -42,33 +42,25 @@ Sources with known "last modified" timestamps allow Minify to implement conditional GETs. You configure Minify via a Minify_Controller object. The controller -supplies the sources and some other information about a request, +supplies the sources and default options to serve a request, determining how it's to be responded to. Several controllers are supplied with Minify, but it's also fairly easy to write your own. E.g. a controller could emulate v1.x of minify.php. -To use an existing controller, you call Minify::serve(), passing it the -name of your controller of choice (without the "Minify_Controller" -prefix), an array of options for the controller, and an array of general -Minify options (optional). Eg.: +To use an existing controller, you call Minify::serve(), passing it: +1. the name of your controller of choice (without the "Minify_Controller" + prefix) or a custom controller object subclassed from Minify_Controller_Base. +2. a combined array of controller and Minify options. Eg.: // serve a minified javascript file and tell clients to cache it for a // year -Minify::serve( - 'Files' - ,array('/path/to/file1.js', '/path/to/file2.js') - ,array('setExpires' => (time() + 86400 * 365)) -); +Minify::serve('Files', array( + 'files' => array('/path/to/file1.js', '/path/to/file2.js') + ,'setExpires' => (time() + 86400 * 365) +)); -The above creates an instance of Minify_Controller_Files, passing the -two arrays to its constructor. - -The first array depends on the controller. In this case, -Minify_Controller_Files needs a list of file paths. - -The second array (optional) has options for Minify. (Since this is -passed through the controller, some controllers may use/alter this -data). +The above creates an instance of Minify_Controller_Files, which creates +source objects from the 'files' option, and supplies other default options. MINIFY USAGE diff --git a/lib/Minify.php b/lib/Minify.php index 427e935..d2b9976 100644 --- a/lib/Minify.php +++ b/lib/Minify.php @@ -26,6 +26,10 @@ require_once 'Minify/Source.php'; class Minify { + const TYPE_CSS = 'text/css'; + const TYPE_JS = 'application/x-javascript'; + const TYPE_HTML = 'text/html'; + /** * @var bool Should the un-encoded version be cached? * @@ -63,7 +67,7 @@ class Minify { * * @return mixed false on failure or array of content and headers sent */ - public static function serve($type, $ctrlOptions = array(), $minOptions = array()) { + public static function serveold($type, $ctrlOptions = array(), $minOptions = array()) { $class = 'Minify_Controller_' . $type; if (! class_exists($class, false)) { require_once "Minify/Controller/{$type}.php"; @@ -80,22 +84,82 @@ class Minify { } /** - * Handle a request for a minified file. + * Serve a request for a minified file. * - * You must supply a controller object which has the same public API - * as Minify_Controller. + * @param mixed instance of subclass of Minify_Controller_Base or string name of controller. E.g. 'Files' * - * @param Minify_Controller $controller + * @param array $options controller/serve options + * + * @return array success, statusCode, content, and headers generated + * + * Here are the available options and defaults in the base controller: + * + * 'isPublic' : send "public" instead of "private" in Cache-Control + * headers, allowing shared caches to cache the output. (default true) + * + * 'quiet' : set to true to have no content/headers sent (default false) + * + * 'encodeOutput' : to disable content encoding, set this to false (default true) + * + * 'encodeMethod' : generally you should let this be determined by + * HTTP_Encoder (leave null), but you can force a particular encoding + * to be returned, by setting this to 'gzip', 'deflate', 'compress', or '' + * (no encoding) + * + * 'encodeLevel' : level of encoding compression (0 to 9, default 9) + * + * 'contentTypeCharset' : if given, this will be appended to the Content-Type + * header sent (needed mainly for HTML docs) + * + * 'setExpires' : set this to a timestamp or GMT date to have Minify send + * an HTTP Expires header instead of checking for conditional GET (default null). + * E.g. (time() + 86400 * 365) for 1yr + * Note this has nothing to do with server-side caching. + * + * 'perType' : this is an array of options to send to a particular minifier + * function using the content-type as key. E.g. To send the CSS minifier an + * option: + * + * $options['perType'][Minify::TYPE_CSS]['optionName'] = 'optionValue'; + * + * When the CSS minifier is called, the 2nd argument will be + * array('optionName' => 'optionValue'). + * + * Any controller options are documented in that controller's setupSources() method. * - * @return mixed false on failure or array of content and headers sent */ - public static function handleRequest($controller) { - if (! $controller->requestIsValid) { - return false; + public static function serve($controller, $options = array()) { + if (is_string($controller)) { + // make $controller into object + $class = 'Minify_Controller_' . $controller; + if (! class_exists($class, false)) { + require_once "Minify/Controller/{$controller}.php"; + } + $controller = new $class(); + } + + // set up controller sources and mix remaining options with + // controller defaults + $options = $controller->setupSources($options); + $options = $controller->analyzeSources($options); + self::$_options = $controller->mixInDefaultOptions($options); + + if (! $controller->sources) { + // invalid request! + if (! self::$_options['quiet']) { + header(self::$_options['badRequestHeader']); + echo self::$_options['badRequestHeader']; + } + list(,$statusCode) = explode(' ', self::$_options['badRequestHeader']); + return array( + 'success' => false + ,'statusCode' => (int)$statusCode + ,'content' => '' + ,'headers' => array() + ); } self::$_controller = $controller; - self::_resolveOptions($controller->minOptions); $cgOptions = array( 'lastModifiedTime' => self::$_options['lastModifiedTime'] @@ -112,11 +176,13 @@ class Minify { // client's cache is valid if (self::$_options['quiet']) { return array( - 'content' => '' - ,'headers' => $cg->getHeaders() + 'success' => true + ,'statusCode' => 304 + ,'content' => '' + ,'headers' => array() ); } else { - $cg->sendHeaders(); + $cg->sendHeaders(); } } // client will need output @@ -165,10 +231,12 @@ class Minify { foreach ($headers as $name => $val) { header($name . ': ' . $val); } - echo $content; + echo $content; } return array( - 'content' => $content + 'success' => true + ,'statusCode' => 200 + ,'content' => $content ,'headers' => $headers ); } @@ -193,34 +261,7 @@ class Minify { */ private static $_cache = null; - /** - * Resolve Minify options based on those passed from controller and Minify's defaults - * - * @return null - */ - private static function _resolveOptions($ctrlOptions) - { - self::$_options = array_merge(array( - // default options - 'isPublic' => true - ,'encodeOutput' => true - ,'encodeMethod' => null // determine later - ,'encodeLevel' => 9 - ,'perType' => array() // per-type minifier options - ,'contentTypeCharset' => null // leave out of Content-Type header - ,'setExpires' => null // send Expires header - ,'quiet' => false - ), $ctrlOptions); - $defaultMinifiers = array( - 'text/css' => array('Minify_CSS', 'minify') - ,'application/x-javascript' => array('Minify_Javascript', 'minify') - ,'text/html' => array('Minify_HTML', 'minify') - ); - if (! isset($ctrlOptions['minifiers'])) { - $ctrlOptions['minifiers'] = array(); - } - self::$_options['minifiers'] = array_merge($defaultMinifiers, $ctrlOptions['minifiers']); - } + /** * Fetch encoded content from cache (or generate and store it). @@ -288,21 +329,25 @@ class Minify { $type = self::$_options['contentType']; // ease readability // when combining scripts, make sure all statements separated - $implodeSeparator = ($type === 'application/x-javascript') + $implodeSeparator = ($type === self::TYPE_JS) ? ';' : ''; - // default options and minifier function for all sources + // allow the user to pass a particular array of options to each + // minifier (designated by type). source objects may still override + // these $defaultOptions = isset(self::$_options['perType'][$type]) ? self::$_options['perType'][$type] : array(); - // if minifier not set, default is no minification + // if minifier not set, default is no minification. source objects + // may still override this $defaultMinifier = isset(self::$_options['minifiers'][$type]) ? self::$_options['minifiers'][$type] : false; if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) { // all source have same options/minifier, better performance + // to combine, then minify once foreach (self::$_controller->sources as $source) { $pieces[] = $source->getContent(); } @@ -312,7 +357,7 @@ class Minify { $content = call_user_func($defaultMinifier, $content, $defaultOptions); } } else { - // minify each source with its own options and minifier + // minify each source with its own options and minifier, then combine foreach (self::$_controller->sources as $source) { // allow the source to override our minifier and options $minifier = (null !== $source->minifier) diff --git a/lib/Minify/Controller/Base.php b/lib/Minify/Controller/Base.php index 304f15c..f468a76 100644 --- a/lib/Minify/Controller/Base.php +++ b/lib/Minify/Controller/Base.php @@ -7,111 +7,58 @@ * for minification and set options like contentType. It's also responsible * for loading minifier code upon request. */ -class Minify_Controller_Base { +abstract class Minify_Controller_Base { /** - * @var array instances of Minify_Source, which provide content and - * any individual minification needs. + * Setup controller sources * - * @see Minify_Source + * You must override this method in your subclass controller to set + * $this->sources. If the request is NOT valid, make sure $this->sources + * is left an empty array. Then strip any controller-specific options from + * $options and return it. + * + * @param array $options controller and Minify options + * + * @param array $options Minify options */ - public $sources = array(); + abstract public function setupSources($options); /** - * @var array options to be read by Minify + * Get default Minify options for this controller. * - * Any unspecified options will use the default values. - * - * 'minifiers': this is an array with content-types as keys and callbacks as - * values. Specify a custom minifier by setting this option. E.g.: - * - * - * $this->options['minifiers']['application/x-javascript'] = - * array('Minify_Packer', 'minify'); // callback - * - * - * Note that, when providing your own minifier, the controller must be able - * to load its code on demand. @see loadMinifier() - * - * 'perType' : this is an array of options to send to a particular content - * type minifier by using the content-type as key. E.g. To send the CSS - * minifier an option: $options['perType']['text/css']['foo'] = 'bar'; - * When the CSS minifier is called, the 2nd argument will be - * array('foo' => 'bar'). - * - * 'isPublic' : send "public" instead of "private" in Cache-Control headers, - * allowing shared caches to cache the output. (default true) - * - * 'encodeOutput' : to disable content encoding, set this to false - * - * 'encodeMethod' : generally you should let this be determined by - * HTTP_Encoder (the default null), but you can force a particular encoding - * to be returned, by setting this to 'gzip', 'deflate', 'compress', or '' - * (no encoding) - * - * 'encodeLevel' : level of encoding compression (0 to 9, default 9) - * - * 'contentTypeCharset' : if given, this will be appended to the Content-Type - * header sent, useful mainly for HTML docs. - * - * 'setExpires' : set this to a timestamp or GMT date to have Minify send - * an HTTP Expires header instead of checking for conditional GET. - * E.g. (time() + 86400 * 365) for 1yr (default null) - * This has nothing to do with server-side caching. - * - * 'quiet' : set to true to have Minify not output any content/headers - * (bool, default = false) + * Override in subclass to change defaults * + * @return array options for Minify */ - public $minOptions = array(); + public function getDefaultMinifyOptions() { + return array( + 'isPublic' => true + ,'encodeOutput' => true + ,'encodeMethod' => null // determine later + ,'encodeLevel' => 9 + ,'perType' => array() // no per-type minifier options + ,'contentTypeCharset' => null // leave out of Content-Type header + ,'setExpires' => null // use conditional GET + ,'quiet' => false // serve() will send headers and output + + // if you override this, the response code MUST be directly after + // the first space. + ,'badRequestHeader' => 'HTTP/1.0 400 Bad Request' + ); + } /** - * @var bool was the user request valid + * Get default minifiers for this controller. * - * This must be explicity be set to true to process the request. This should - * be done by the child class constructor. + * Override in subclass to change defaults + * + * @return array minifier callbacks for common types */ - public $requestIsValid = false; - - /** - * Parent constructor for a controller class - * - * If your subclass controller is not happy with the request, it can simply return - * without setting $this->requestIsValid. Minify will not check any other member. - * - * If the request is valid, you must set $this->requestIsValid = true and also call - * the parent constructor, passing along an array of source objects and any Minify - * options: - * - * parent::__construct($sources, $options); - * - * - * This function sets $this->sources and determines $this->options 'contentType' and - * 'lastModifiedTime'. - * - * @param array $sources array of Minify_Source instances - * - * @param array $options options for Minify - * - * @return null - */ - public function __construct($sources, $options = array()) { - if (empty($sources)) { - $this->requestIsValid = false; - } - $this->sources = $sources; - if (! isset($options['contentType'])) { - $options['contentType'] = Minify_Source::getContentType($this->sources); - } - // last modified is needed for caching, even if setExpires is set - if (! isset($options['lastModifiedTime'])) { - $max = 0; - foreach ($sources as $source) { - $max = max($source->lastModified, $max); - } - $options['lastModifiedTime'] = $max; - } - $this->minOptions = $options; + public function getDefaultMinifers() { + $ret[Minify::TYPE_JS] = array('Minify_Javascript', 'minify'); + $ret[Minify::TYPE_CSS] = array('Minify_CSS', 'minify'); + $ret[Minify::TYPE_HTML] = array('Minify_HTML', 'minify'); + return $ret; } /** @@ -124,7 +71,10 @@ class Minify_Controller_Base { * function will include 'Jimmy/Minifier.php' * * If you need code loaded on demand and this doesn't suit you, you'll need - * to override this function by extending the class. + * to override this function in your subclass. + * @see Minify_Controller_Page::loadMinifier() + * + * @param callback $minifierCallback callback of minifier function * * @return null */ @@ -137,4 +87,58 @@ class Minify_Controller_Base { require str_replace('_', '/', $minifierCallback[0]) . '.php'; } } + + /** + * @var array instances of Minify_Source, which provide content and + * any individual minification needs. + * + * @see Minify_Source + */ + public $sources = array(); + + /** + * Mix in default controller options with user-given options + * + * @param array $options user options + * + * @return array mixed options + */ + public final function mixInDefaultOptions($options) + { + $ret = array_merge( + $this->getDefaultMinifyOptions(), $options + ); + if (! isset($options['minifiers'])) { + $options['minifiers'] = array(); + } + $ret['minifiers'] = array_merge( + $this->getDefaultMinifers(), $options['minifiers'] + ); + return $ret; + } + + /** + * Analyze sources (if there are any) and set $options 'contentType' + * and 'lastModifiedTime' if they already aren't. + * + * @param array $options options for Minify + * + * @return array options for Minify + */ + public final function analyzeSources($options = array()) { + if ($this->sources) { + if (! isset($options['contentType'])) { + $options['contentType'] = Minify_Source::getContentType($this->sources); + } + // last modified is needed for caching, even if setExpires is set + if (! isset($options['lastModifiedTime'])) { + $max = 0; + foreach ($this->sources as $source) { + $max = max($source->lastModified, $max); + } + $options['lastModifiedTime'] = $max; + } + } + return $options; + } } diff --git a/lib/Minify/Controller/Files.php b/lib/Minify/Controller/Files.php index f6f18fb..9c3535a 100644 --- a/lib/Minify/Controller/Files.php +++ b/lib/Minify/Controller/Files.php @@ -9,9 +9,11 @@ require_once 'Minify/Controller/Base.php'; * * $dr = $_SERVER['DOCUMENT_ROOT']; * Minify::serve('Files', array( - * $dr . '/js/jquery.js' - * ,$dr . '/js/plugins.js' - * ,$dr . '/js/site.js' + * 'files' => array( + * $dr . '/js/jquery.js' + * ,$dr . '/js/plugins.js' + * ,$dr . '/js/site.js' + * ) * )); * * @@ -19,28 +21,36 @@ require_once 'Minify/Controller/Base.php'; class Minify_Controller_Files extends Minify_Controller_Base { /** - * @param array $spec array of full paths of files to be minified + * Set up file sources * - * @param array $options options to pass to Minify + * @param array $options controller and Minify options + * @return array Minify options * - * @return null + * Controller options: + * + * 'files': (required) array of complete file paths */ - public function __construct($spec, $options = array()) { + public function setupSources($options) { + // strip controller options + $files = $options['files']; + unset($options['files']); + $sources = array(); - foreach ($spec as $file) { + foreach ($files as $file) { $file = realpath($file); if (file_exists($file)) { $sources[] = new Minify_Source(array( 'filepath' => $file )); } else { - return; + // file not found + return $options; } } if ($sources) { - $this->requestIsValid = true; + $this->sources = $sources; } - parent::__construct($sources, $options); + return $options; } } diff --git a/lib/Minify/Controller/Groups.php b/lib/Minify/Controller/Groups.php index d94db39..94b525d 100644 --- a/lib/Minify/Controller/Groups.php +++ b/lib/Minify/Controller/Groups.php @@ -8,16 +8,11 @@ require_once 'Minify/Controller/Base.php'; * * * $dr = $_SERVER['DOCUMENT_ROOT']; - * Minify::serve('Groups', array( - * 'css' => array( - * $dr . '/css/type.css' - * ,$dr . '/css/layout.css' - * ) - * ,'js' => array( - * $dr . '/js/jquery.js' - * ,$dr . '/js/plugins.js' - * ,$dr . '/js/site.js' - * ) + * Minify::serve('Groups', array( + * 'groups' => array( + * 'css' => array($dr . '/css/type.css', $dr . '/css/layout.css') + * ,'js' => array($dr . '/js/jquery.js', $dr . '/js/site.js') + * ) * )); * * @@ -27,20 +22,28 @@ require_once 'Minify/Controller/Base.php'; class Minify_Controller_Groups extends Minify_Controller_Base { /** - * @param array $spec associative array of keys to arrays of file paths. + * Set up groups of files as sources * - * @param array $options optional options to pass to Minify + * @param array $options controller and Minify options + * @return array Minify options * - * @return null + * Controller options: + * + * 'groups': (required) array mapping PATH_INFO strings to arrays + * of complete file paths. @see Minify_Controller_Groups */ - public function __construct($spec, $options = array()) { + public function setupSources($options) { + // strip controller options + $groups = $options['groups']; + unset($options['groups']); + $pi = substr($_SERVER['PATH_INFO'], 1); - if (! isset($spec[$pi])) { + if (! isset($groups[$pi])) { // not a valid group - return; + return $options; } $sources = array(); - foreach ($spec[$pi] as $file) { + foreach ($groups[$pi] as $file) { $file = realpath($file); if (file_exists($file)) { $sources[] = new Minify_Source(array( @@ -48,13 +51,13 @@ class Minify_Controller_Groups extends Minify_Controller_Base { )); } else { // file doesn't exist - return; + return $options; } } if ($sources) { - $this->requestIsValid = true; + $this->sources = $sources; } - parent::__construct($sources, $options); + return $options; } } diff --git a/lib/Minify/Controller/Page.php b/lib/Minify/Controller/Page.php index 94ba53b..b9ebaed 100644 --- a/lib/Minify/Controller/Page.php +++ b/lib/Minify/Controller/Page.php @@ -11,37 +11,47 @@ require_once 'Minify/Controller/Base.php'; class Minify_Controller_Page extends Minify_Controller_Base { /** - * @param array $spec array of options. You *must* set 'content' and 'id', - * but setting 'lastModifiedTime' is recommeded in order to allow server - * and client-side caching. + * Set up source of HTML content * - * If you set 'minifyAll' => 1, all CSS and Javascript blocks - * will be individually minified. + * @param array $options controller and Minify options + * @return array Minify options * - * @param array $options optional options to pass to Minify + * Controller options: * - * @return null + * 'content': (required) HTML markup + * + * 'id': (required) id of page (string for use in server-side caching) + * + * 'lastModifiedTime': timestamp of when this content changed. This + * is recommended to allow both server and client-side caching. + * + * 'minifyAll': should all CSS and Javascript blocks be individually + * minified? (default false) */ - public function __construct($spec, $options = array()) { + public function setupSources($options) { + // strip controller options $sourceSpec = array( - 'content' => $spec['content'] - ,'id' => $spec['id'] - ,'minifier' => array('Minify_HTML', 'minify') + 'content' => $options['content'] + ,'id' => $options['id'] ); - if (isset($spec['minifyAll'])) { + unset($options['content'], $options['id']); + + if (isset($options['minifyAll'])) { + // this will be the 2nd argument passed to Minify_HTML::minify() $sourceSpec['minifyOptions'] = array( 'cssMinifier' => array('Minify_CSS', 'minify') ,'jsMinifier' => array('Minify_Javascript', 'minify') ); $this->_loadCssJsMinifiers = true; + unset($options['minifyAll']); } - $sources[] = new Minify_Source($sourceSpec); - if (isset($spec['lastModifiedTime'])) { - $options['lastModifiedTime'] = $spec['lastModifiedTime']; - } - $options['contentType'] = 'text/html'; - $this->requestIsValid = true; - parent::__construct($sources, $options); + $this->sources[] = new Minify_Source($sourceSpec); + + // may not be needed + //$options['minifier'] = array('Minify_HTML', 'minify'); + + $options['contentType'] = Minify::TYPE_HTML; + return $options; } private $_loadCssJsMinifiers = false; diff --git a/web/examples/1/m.php b/web/examples/1/m.php index ff4cce5..1cb7dfe 100644 --- a/web/examples/1/m.php +++ b/web/examples/1/m.php @@ -38,9 +38,8 @@ if (isset($_GET['f'])) { // The Files controller serves an array of files, but here we just // need one. Minify::serve('Files', array( - dirname(__FILE__) . '/' . $filename - ), array( - 'setExpires' => $setExpires + 'files' => array(dirname(__FILE__) . '/' . $filename) + ,'setExpires' => $setExpires )); exit(); } diff --git a/web/test/test_Minify.php b/web/test/test_Minify.php index 1489bbe..5cf0f54 100644 --- a/web/test/test_Minify.php +++ b/web/test/test_Minify.php @@ -14,8 +14,10 @@ $lastModified = time() - 86400; // Test minifying JS and serving with Expires header $expected = array( + 'success' => true + ,'statusCode' => 200 // Minify_Javascript always converts to \n line endings - 'content' => preg_replace('/\\r\\n?/', "\n", file_get_contents($thisDir . '/minify/minified.js')) + ,'content' => preg_replace('/\\r\\n?/', "\n", file_get_contents($thisDir . '/minify/minified.js')) ,'headers' => array ( 'Cache-Control' => 'public', 'Expires' => gmdate('D, d M Y H:i:s \G\M\T', $tomorrow), @@ -23,10 +25,11 @@ $expected = array( ) ); $output = Minify::serve('Files', array( - $thisDir . '/minify/email.js' - ,$thisDir . '/minify/QueryString.js' -), array( - 'quiet' => true + 'files' => array( + $thisDir . '/minify/email.js' + ,$thisDir . '/minify/QueryString.js' + ) + ,'quiet' => true ,'setExpires' => $tomorrow ,'encodeOutput' => false )); @@ -44,7 +47,9 @@ if (! $passed) { unset($_SERVER['HTTP_IF_NONE_MATCH'], $_SERVER['HTTP_IF_MODIFIED_SINCE']); $expected = array( - 'content' => file_get_contents($thisDir . '/minify/minified.css') + 'success' => true + ,'statusCode' => 200 + ,'content' => file_get_contents($thisDir . '/minify/minified.css') ,'headers' => array ( 'Last-Modified' => gmdate('D, d M Y H:i:s \G\M\T', $lastModified), 'ETag' => "\"{$lastModified}pub\"", @@ -53,13 +58,14 @@ $expected = array( ) ); $output = Minify::serve('Files', array( - $thisDir . '/css/styles.css' - ,$thisDir . '/css/subsilver.css' -), array( - 'quiet' => true + 'files' => array( + $thisDir . '/css/styles.css' + ,$thisDir . '/css/subsilver.css' + ) + ,'quiet' => true ,'lastModifiedTime' => $lastModified ,'encodeOutput' => false -)); +)); $passed = assertTrue($expected === $output, 'Minify - CSS and Etag/Last-Modified'); echo "\nOutput: " .var_export($output, 1). "\n\n"; if (! $passed) { @@ -74,15 +80,16 @@ $_SERVER['HTTP_IF_NONE_MATCH'] = "\"{$lastModified}pub\""; $_SERVER['HTTP_IF_MODIFIED_SINCE'] = gmdate('D, d M Y H:i:s \G\M\T', $lastModified); $expected = array ( - 'content' => '', - 'headers' => array ( - '_responseCode' => 'HTTP/1.0 304 Not Modified', - ), + 'success' => true + ,'statusCode' => 304 + ,'content' => '', + 'headers' => array() ); $output = Minify::serve('Files', array( - $thisDir . '/css/styles.css' -), array( - 'quiet' => true + 'files' => array( + $thisDir . '/css/styles.css' + ) + ,'quiet' => true ,'lastModifiedTime' => $lastModified ,'encodeOutput' => false ));