19

I've done some looking around, but I'm still confused a bit.

I tried Crockford's JSMin, but Win XP can't unzip the executable file for some reason.

What I really want though is a simple and easy-to-use JS minifier that uses PHP to minify JS code--and return the result.

The reason why is because: I have 2 files (for example) that I'm working between: scripts.js and scripts_template.js

scripts_template is normal code that I write out--then I have to minify it and paste the minified script into scripts.js--the one that I actually USE on my website.

I want to eradicate the middle man by simply doing something like this on my page:

<script type="text/javascript" src="scripts.php"></script>

And then for the contents of scripts.php:

<?php include("include.inc"); header("Content-type:text/javascript"); echo(minify_js(file_get_contents("scripts_template.js")));

This way, whenever I update my JS, I don't have to constantly go to a website to minify it and re-paste it into scripts.js--everything is automatically updated.

Yes, I have also tried Crockford's PHP Minifier and I've taken a look at PHP Speedy, but I don't understand PHP classes just yet...Is there anything out there that a monkey could understand, maybe something with RegExp?

How about we make this even simpler?

I just want to remove tab spaces--I still want my code to be readable.

It's not like the script makes my site lag epically, it's just anything is better than nothing.

Tab removal, anyone? And if possible, how about removing completely BLANK lines?

RickyAYoder
  • 963
  • 1
  • 13
  • 29

6 Answers6

28

I have used a PHP implementation of JSMin by Douglas Crockford for quite some time. It can be a little risky when concatenating files, as there may be a missing semicolon on the end of a closure.

It'd be a wise idea to cache the minified output and echo what is cached so long as it's newer than the source file.

require 'jsmin.php';

if(filemtime('scripts_template.js') < filemtime('scripts_template.min.js')) {
  read_file('scripts_template.min.js');
} else {
  $output = JSMin::minify(file_get_contents('scripts_template.js'));
  file_put_contents('scripts_template.min.js', $output);
  echo $output;
}

You could also try JShrink. I haven't ever used it before, since I haven't had difficulty with JSMin before, but this code below should do the trick. I hadn't realized this, but JShrink requires PHP 5.3 and namespaces.

require 'JShrink/Minifier.php';

if(filemtime('scripts_template.js') < filemtime('scripts_template.min.js')) {
  read_file('scripts_template.min.js');
} else {
  $output = \JShrink\Minifier::minify(file_get_contents('scripts_template.js'));
  file_put_contents('scripts_template.min.js', $output);
  echo $output;
}
Robert K
  • 30,064
  • 12
  • 61
  • 79
  • I tried using this, but it doesn't minify the entire code. In fact, it CUTS code halfway through the script, so I'm right in the middle of a for() loop, which would cause the script to be corrupt anyway. – RickyAYoder Jun 12 '12 at 16:33
  • @RickyAYoder Were any notices or errors output? It could be a syntax error in your Javascript. – Robert K Jun 12 '12 at 16:37
  • Nope. When I run the script hand-made and un-minified, there are no errors to be reported. – RickyAYoder Jun 12 '12 at 16:40
  • Any other sources for a minifier? – RickyAYoder Jun 12 '12 at 16:40
  • 1
    Also note that getters/setters aren't commonly supported by these packages unless they say so. If you do have Node.js in your environment, I suggest using [UglifyJS](https://github.com/mishoo/UglifyJS) instead. – Robert K Jun 12 '12 at 17:28
  • 1
    I tested both on one of my source files. jsmin: (46,385 => 26,031 bytes). JShrink: (463,85->26,027 bytes). Very similar performance. However, javascript-minifier.com minified it to 19,526 bytes because it replaces long variable names with shorter versions. – EricP Feb 27 '15 at 21:44
  • @JoeCoder Yes, I don't think that JSMin or JShrink mangle variable names. They may have that option. – Robert K Mar 02 '15 at 14:44
  • missing paranthesis after `filemtime('scripts_template.min.js')`, could not edit it because edits are only accepted if the diff is >= 6 chars. – 0x8BADF00D Aug 03 '15 at 09:58
  • @0x8BADF00D Fixed. That example was typed in the actual edit box, not extracted directly from working code. – Robert K Aug 03 '15 at 13:38
  • The first link in the answer isn't available anymore. There is github project with tons of code. Here is a simple class based on Douglas Crockford code http://cgit.drupalcode.org/javascript_aggregator/tree/jsmin.php?h=6.x-1.x – ymakux Nov 01 '15 at 07:32
