1
0
mirror of https://github.com/mrclay/minify.git synced 2025-02-22 07:52:25 +01:00
minify/lib/Minify.php
2008-02-28 18:42:56 +00:00

365 lines
13 KiB
PHP

<?php
/**
* Minify - Combines, minifies, and caches JavaScript and CSS files on demand.
*
* See http://code.google.com/p/minify/ for usage instructions.
*
* This library was inspired by jscsscomp by Maxim Martynyuk <flashkot@mail.ru>
* and by the article "Supercharged JavaScript" by Patrick Hunlock
* <wb@hunlock.com>.
*
* JSMin was originally written by Douglas Crockford <douglas@crockford.com>.
*
* Requires PHP 5.2.1+.
*
* @package Minify
* @author Ryan Grove <ryan@wonko.com>
* @author Stephen Clay <steve@mrclay.org>
* @copyright 2007 Ryan Grove. All rights reserved.
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @version 1.9.0
* @link http://code.google.com/p/minify/
*/
require_once 'Minify/Source.php';
class Minify {
/**
* @var bool Should the un-encoded version be cached?
*
* True results in more cache files, but lower PHP load if different
* encodings are commonly requested.
*/
public static $cacheUnencodedVersion = true;
/**
* Specify a writeable directory for cache files. If not called, Minify
* will not use a disk cache and, for each 200 response, will need to
* recombine files, minify and encode the output.
*
* @param string $path Full directory path for cache files (should not end
* in directory separator character). If not provided, Minify will attempt to
* write to the path returned by sys_get_temp_dir().
*
* @return null
*/
public static function useServerCache($path = null) {
self::$_cachePath = (null === $path)
? sys_get_temp_dir()
: $path;
}
/**
* Create a controller instance and handle the request
*
* @param string type This should be the filename of the controller without
* extension. e.g. 'Group'
*
* @param array $spec options for the controller's constructor
*
* @return mixed a Minify controller object
*/
public static function serve($type, $spec = array(), $options = array()) {
$class = 'Minify_Controller_' . $type;
if (! class_exists($class, false)) {
require_once "Minify/Controller/{$type}.php";
}
$ctrl = new $class($spec, $options);
if (! self::handleRequest($ctrl)) {
header("HTTP/1.0 400 Bad Request");
exit('400 Bad Request');
}
}
/**
* Handle a request for a minified file.
*
* You must supply a controller object which has the same public API
* as Minify_Controller.
*
* @param Minify_Controller $controller
*
* @return bool successfully sent a 304 or 200 with content
*/
public static function handleRequest($controller) {
if (! $controller->requestIsValid) {
return false;
}
self::$_controller = $controller;
self::_setOptions();
$cgOptions = array(
'lastModifiedTime' => self::$_options['lastModifiedTime']
,'isPublic' => self::$_options['isPublic']
);
if (null !== self::$_options['cacheUntil']) {
$cgOptions['cacheUntil'] = self::$_options['cacheUntil'];
}
// check client cache
require_once 'HTTP/ConditionalGet.php';
$cg = new HTTP_ConditionalGet($cgOptions);
if ($cg->cacheIsValid) {
// client's cache is valid
$cg->sendHeaders();
return true;
}
// client will need output
$headers = $cg->getHeaders();
unset($cg);
// determine encoding
if (self::$_options['encodeOutput']) {
if (self::$_options['encodeMethod'] !== null) {
// controller specifically requested this
$contentEncoding = self::$_options['encodeMethod'];
} else {
// sniff request header
require_once 'HTTP/Encoder.php';
// depending on what the client accepts, $contentEncoding may be
// 'x-gzip' while our internal encodeMethod is 'gzip'
list(self::$_options['encodeMethod'], $contentEncoding) = HTTP_Encoder::getAcceptedEncoding();
}
} else {
self::$_options['encodeMethod'] = ''; // identity (no encoding)
}
if (null !== self::$_cachePath) {
self::_setupCache();
// fetch content from cache file(s).
$content = self::_fetchContent(self::$_options['encodeMethod']);
self::$_cache = null;
} else {
// no cache, just combine, minify, encode
$content = self::_combineMinify();
$content = self::_encode($content);
}
// add headers to those from ConditionalGet
//$headers['Content-Length'] = strlen($content);
$headers['Content-Type'] = (null !== self::$_options['contentTypeCharset'])
? self::$_options['contentType'] . ';charset=' . self::$_options['contentTypeCharset']
: self::$_options['contentType'];
if (self::$_options['encodeMethod'] !== '') {
$headers['Content-Encoding'] = $contentEncoding;
$headers['Vary'] = 'Accept-Encoding';
}
// output headers & content
foreach ($headers as $name => $val) {
header($name . ': ' . $val);
}
echo $content;
return true;
}
/**
* @var mixed null if disk cache is not to be used
*/
private static $_cachePath = null;
/**
* @var Minify_Controller active controller for current request
*/
private static $_controller = null;
/**
* @var array options for current request
*/
private static $_options = null;
/**
* @var Cache_Lite_File cache obj for current request
*/
private static $_cache = null;
/**
* Set class options based on controller's options and defaults
*
* @return null
*/
private static function _setOptions()
{
$given = self::$_controller->options;
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
,'cacheUntil' => null
), $given);
$defaultMinifiers = array(
'text/css' => array('Minify_CSS', 'minify')
,'application/x-javascript' => array('Minify_Javascript', 'minify')
,'text/html' => array('Minify_HTML', 'minify')
);
if (! isset($given['minifiers'])) {
$given['minifiers'] = array();
}
self::$_options['minifiers'] = array_merge($defaultMinifiers, $given['minifiers']);
}
/**
* Fetch encoded content from cache (or generate and store it).
*
* If self::$cacheUnencodedVersion is true and encoded content must be
* generated, this function will call itself recursively to fetch (or
* generate) the minified content. Otherwise, it will always recombine
* and reminify files to generate different encodings.
*
* @param string $encodeMethod
*
* @return string minified, encoded content
*/
private static function _fetchContent($encodeMethod)
{
$cacheId = self::_getCacheId(self::$_controller->sources, self::$_options)
. $encodeMethod;
$content = self::$_cache->get($cacheId, 'Minify');
if (false === $content) {
// must generate
if ($encodeMethod === '') {
// generate identity cache to store
$content = self::_combineMinify();
} else {
// fetch identity cache & encode it to store
if (self::$cacheUnencodedVersion) {
// double layer cache
$content = self::_fetchContent('');
} else {
// recombine
$content = self::_combineMinify();
}
$content = self::_encode($content);
}
self::$_cache->save($content, $cacheId, 'Minify');
}
return $content;
}
/**
* Set self::$_cache to a new instance of Cache_Lite_File (patched 2007-10-03)
*
* @return null
*/
private 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';
self::$_cache = new Cache_Lite_File(array(
'cacheDir' => self::$_cachePath . '/'
,'fileNameProtection' => false
// currently only available in patched Cache_Lite_File
,'masterTime' => self::$_options['lastModifiedTime']
));
}
/**
* Combines sources and minifies the result.
*
* @return string
*/
private static function _combineMinify() {
$type = self::$_options['contentType']; // ease readability
// when combining scripts, make sure all statements separated
$implodeSeparator = ($type === 'application/x-javascript')
? ';'
: '';
// default options and minifier function for all sources
$defaultOptions = isset(self::$_options['perType'][$type])
? self::$_options['perType'][$type]
: array();
$defaultMinifier = isset(self::$_options['minifiers'][$type])
? self::$_options['minifiers'][$type]
: array('Minify', '_trim');
if (Minify_Source::haveNoMinifyPrefs(self::$_controller->sources)) {
// all source have same options/minifier, better performance
foreach (self::$_controller->sources as $source) {
$pieces[] = $source->getContent();
}
$content = implode($implodeSeparator, $pieces);
self::$_controller->loadMinifier($defaultMinifier);
$content = call_user_func($defaultMinifier, $content, $defaultOptions);
} else {
// minify each source with its own options and minifier
foreach (self::$_controller->sources as $source) {
// allow the source to override our minifier and options
$minifier = (null !== $source->minifier)
? $source->minifier
: $defaultMinifier;
$options = (null !== $source->minifyOptions)
? array_merge($defaultOptions, $source->minifyOptions)
: $defaultOptions;
self::$_controller->loadMinifier($minifier);
// get source content and minify it
$pieces[] = call_user_func($minifier, $source->getContent(), $options);
}
$content = implode($implodeSeparator, $pieces);
}
return $content;
}
/**
* Applies HTTP encoding
*
* @param string $content
*
* @return string
*/
private static function _encode($content)
{
if (self::$_options['encodeMethod'] === ''
|| ! self::$_options['encodeOutput']) {
// "identity" encoding
return $content;
}
require_once 'HTTP/Encoder.php';
$encoder = new HTTP_Encoder(array(
'content' => $content
,'method' => self::$_options['encodeMethod']
));
$encoder->encode(self::$_options['encodeLevel']);
return $encoder->getContent();
}
/**
* Make a unique cache id for for this request.
*
* Any settings that could affect output are taken into consideration
*
* @return string
*/
private static function _getCacheId() {
return md5(serialize(array(
Minify_Source::getDigest(self::$_controller->sources)
,self::$_options['minifiers']
,self::$_options['perType']
)));
}
/**
* The default minifier if content-type has no minifier
*
* This is necessary because trim() throws notices when you send in options
* as a 2nd arg.
*
* @param string $content
*
* @return string
*/
private static function _trim($content, $options)
{
return trim($content);
}
}