4

Take a look at Assetic, a great asset management library in PHP. It is well integrated with Symfony2 and widely used.

https://github.com/kriswallsmith/assetic

Louis-Philippe Huberdeau
  • 5,341
  • 1
  • 19
  • 22
2

Depending on the restrictions of your server (eg, not running in safe mode), perhaps you can also look beyond PHP for a minifier and run it using shell_exec(). For instance, if you can run Java on your server, put a copy of YUI Compressor on the server and use it directly.

Then scripts.php would be something like:

<?php 

  $cmd = "java -cp [path-to-yui-dir] -jar [path-to-yuicompressor.jar] [path-to-scripts_template.js]";

  echo(shell_exec($cmd));

?>

Other suggestion: build the minification step into your development workflow, before you deploy to the server. For example I set up my Eclipse PHP projects to compress JS and CSS files into a "build" folder. Works like a charm.

Kevin Bray
  • 366
  • 1
  • 5
  • 2
    Starting a a JAVA app for a tiny utility seems a huge bloat to me. It would be impossible to fit this solution in a request-response flow as the OP wanted. – karatedog Jul 14 '15 at 18:19
  • 1
    What you're saying is not exclusively true of a Java app. Re-minifying on every request would be a needless expense with any utility. Note [Robert-K's earlier advice](http://stackoverflow.com/a/11000599/1301247) to cache the result, or my "other suggestion", to move it into an automated build step. Now, three years later, there are better options for minifying than YUI anyway. – Kevin Bray Sep 19 '15 at 05:29
2

Using "PHPWee": https://github.com/searchturbine/phpwee-php-minifier (which also uses JSmin), I pushed @Robert K solution a little bit further.

This solution allows minifying both CSS and JS files. If the non-minified file cannot be found, it will return an empty string. If the minified file is older than the non-minified, it will try to create it. It will create a sub-folder for the minified file if it doesn't exist. If the method can minify the file successfully, it returns it either in a <script> (javascript) or a <link> (CSS) tag. Otherwise, the method will return the non-minified version in the proper tag.

Note: tested with PHP 7.0.13

/**
* Try to minify the JS/CSS file. If we are not able to minify,
*   returns the path of the full file (if it exists).
*
* @param $matches Array
*   0 = Full partial path
*   1 = Path without the file
*   2 = File name and extension
*
* @param $fileType Boolean
*   FALSE: css file.
*   TRUE: js file
*
* @return String
*/
private static function createMinifiedFile(array $matches, bool $fileType)
{
    if (strpos($matches[1], 'shared_code') !== false) {

        $path = realpath(dirname(__FILE__)) . str_replace(
            'shared_code',
            '..',
            $matches[1]
        );

    } else {

        $path = realpath(dirname(__FILE__)) .
            "/../../" . $matches[1];
    }

    if (is_file($path . $matches[2])) {

        $filePath = $link = $matches[0];

        $min = 'min/' . str_replace(
            '.',
            '.min.',
            $matches[2]
        );

        if (!is_file($path . $min) or 
            filemtime($path . $matches[2]) > 
            filemtime($path . $min)
        ) {

            if (!is_dir($path . 'min')) {

                mkdir($path . 'min');   
            }

            if ($fileType) { // JS

                $minified = preg_replace(
                        array(
                            '/(\))\R({)/',
                            '/(})\R/'
                        ),
                        array(
                            '$1$2',
                            '$1'
                        ),
                        Minify::js(
                        (string) file_get_contents(
                            $path . $matches[2]
                        )
                    )
                );

            } else { // CSS

                $minified = preg_replace(
                    '@/\*(?:[\r\s\S](?!\*/))+\R?\*/@', //deal with multiline comments
                    '',
                    Minify::css(
                        (string) file_get_contents(
                            $path . $matches[2]
                        )
                    )
                );
            }

            if (!empty($minified) and file_put_contents(
                    $path . $min, 
                    $minified 
                )
            ) {

                $filePath = $matches[1] . $min;
            }

        } else { // up-to-date

            $filePath = $matches[1] . $min;
        }

    } else { // full file doesn't exists

        $filePath = "";
    }

    return $filePath;
}

/**
* Return the minified version of a CSS file (must end with the .css extension).
*   If the minified version of the file is older than the full CSS file,
*   the CSS file will be shrunk.
*
*   Note: An empty string will be return if the CSS file doesn't exist.
*
*   Note 2: If the file exists, but the minified file cannot be created, 
*       we will return the path of the full file.
*
* @link https://github.com/searchturbine/phpwee-php-minifier Source
*
* @param $path String name or full path to reach the CSS file.
*   If only the file name is specified, we assume that you refer to the shared path.
*
* @return String
*/
public static function getCSSMin(String $path)
{
    $link = "";
    $matches = array();

    if (preg_match(
            '@^(/[\w-]+/view/css/)?([\w-]+\.css)$@',
            $path,
            $matches
        )
    ) {

        if (empty($matches[1])) { // use the default path

            $matches[1] = self::getCssPath();

            $matches[0] = $matches[1] . $matches[2];
        }

        $link = self::createMinifiedFile($matches, false);

    } else {

        $link = "";
    }

    return (empty($link) ?
        '' :
        '<link rel="stylesheet" href="' . $link . '">'
    );
}

/**
* Return the path to fetch CSS sheets.
* 
* @return String
*/
public static function getCssPath()
{
    return '/shared_code/css/' . self::getCurrentCSS() . "/";
}

/**
* Return the minified version of a JS file (must end with the .css extension).
*   If the minified version of the file is older than the full JS file,
*   the JS file will be shrunk.
*
*   Note: An empty string will be return if the JS file doesn't exist.
*
*   Note 2: If the file exists, but the minified file cannot be created, 
*       we will return the path of the full file.
*
* @link https://github.com/searchturbine/phpwee-php-minifier Source
*
* @param $path String name or full path to reach the js file.
*
* @return String
*/
public static function getJSMin(String $path)
{
    $matches = array();

    if (preg_match(
            '@^(/[\w-]+(?:/view)?/js/)([\w-]+\.js)$@',
            $path,
            $matches
        )
    ) {
        $script = self::createMinifiedFile($matches, true);

    } else {

        $script = "";
    }

    return (empty($script) ? 
        '' :
        '<script src="' . $script . '"></script>'
    );
}

In a (Smarty) template, you might use those methods like this:

{$PageController->getCSSMin("main_frame.css")}
//Output: <link rel="stylesheet" href="/shared_code/css/default/min/main_frame.min.css">

{$PageController->getCSSMin("/gem-mechanic/view/css/gem_mechanic.css")}
//Output: <link rel="stylesheet" href="/gem-mechanic/view/css/min/gem_mechanic.min.css">

{$PageController->getJSMin("/shared_code/js/control_utilities.js")}
//Output: <script src="/shared_code/js/min/control_utilities.min.js"></script>

{$PageController->getJSMin("/PC_administration_interface/view/js/error_log.js")}
//Output: <script src="/PC_administration_interface/view/js/min/error_log.min.js"></script>

Unit tests:

/**
* Test that we can minify CSS files successfully.
*/
public function testGetCSSMin()
{
    //invalid style
    $this->assertEmpty(
        PageController::getCSSMin('doh!!!')
    );


    //shared style
    $path = realpath(dirname(__FILE__)) . '/../css/default/min/main_frame.min.css';

    if (is_file($path)) {

        unlink ($path);
    }

    $link = PageController::getCSSMin("main_frame.css");

    $this->assertNotEmpty($link);

    $this->assertEquals(
        '<link rel="stylesheet" href="/shared_code/css/default/min/main_frame.min.css">',
        $link
    );

    $this->validateMinifiedFile($path);


    //project style
    $path = realpath(dirname(__FILE__)) . '/../../gem-mechanic/view/css/min/gem_mechanic.min.css';

    if (is_file($path)) {

        unlink ($path);
    }

    $link = PageController::getCSSMin("/gem-mechanic/view/css/gem_mechanic.css");

    $this->assertNotEmpty($link);

    $this->assertEquals(
        '<link rel="stylesheet" href="/gem-mechanic/view/css/min/gem_mechanic.min.css">',
        $link
    );

    $this->validateMinifiedFile($path);
}

/**
* Test that we can minify JS files successfully.
*/
public function testGetJSMin()
{
    //invalid script
    $this->assertEmpty(
        PageController::getJSMin('doh!!!')
    );


    //shared script
    $path = realpath(dirname(__FILE__)) . '/../js/min/control_utilities.min.js';

    if (is_file($path)) {

        unlink ($path);
    }

    $script = PageController::getJSMin("/shared_code/js/control_utilities.js");

    $this->assertNotEmpty($script);

    $this->assertEquals(
        '<script src="/shared_code/js/min/control_utilities.min.js"></script>',
        $script
    );

    $this->validateMinifiedFile($path);


    //project script
    $path = realpath(dirname(__FILE__)) . '/../../PC_administration_interface/view/js/min/error_log.min.js';

    if (is_file($path)) {

        unlink ($path);
    }

    $script = PageController::getJSMin("/PC_administration_interface/view/js/error_log.js");

    $this->assertNotEmpty($script);

    $this->assertEquals(
        '<script src="/PC_administration_interface/view/js/min/error_log.min.js"></script>',
        $script
    );

    $this->validateMinifiedFile($path);
}

/**
* Make sure that the minified file exists and that its content is valid.
*
* @param $path String the path to reach the file
*/
private function validateMinifiedFile(string $path)
{
    $this->assertFileExists($path);

    $content = (string) file_get_contents($path);

    $this->assertNotEmpty($content);

    $this->assertNotContains('/*', $content);

    $this->assertEquals(
        0,
        preg_match(
            '/\R/',
            $content
        )
    );
}

Additional notes:

  1. In phpwee.php I had to replace <? by <?php.
  2. I had problems with the namespace (the function class_exists() was not able to find the classes even though they were in the same file). I solved this problem by removing the namespace in every file.
0

JavaScriptPacker works since 2008, and is quite simple

0

I know this question is really old, but after having some trouble with newer syntax and older formatting solutions, I came up with this PHP function that removes comments and unnecessary spaces in a JS string.

For this to work, the JS code needs to have semicolons after function definitions, and this won't work if there's a regular expression with two forward slashes (//) in it. I'm open to ideas on how to detect that, haven't come up with anything yet.


//echo a minified version of the $source JavaScript
function echo_minified($source){

    //a list of characters that don't need spaces around them
    $NO_SPACE_NEAR = ' +=-*/%&|^!~?:;,.<>(){}[]';

    //loop through each line of the source, removing comments and unnecessary whitespace
    $lines = explode("\n", $source);

    //keep track of whether we're in a string or not
    $in_string = false;

    //keep track of whether we're in a comment or not
    $multiline_comment = false;

    foreach($lines as $line){

        //remove whitespace from the start and end of the line
        $line = trim($line);

        //skip blank lines
        if($line == '') continue;

        //remove "use strict" statements
        if(!$in_string && str_starts_with($line, '"use strict"')) continue;

        //loop through the current line
        $string_len = strlen($line);

        for($position = 0; $position < $string_len; $position++){
            //if currently in a string, check if the string ended (making sure to ignore escaped quotes)
            if($in_string && $line[$position] === $in_string && ($position < 1 || $line[$position - 1] !== '\\')){
                $in_string = false;
            }
            else if($multiline_comment){
                //check if this is the end of a multiline comment
                if($position > 0 && $line[$position] === "/" && $line[$position - 1] === "*"){
                    $multiline_comment = false;
                }
                continue;
            }
            //check everything else
            else if(!$in_string && !$multiline_comment){

                //check if this is the start of a string
                if($line[$position] == '"' || $line[$position] == "'" || $line[$position] == '`'){
                    //record the type of string
                    $in_string = $line[$position];
                } 

                //check if this is the start of a single-line comment
                else if($position < $string_len - 1 && $line[$position] == '/' && $line[$position + 1] == '/'){
                    //this is the start of a single line comment, so skip the rest of the line
                    break;
                }

                //check if this is the start of a multiline comment
                else if($position < $string_len - 1 && $line[$position] == '/' && $line[$position + 1] == '*'){
                    $multiline_comment = true;
                    continue;
                }

                else if(
                        $line[$position] == ' ' && (
                            //if this is not the first character, check if the character before it requires a space
                            ($position > 0 && strpos($NO_SPACE_NEAR, $line[$position - 1]) !== false) 
                            //if this is not the last character, check if the character after it requires a space
                            || ($position < $string_len - 1 && strpos($NO_SPACE_NEAR, $line[$position + 1]) !== false)
                        )
                    ){
                    //there is no need for this space, so keep going
                    continue;
                }
            }

            //print the current character and continue
            echo $line[$position];
        }

        //if this is a multi-line string, preserve the line break
        if($in_string){
            echo "\\n";
        }
    }
}
maxpelic
  • 161
  • 11