Browse Source

Code formatting

tags/v3.1.0
Cédric Belin 2 months ago
parent
commit
d476f3157a
31 changed files with 821 additions and 814 deletions
  1. +6
    -3
      .editorconfig
  2. +1
    -1
      .github/workflows/build.yaml
  3. +5
    -5
      .vscode/settings.json
  4. +9
    -9
      RoboFile.php
  5. +58
    -58
      composer.json
  6. +1
    -1
      doc/index.md
  7. +5
    -5
      doc/installation.md
  8. +48
    -48
      doc/usage.md
  9. +9
    -5
      etc/mkdocs.yaml
  10. +22
    -22
      etc/phpdoc.xml
  11. +6
    -6
      etc/phpstan.neon
  12. +13
    -13
      etc/phpunit.xml
  13. +15
    -15
      example/RoboFile.php
  14. +84
    -84
      src/FastTransformer.php
  15. +188
    -188
      src/Minifier.php
  16. +24
    -24
      src/SafeTransformer.php
  17. +48
    -48
      src/Server.php
  18. +9
    -9
      src/Tasks.php
  19. +5
    -5
      src/TransformMode.php
  20. +8
    -8
      src/Transformer.php
  21. +1
    -1
      src/index.php
  22. +87
    -87
      test/FastTransformerTest.php
  23. +45
    -45
      test/MinifierTest.php
  24. +44
    -44
      test/SafeTransformerTest.php
  25. +42
    -42
      test/ServerTest.php
  26. +22
    -22
      test/fixtures/sample.php
  27. +3
    -3
      tool/clean.ps1
  28. +1
    -1
      tool/doc.ps1
  29. +1
    -1
      tool/lint.ps1
  30. +1
    -1
      tool/upgrade.ps1
  31. +10
    -10
      tool/watch.ps1

+ 6
- 3
.editorconfig View File

@@ -3,11 +3,14 @@ root = true

[*]
charset = utf-8
indent_size = 2
indent_style = space
indent_style = tab
insert_final_newline = true
tab_width = 2
trim_trailing_whitespace = true

[*.md]
indent_size = 4
trim_trailing_whitespace = false

[*.{yaml,yml}]
indent_size = 2
indent_style = space

+ 1
- 1
.github/workflows/build.yaml View File

@@ -3,7 +3,7 @@ on:
pull_request:
push:
schedule:
- cron: '0 0 1 * *'
- cron: "0 0 1 * *"
jobs:
test:
runs-on: ubuntu-latest


+ 5
- 5
.vscode/settings.json View File

@@ -1,7 +1,7 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
"editor.insertSpaces": false,
"editor.tabSize": 2,
"files.encoding": "utf8",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true
}

+ 9
- 9
RoboFile.php View File

@@ -4,17 +4,17 @@ use Robo\{Result, Tasks};
use Robo\PhpMinify\{TransformMode};

// Load the dependencies.
require_once __DIR__.'/vendor/autoload.php';
require_once __DIR__."/vendor/autoload.php";

/** Provides tasks for the build system. */
class RoboFile extends Tasks {
use \Robo\PhpMinify\Tasks;
use \Robo\PhpMinify\Tasks;

/**
* Builds the project.
* @return Result The task result.
*/
function build(): Result {
return $this->taskPhpMinify('src')->mode(TransformMode::fast)->to('build')->run();
}
/**
* Builds the project.
* @return Result The task result.
*/
function build(): Result {
return $this->taskPhpMinify("src")->mode(TransformMode::fast)->to("build")->run();
}
}

+ 58
- 58
composer.json View File

@@ -1,60 +1,60 @@
{
"description": "Robo plug-in minifying PHP source code by removing comments and whitespace.",
"homepage": "https://docs.belin.io/robo-php-minify",
"license": "MIT",
"name": "cedx/robo-php-minify",
"type": "robo-tasks",
"version": "3.0.0",
"authors": [
{"email": "cedric@belin.io", "homepage": "https://belin.io", "name": "Cédric Belin"}
],
"autoload": {
"psr-4": {"Robo\\PhpMinify\\": "src/"}
},
"autoload-dev": {
"psr-4": {"Robo\\PhpMinify\\": "test/"}
},
"config": {
"optimize-autoloader": true
},
"funding": [
{"type": "patreon", "url": "https://www.patreon.com/cedx"}
],
"keywords": [
"build",
"compress",
"minify",
"php",
"robo",
"task"
],
"require": {
"php": ">=7.4.0",
"ext-mbstring": "*",
"ext-spl": "*",
"cedx/enum": "^8.2.0",
"cedx/which": "^9.0.0",
"consolidation/robo": "^2.0.3",
"nyholm/psr7": "^1.2.1",
"psr/http-client": "^1.0.0",
"symfony/finder": "^4.4.8 || ^5.0.8",
"symfony/http-client": "^5.0.8",
"symfony/process": "^4.4.8 || ^5.0.8",
"webmozart/path-util": "^2.3.0"
},
"require-dev": {
"cedx/coveralls": "^13.0.0",
"league/container": "^2.4.1 || ^3.3.0",
"phpstan/phpstan": "^0.12.25",
"phpunit/phpunit": "^9.1.4",
"symfony/console": "^4.4.8 || ^5.0.8"
},
"scripts": {
"coverage": "coveralls var/coverage.xml",
"test": "phpunit --configuration=etc/phpunit.xml"
},
"support": {
"docs": "https://docs.belin.io/robo-php-minify/api",
"issues": "https://git.belin.io/cedx/robo-php-minify/issues"
}
"description": "Robo plug-in minifying PHP source code by removing comments and whitespace.",
"homepage": "https://docs.belin.io/robo-php-minify",
"license": "MIT",
"name": "cedx/robo-php-minify",
"type": "robo-tasks",
"version": "3.0.0",
"authors": [
{"email": "cedric@belin.io", "homepage": "https://belin.io", "name": "Cédric Belin"}
],
"autoload": {
"psr-4": {"Robo\\PhpMinify\\": "src/"}
},
"autoload-dev": {
"psr-4": {"Robo\\PhpMinify\\": "test/"}
},
"config": {
"optimize-autoloader": true
},
"funding": [
{"type": "patreon", "url": "https://www.patreon.com/cedx"}
],
"keywords": [
"build",
"compress",
"minify",
"php",
"robo",
"task"
],
"require": {
"php": ">=7.4.0",
"ext-mbstring": "*",
"ext-spl": "*",
"cedx/enum": "^8.2.0",
"cedx/which": "^9.0.0",
"consolidation/robo": "^2.0.3",
"nyholm/psr7": "^1.2.1",
"psr/http-client": "^1.0.0",
"symfony/finder": "^4.4.8 || ^5.0.8",
"symfony/http-client": "^5.0.8",
"symfony/process": "^4.4.8 || ^5.0.8",
"webmozart/path-util": "^2.3.0"
},
"require-dev": {
"cedx/coveralls": "^13.0.0",
"league/container": "^2.4.1 || ^3.3.0",
"phpstan/phpstan": "^0.12.25",
"phpunit/phpunit": "^9.1.4",
"symfony/console": "^4.4.8 || ^5.0.8"
},
"scripts": {
"coverage": "coveralls var/coverage.xml",
"test": "phpunit --configuration=etc/phpunit.xml"
},
"support": {
"docs": "https://docs.belin.io/robo-php-minify/api",
"issues": "https://git.belin.io/cedx/robo-php-minify/issues"
}
}

+ 1
- 1
doc/index.md View File

@@ -8,7 +8,7 @@
## Quick start
Install the latest version of **Robo-PHP-Minify** with [Composer](https://getcomposer.org):

```shell
``` shell
composer require cedx/robo-php-minify
```



+ 5
- 5
doc/installation.md View File

@@ -6,7 +6,7 @@ and [Composer](https://getcomposer.org), the PHP package manager, up and running

You can verify if you're already good to go with the following commands:

```shell
``` shell
php --version
# PHP 7.4.5 (cli) (built: Apr 14 2020 16:17:19) ( NTS Visual C++ 2017 x64 )

@@ -15,22 +15,22 @@ composer --version
```

!!! info
If you plan to play with the package sources, you will also need the latest versions of
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).
If you plan to play with the package sources, you will also need the latest versions of
[PowerShell](https://docs.microsoft.com/en-us/powershell) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material).

## Installing with Composer package manager

### 1. Install it
From a command prompt, run:

```shell
``` shell
composer require cedx/robo-php-minify
```

### 2. Import it
Now in your [PHP](https://www.php.net) code, you can use:

```php
``` php
<?php
use Robo\PhpMinify\{Tasks, TransformMode};
```

+ 48
- 48
doc/usage.md View File

@@ -3,30 +3,30 @@ If you haven't used [Robo](https://robo.li) before, be sure to check out the [re

## Programming interface
The plug-in provides a single task, `PhpMinify`, that takes a list of [PHP](https://www.php.net) scripts as input, and remove the comments and whitespace in these files by applying the [`php_strip_whitespace()`](https://www.php.net/manual/en/function.php-strip-whitespace.php) function on their contents.
### **taskPhpMinify**(mixed $patterns)
Minifies the PHP scripts corresponding to the specified file patterns, and saves the resulting output to a destination directory specified using the `to()` method:

```php
``` php
<?php
use Robo\{Result, Tasks};

class RoboFile extends Tasks {
use \Robo\PhpMinify\Tasks;
use \Robo\PhpMinify\Tasks;

function compressPhp(): Result {
return $this->taskPhpMinify('path/to/src/*.php')
->to('path/to/out')
->run();
}
function compressPhp(): Result {
return $this->taskPhpMinify("path/to/src/*.php")
->to("path/to/out")
->run();
}
}
```

The file patterns use the same syntax as the [Symfony Finder component](https://symfony.com/doc/current/components/finder.html) (for example: `"path/*/to/*/*/src"`).

!!! tip
You can provide several file patterns to the `taskPhpMinify()` method:
the `$patterns` parameter can be a `string` (single pattern) or an array of `string` (multiple patterns).
You can provide several file patterns to the `taskPhpMinify()` method:
the `$patterns` parameter can be a `string` (single pattern) or an array of `string` (multiple patterns).

## Options
The `PhpMinify` task also support the following options:
@@ -36,25 +36,25 @@ When writing the minified files to the destination directory, the task tries to

If the resulting file tree does not meet your expectations, or if you want to customize it, you can use the `base` option. It is treated as a base path that is stripped from the computed path of the destination files:

```php
``` php
<?php
use Robo\{Tasks};

class RoboFile extends Tasks {
use \Robo\PhpMinify\Tasks;
use \Robo\PhpMinify\Tasks;

function compressPhp(): void {
// Given the script "src/subdir/script.php"...
function compressPhp(): void {
// Given the script "src/subdir/script.php"...

$this->taskPhpMinify('src/*.php')->to('out1')->run();
// ...will probably create the file "out1/subdir/script.php".
$this->taskPhpMinify("src/*.php")->to("out1")->run();
// ...will probably create the file "out1/subdir/script.php".

$this->taskPhpMinify('src/*.php')->base('.')->to('out2')->run();
// ...will create the file "out2/src/subdir/script.php".
$this->taskPhpMinify("src/*.php")->base(".")->to("out2")->run();
// ...will create the file "out2/src/subdir/script.php".

$this->taskPhpMinify('src/*.php')->base('src/subdir')->to('out3')->run();
// ...will create the file "out3/script.php".
}
$this->taskPhpMinify("src/*.php")->base("src/subdir")->to("out3")->run();
// ...will create the file "out3/script.php".
}
}
```

@@ -63,19 +63,19 @@ The `PhpMinify` task relies on the availability of the [PHP](https://www.php.net

If you want to use a different one, you can provide the path to the `php` executable by using the `binary` option:

```php
``` php
<?php
use Robo\{Result, Tasks};

class RoboFile extends Tasks {
use \Robo\PhpMinify\Tasks;
function compressPhp(): Result {
return $this->taskPhpMinify('src/*.php')
->binary('C:\\Program Files\\PHP\\php.exe')
->to('out')
->run();
}
use \Robo\PhpMinify\Tasks;
function compressPhp(): Result {
return $this->taskPhpMinify("src/*.php")
->binary("C:\\Program Files\\PHP\\php.exe")
->to("out")
->run();
}
}
```

@@ -85,38 +85,38 @@ The `PhpMinify` task can work in two manners, which can be selected using the `m
- the `safe` mode: as its name implies, this mode is very reliable. But it is also very slow as it spawns a new PHP process for every file to be processed. This is the default mode.
- the `fast` mode: as its name implies, this mode is very fast, but it is not very reliable. It spawns a PHP web server that processes the input files, but on some systems this fails.

```php
``` php
<?php
use Robo\{Result, Tasks};
use Robo\PhpMinify\{TransformMode};

class RoboFile extends Tasks {
use \Robo\PhpMinify\Tasks;
function compressPhp(): Result {
return $this->taskPhpMinify('src/*.php')
->mode(TransformMode::fast)
->to('out')
->run();
}
use \Robo\PhpMinify\Tasks;
function compressPhp(): Result {
return $this->taskPhpMinify("src/*.php")
->mode(TransformMode::fast)
->to("out")
->run();
}
}
```

### **silent**(bool $value = `false`)
By default, the `PhpMinify` task prints to the standard output the paths of the minified scripts. You can disable this output by setting the `silent` option.

```php
``` php
<?php
use Robo\{Result, Tasks};

class RoboFile extends Tasks {
use \Robo\PhpMinify\Tasks;
function compressPhp(): Result {
return $this->taskPhpMinify('src/*.php')
->silent()
->to('out')
->run();
}
use \Robo\PhpMinify\Tasks;
function compressPhp(): Result {
return $this->taskPhpMinify("src/*.php")
->silent()
->to("out")
->run();
}
}
```

+ 9
- 5
etc/mkdocs.yaml View File

@@ -8,19 +8,23 @@ site_dir: ../www

repo_name: git.belin.io
repo_url: https://git.belin.io/cedx/robo-php-minify
edit_uri: ''
edit_uri: ""

copyright: Copyright &copy; 2019 - 2020 Cédric Belin
extra:
social:
- icon: fontawesome/solid/globe
link: 'https://belin.io'
link: "https://belin.io"
name: Belin.io
- icon: fontawesome/brands/github
link: 'https://github.com/cedx'
link: "https://github.com/cedx"
name: GitHub
- icon: fontawesome/brands/twitter
link: 'https://twitter.com/cedxbelin'
link: "https://twitter.com/cedxbelin"
name: Twitter
- icon: fontawesome/brands/linkedin
link: 'https://linkedin.com/in/cedxbelin'
link: "https://linkedin.com/in/cedxbelin"
name: LinkedIn

markdown_extensions:
- admonition


+ 22
- 22
etc/phpdoc.xml View File

@@ -1,25 +1,25 @@
<?xml version="1.0"?>
<phpdocumentor
configVersion="3"
xmlns="http://www.phpdoc.org"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://docs.phpdoc.org/latest/phpdoc.xsd">
<title>Robo-PHP-Minify</title>
<paths>
<cache>../var/phpdoc</cache>
<output>../doc/api</output>
</paths>
<version number="3.0.0">
<api>
<markers>
<marker>TODO</marker>
</markers>
<source dsn="..">
<path>src</path>
</source>
<visibility>protected</visibility>
<visibility>public</visibility>
</api>
</version>
configVersion="3"
xmlns="http://www.phpdoc.org"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://docs.phpdoc.org/latest/phpdoc.xsd">
<title>Robo-PHP-Minify</title>
<paths>
<cache>../var/phpdoc</cache>
<output>../doc/api</output>
</paths>
<version number="3.0.0">
<api>
<markers>
<marker>TODO</marker>
</markers>
<source dsn="..">
<path>src</path>
</source>
<visibility>protected</visibility>
<visibility>public</visibility>
</api>
</version>
</phpdocumentor>

+ 6
- 6
etc/phpstan.neon View File

@@ -1,7 +1,7 @@
parameters:
checkMissingIterableValueType: false
excludes_analyse: [../test/fixtures]
ignoreErrors: ['/Call to an undefined method Robo\\Collection\\CollectionBuilder/']
level: max
paths: [../src, ../test]
treatPhpDocTypesAsCertain: false
checkMissingIterableValueType: false
excludes_analyse: [../test/fixtures]
ignoreErrors: ['/Call to an undefined method Robo\\Collection\\CollectionBuilder/']
level: max
paths: [../src, ../test]
treatPhpDocTypesAsCertain: false

+ 13
- 13
etc/phpunit.xml View File

@@ -1,18 +1,18 @@
<?xml version="1.0"?>
<phpunit bootstrap="../vendor/autoload.php" cacheResult="false" testdox="true">
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../src</directory>
</whitelist>
</filter>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../src</directory>
</whitelist>
</filter>

<logging>
<log type="coverage-clover" target="../var/coverage.xml"/>
</logging>
<logging>
<log type="coverage-clover" target="../var/coverage.xml"/>
</logging>

<testsuites>
<testsuite name="all">
<directory suffix="Test.php">../test</directory>
</testsuite>
</testsuites>
<testsuites>
<testsuite name="all">
<directory suffix="Test.php">../test</directory>
</testsuite>
</testsuites>
</phpunit>

+ 15
- 15
example/RoboFile.php View File

@@ -4,23 +4,23 @@ use Robo\{Result, Tasks};
use Robo\PhpMinify\{TransformMode};

// Load the dependencies.
require_once __DIR__.'/vendor/autoload.php';
require_once __DIR__."/vendor/autoload.php";

/** Provides tasks for the build system. */
class RoboFile extends Tasks {
use \Robo\PhpMinify\Tasks;
use \Robo\PhpMinify\Tasks;

/**
* Compresses a given set of PHP scripts.
* @return Result The task result.
*/
function compressPhp(): Result {
$isWindows = PHP_OS_FAMILY == 'Windows';
return $this->taskPhpMinify('path/to/src/*.php')
->binary($isWindows ? 'C:\\Program Files\\PHP\\php.exe' : '/usr/bin/php')
->mode($isWindows ? TransformMode::safe : TransformMode::fast)
->silent(stream_isatty(STDOUT))
->to('path/to/out')
->run();
}
/**
* Compresses a given set of PHP scripts.
* @return Result The task result.
*/
function compressPhp(): Result {
$isWindows = PHP_OS_FAMILY == "Windows";
return $this->taskPhpMinify("path/to/src/*.php")
->binary($isWindows ? "C:\\Program Files\\PHP\\php.exe" : "/usr/bin/php")
->mode($isWindows ? TransformMode::safe : TransformMode::fast)
->silent(stream_isatty(STDOUT))
->to("path/to/out")
->run();
}
}

+ 84
- 84
src/FastTransformer.php View File

@@ -7,88 +7,88 @@ use Symfony\Component\Process\{Process};
/** Removes comments and whitespace from a PHP script, by calling a Web service. */
class FastTransformer implements Transformer {

/** @var string The address that the server is listening on. */
const address = '127.0.0.1';
/** @var \SplFileInfo The path to the PHP executable. */
private \SplFileInfo $executable;
/** @var Psr18Client The HTTP client. */
private Psr18Client $http;
/** @var int The port that the PHP process is listening on. */
private int $port = -1;
/** @var Process|null The underlying PHP process. */
private ?Process $process = null;
/**
* Creates a new safe transformer.
* @param \SplFileInfo|null $executable The path to the PHP executable.
*/
function __construct(?\SplFileInfo $executable = null) {
$this->executable = $executable ?? new \SplFileInfo('php');
$this->http = new Psr18Client;
}
/** Closes this transformer and releases any resources associated with it. */
function close(): void {
if (!$this->isListening()) return;
/** @var Process $process */
$process = $this->process;
$process->stop();
$this->process = null;
}
/**
* Gets a value indicating whether the PHP process is currently listening.
* @return bool `true` if the PHP process is currently listening, otherwise `false`.
*/
function isListening(): bool {
return (bool) $this->process;
}
/**
* Starts the underlying PHP process: begins accepting connections.
* @return int The port used by the PHP process.
*/
function listen(): int {
if (!$this->isListening()) {
$address = static::address;
$this->port = $this->getPort();
$this->process = new Process([$this->executable->getPathname(), '-S', "$address:{$this->port}", '-t', __DIR__]);
$this->process->start();
sleep(1);
}
return $this->port;
}
/**
* Processes a PHP script.
* @param \SplFileInfo $script The path to the PHP script.
* @return string The transformed script.
*/
function transform(\SplFileInfo $script): string {
$address = static::address;
$file = rawurlencode((string) $script->getRealPath());
$request = $this->http->createRequest('GET', "http://$address:{$this->listen()}/?file=$file");
return $this->http->sendRequest($request)->getBody()->getContents();
}
/**
* Gets an ephemeral port chosen by the system.
* @return int A port that the server can listen on.
* @throws \RuntimeException The socket could not be created.
*/
private function getPort(): int {
$address = static::address;
$socket = stream_socket_server("tcp://$address:0");
if (!$socket) throw new \RuntimeException('The socket could not be created.');
$parts = explode(':', stream_socket_get_name($socket, false));
fclose($socket);
return (int) end($parts);
}
/** @var string The address that the server is listening on. */
const address = "127.0.0.1";
/** @var \SplFileInfo The path to the PHP executable. */
private \SplFileInfo $executable;
/** @var Psr18Client The HTTP client. */
private Psr18Client $http;
/** @var int The port that the PHP process is listening on. */
private int $port = -1;
/** @var Process|null The underlying PHP process. */
private ?Process $process = null;
/**
* Creates a new safe transformer.
* @param \SplFileInfo|null $executable The path to the PHP executable.
*/
function __construct(?\SplFileInfo $executable = null) {
$this->executable = $executable ?? new \SplFileInfo("php");
$this->http = new Psr18Client;
}
/** Closes this transformer and releases any resources associated with it. */
function close(): void {
if (!$this->isListening()) return;
/** @var Process $process */
$process = $this->process;
$process->stop();
$this->process = null;
}
/**
* Gets a value indicating whether the PHP process is currently listening.
* @return bool `true` if the PHP process is currently listening, otherwise `false`.
*/
function isListening(): bool {
return (bool) $this->process;
}
/**
* Starts the underlying PHP process: begins accepting connections.
* @return int The port used by the PHP process.
*/
function listen(): int {
if (!$this->isListening()) {
$address = static::address;
$this->port = $this->getPort();
$this->process = new Process([$this->executable->getPathname(), "-S", "$address:{$this->port}", "-t", __DIR__]);
$this->process->start();
sleep(1);
}
return $this->port;
}
/**
* Processes a PHP script.
* @param \SplFileInfo $script The path to the PHP script.
* @return string The transformed script.
*/
function transform(\SplFileInfo $script): string {
$address = static::address;
$file = rawurlencode((string) $script->getRealPath());
$request = $this->http->createRequest("GET", "http://$address:{$this->listen()}/?file=$file");
return $this->http->sendRequest($request)->getBody()->getContents();
}
/**
* Gets an ephemeral port chosen by the system.
* @return int A port that the server can listen on.
* @throws \RuntimeException The socket could not be created.
*/
private function getPort(): int {
$address = static::address;
$socket = stream_socket_server("tcp://$address:0");
if (!$socket) throw new \RuntimeException("The socket could not be created.");
$parts = explode(":", stream_socket_get_name($socket, false));
fclose($socket);
return (int) end($parts);
}
}

+ 188
- 188
src/Minifier.php View File

@@ -12,192 +12,192 @@ use function Which\{which};
/** Removes PHP comments and whitespace by applying the `php_strip_whitespace()` function. */
class Minifier extends BaseTask implements TaskInterface {

/** @var \SplFileInfo|null The base path that is stripped from the computed path of the destination files. */
private ?\SplFileInfo $base = null;
/** @var \SplFileInfo|null The path to the PHP executable. */
private ?\SplFileInfo $binary = null;
/** @var \SplFileInfo|null The path of the destination directory. */
private ?\SplFileInfo $destination = null;
/** @var string The transform mode. */
private string $mode = TransformMode::safe;
/** @var bool Value indicating whether to silent the minifier output. */
private bool $silent = false;
/** @var int The number of progress steps. */
private int $steps = 0;
/** @var string[] The file patterns of the input scripts. */
private array $sources;
/**
* Creates a new minifier.
* @param string|string[] $patterns The file patterns corresponding to the input scripts.
*/
function __construct($patterns) {
assert(is_string($patterns) || is_array($patterns));
$this->sources = is_array($patterns) ? $patterns : [$patterns];
}
/**
* Sets the base path that is stripped from the computed path of the destination files.
* @param string $path The new base path.
* @return $this This instance.
*/
function base(string $path): self {
assert(mb_strlen($path) > 0);
$this->base = new \SplFileInfo(str_replace('/', DIRECTORY_SEPARATOR, Path::canonicalize($path)));
return $this;
}
/**
* Sets the path to the PHP executable.
* @param string $executable The new executable path.
* @return $this This instance.
*/
function binary(string $executable): self {
assert(mb_strlen($executable) > 0);
$this->binary = new \SplFileInfo(str_replace('/', DIRECTORY_SEPARATOR, Path::canonicalize($executable)));
return $this;
}
/**
* Sets a value indicating the type of transformation applied by this minifier.
* @param string $transformMode The transform mode.
* @return $this This instance.
*/
function mode(string $transformMode): self {
$this->mode = TransformMode::coerce($transformMode, TransformMode::safe);
return $this;
}
/**
* Returns the number of progress steps.
* @return int The number of progress steps.
*/
public function progressIndicatorSteps(): int {
return $this->steps;
}
/**
* Runs this task.
* @return Result The task result.
*/
function run(): Result {
if (!$this->destination) return Result::error($this, 'The destination directory is undefined.');
try { $files = $this->findFiles(); }
catch (DirectoryNotFoundException $e) { return Result::fromException($this, $e); }
if (!$files) {
$message = 'No PHP files to minify.';
$this->printTaskSuccess($message);
return Result::success($this, $message);
}
$count = $this->minifyFiles($files);
$context = ['count' => $count, 'total' => $this->steps, 'destination' => $this->destination->getPathname()];
$fileLabel = $this->steps <= 1 ? 'file' : 'files';
$message = "Minified {count} out of {total} PHP $fileLabel into {destination}";
if ($count != $this->steps) return Result::error($this, $message, $context);
$this->printTaskSuccess($message, $context);
return Result::success($this, $message, $context);
}
/**
* Sets a value indicating whether to silent the minifier output.
* @param bool $value `true` to silent the minifier output, otherwise `false`.
* @return $this This instance.
*/
function silent(bool $value = true): self {
$this->silent = $value;
return $this;
}
/**
* Sets the path of the output directory.
* @param string $destination The destination directory for the minified scripts.
* @return $this This instance.
*/
function to(string $destination): self {
assert(mb_strlen($destination) > 0);
$this->destination = new \SplFileInfo(str_replace('/', DIRECTORY_SEPARATOR, Path::canonicalize($destination)));
return $this;
}
/**
* Creates a transformer corresponding to the input mode.
* @return Transformer The transformer corresponding to the input mode.
*/
private function createTransformer(): Transformer {
if ($this->binary) $binary = $this->binary;
else {
/** @var string $executable */
$executable = which('php', false, fn() => 'php');
$binary = new \SplFileInfo($executable);
}
return $this->mode == TransformMode::fast ? new FastTransformer($binary) : new SafeTransformer($binary);
}
/**
* Finds the files corresponding to the input pattern.
* @return \SplFileInfo[] The found files.
* @throws DirectoryNotFoundException The directory corresponding to the input pattern is not found.
*/
private function findFiles(): array {
$files = [];
foreach ($this->sources as $source) {
$finder = (new Finder)->files()->followLinks();
$hasDirectory = mb_strpos($source, '/') === false && mb_strpos($source, DIRECTORY_SEPARATOR) === false;
$pattern = new \SplFileInfo($hasDirectory ? $source : "./$source");
if ($pattern->isDir()) $finder->in($pattern->getPathname())->name('*.php');
else $finder->in($pattern->getPath() ?: '/')->name($pattern->getBasename());
foreach ($finder as $file) $files[] = $file;
}
return $files;
}
/**
* Minifies the specified PHP files.
* @param \SplFileInfo[] $files The list of PHP files.
* @return int The number of minified files.
*/
private function minifyFiles(array $files): int {
$this->steps = count($files);
$this->startProgressIndicator();
if ($this->base) $basePath = (string) $this->base->getRealPath();
else {
$directories = array_map(fn($file) => $file->getPathInfo()->getRealPath(), $files);
$basePath = Path::getLongestCommonBasePath($directories) ?: (string) getcwd();
}
/** @var \SplFileInfo $destination */
$destination = $this->destination;
$transformer = $this->createTransformer();
$count = 0;
foreach ($files as $file) {
if (!$this->silent) $this->printTaskInfo('Minifying {path}', ['path' => $file->getPathname()]);
$output = new \SplFileInfo(Path::join($destination->getPathname(), Path::makeRelative((string) $file->getRealPath(), $basePath)));
$directory = $output->getPathInfo();
if (!$directory->isDir()) mkdir($directory->getPathname(), 0755, true);
if ($output->openFile('w')->fwrite($transformer->transform($file))) $count++;
$this->advanceProgressIndicator();
}
$transformer->close();
$this->stopProgressIndicator();
return $count;
}
/** @var \SplFileInfo|null The base path that is stripped from the computed path of the destination files. */
private ?\SplFileInfo $base = null;
/** @var \SplFileInfo|null The path to the PHP executable. */
private ?\SplFileInfo $binary = null;
/** @var \SplFileInfo|null The path of the destination directory. */
private ?\SplFileInfo $destination = null;
/** @var string The transform mode. */
private string $mode = TransformMode::safe;
/** @var bool Value indicating whether to silent the minifier output. */
private bool $silent = false;
/** @var int The number of progress steps. */
private int $steps = 0;
/** @var string[] The file patterns of the input scripts. */
private array $sources;
/**
* Creates a new minifier.
* @param string|string[] $patterns The file patterns corresponding to the input scripts.
*/
function __construct($patterns) {
assert(is_string($patterns) || is_array($patterns));
$this->sources = is_array($patterns) ? $patterns : [$patterns];
}
/**
* Sets the base path that is stripped from the computed path of the destination files.
* @param string $path The new base path.
* @return $this This instance.
*/
function base(string $path): self {
assert(mb_strlen($path) > 0);
$this->base = new \SplFileInfo(str_replace("/", DIRECTORY_SEPARATOR, Path::canonicalize($path)));
return $this;
}
/**
* Sets the path to the PHP executable.
* @param string $executable The new executable path.
* @return $this This instance.
*/
function binary(string $executable): self {
assert(mb_strlen($executable) > 0);
$this->binary = new \SplFileInfo(str_replace("/", DIRECTORY_SEPARATOR, Path::canonicalize($executable)));
return $this;
}
/**
* Sets a value indicating the type of transformation applied by this minifier.
* @param string $transformMode The transform mode.
* @return $this This instance.
*/
function mode(string $transformMode): self {
$this->mode = TransformMode::coerce($transformMode, TransformMode::safe);
return $this;
}
/**
* Returns the number of progress steps.
* @return int The number of progress steps.
*/
public function progressIndicatorSteps(): int {
return $this->steps;
}
/**
* Runs this task.
* @return Result The task result.
*/
function run(): Result {
if (!$this->destination) return Result::error($this, "The destination directory is undefined.");
try { $files = $this->findFiles(); }
catch (DirectoryNotFoundException $e) { return Result::fromException($this, $e); }
if (!$files) {
$message = "No PHP files to minify.";
$this->printTaskSuccess($message);
return Result::success($this, $message);
}
$count = $this->minifyFiles($files);
$context = ["count" => $count, "total" => $this->steps, "destination" => $this->destination->getPathname()];
$fileLabel = $this->steps <= 1 ? "file" : "files";
$message = "Minified {count} out of {total} PHP $fileLabel into {destination}";
if ($count != $this->steps) return Result::error($this, $message, $context);
$this->printTaskSuccess($message, $context);
return Result::success($this, $message, $context);
}
/**
* Sets a value indicating whether to silent the minifier output.
* @param bool $value `true` to silent the minifier output, otherwise `false`.
* @return $this This instance.
*/
function silent(bool $value = true): self {
$this->silent = $value;
return $this;
}
/**
* Sets the path of the output directory.
* @param string $destination The destination directory for the minified scripts.
* @return $this This instance.
*/
function to(string $destination): self {
assert(mb_strlen($destination) > 0);
$this->destination = new \SplFileInfo(str_replace("/", DIRECTORY_SEPARATOR, Path::canonicalize($destination)));
return $this;
}
/**
* Creates a transformer corresponding to the input mode.
* @return Transformer The transformer corresponding to the input mode.
*/
private function createTransformer(): Transformer {
if ($this->binary) $binary = $this->binary;
else {
/** @var string $executable */
$executable = which("php", false, fn() => "php");
$binary = new \SplFileInfo($executable);
}
return $this->mode == TransformMode::fast ? new FastTransformer($binary) : new SafeTransformer($binary);
}
/**
* Finds the files corresponding to the input pattern.
* @return \SplFileInfo[] The found files.
* @throws DirectoryNotFoundException The directory corresponding to the input pattern is not found.
*/
private function findFiles(): array {
$files = [];
foreach ($this->sources as $source) {
$finder = (new Finder)->files()->followLinks();
$hasDirectory = mb_strpos($source, "/") === false && mb_strpos($source, DIRECTORY_SEPARATOR) === false;
$pattern = new \SplFileInfo($hasDirectory ? $source : "./$source");
if ($pattern->isDir()) $finder->in($pattern->getPathname())->name("*.php");
else $finder->in($pattern->getPath() ?: "/")->name($pattern->getBasename());
foreach ($finder as $file) $files[] = $file;
}
return $files;
}
/**
* Minifies the specified PHP files.
* @param \SplFileInfo[] $files The list of PHP files.
* @return int The number of minified files.
*/
private function minifyFiles(array $files): int {
$this->steps = count($files);
$this->startProgressIndicator();
if ($this->base) $basePath = (string) $this->base->getRealPath();
else {
$directories = array_map(fn($file) => $file->getPathInfo()->getRealPath(), $files);
$basePath = Path::getLongestCommonBasePath($directories) ?: (string) getcwd();
}
/** @var \SplFileInfo $destination */
$destination = $this->destination;
$transformer = $this->createTransformer();
$count = 0;
foreach ($files as $file) {
if (!$this->silent) $this->printTaskInfo("Minifying {path}", ["path" => $file->getPathname()]);
$output = new \SplFileInfo(Path::join($destination->getPathname(), Path::makeRelative((string) $file->getRealPath(), $basePath)));
$directory = $output->getPathInfo();
if (!$directory->isDir()) mkdir($directory->getPathname(), 0755, true);
if ($output->openFile("w")->fwrite($transformer->transform($file))) $count++;
$this->advanceProgressIndicator();
}
$transformer->close();
$this->stopProgressIndicator();
return $count;
}
}

+ 24
- 24
src/SafeTransformer.php View File

@@ -4,31 +4,31 @@ namespace Robo\PhpMinify;
/** Removes comments and whitespace from a PHP script, by calling a PHP process. */
class SafeTransformer implements Transformer {

/** @var \SplFileInfo The path to the PHP executable. */
private \SplFileInfo $executable;
/** @var \SplFileInfo The path to the PHP executable. */
private \SplFileInfo $executable;

/**
* Creates a new safe transformer.
* @param \SplFileInfo|null $executable The path to the PHP executable.
*/
function __construct(?\SplFileInfo $executable = null) {
$this->executable = $executable ?? new \SplFileInfo('php');
}
/**
* Creates a new safe transformer.
* @param \SplFileInfo|null $executable The path to the PHP executable.
*/
function __construct(?\SplFileInfo $executable = null) {
$this->executable = $executable ?? new \SplFileInfo("php");
}

/** Closes this transformer and releases any resources associated with it. */
function close(): void {
// Noop.
}
/** Closes this transformer and releases any resources associated with it. */
function close(): void {
// Noop.
}

/**
* Processes a PHP script.
* @param \SplFileInfo $script The path to the PHP script.
* @return string The transformed script.
*/
function transform(\SplFileInfo $script): string {
$phpExecutable = escapeshellarg($this->executable->getPathname());
$phpScript = escapeshellarg((string) $script->getRealPath());
exec("$phpExecutable -w $phpScript", $output);
return implode(PHP_EOL, $output);
}
/**
* Processes a PHP script.
* @param \SplFileInfo $script The path to the PHP script.
* @return string The transformed script.
*/
function transform(\SplFileInfo $script): string {
$phpExecutable = escapeshellarg($this->executable->getPathname());
$phpScript = escapeshellarg((string) $script->getRealPath());
exec("$phpExecutable -w $phpScript", $output);
return implode(PHP_EOL, $output);
}
}

+ 48
- 48
src/Server.php View File

@@ -4,52 +4,52 @@ namespace Robo\PhpMinify;
/** An HTTP server that strip comments and whitespace from the files specified in client requests. */
class Server {

/**
* Runs the application.
* @param array<string, string> $args The request parameters.
*/
function run(array $args = []): void {
try {
$this->sendResponse($this->processRequest($args));
}
catch (\Throwable $e) {
$code = $e->getCode();
$this->sendResponse($e->getMessage(), $code >= 400 && $code < 600 ? $code : 500);
}
}
/**
* Sends the specified response body.
* @param string $body The response body to send to the client.
* @param int $status The status code of the response.
*/
function sendResponse(string $body, int $status = 200): void {
assert($status >= 100 && $status < 600);
http_response_code($status);
if (!headers_sent()) {
header('Content-Length: '.strlen($body));
header('Content-Type: text/plain; charset='.mb_internal_encoding());
}
echo $body;
}
/**
* Processes the specified request body.
* @param array<string, string> $args The request sent by a client.
* @return string The stripped source code corresponding to the provided file.
* @throws \Exception The requirements are not met, or an error occurred.
*/
private function processRequest(array $args): string {
if (!isset($args['file']) || !mb_strlen($args['file'])) throw new \LogicException('Bad Request', 400);
$file = new \SplFileInfo($args['file']);
if (!$file->isReadable()) throw new \RuntimeException('Not Found', 404);
$output = php_strip_whitespace($file->getPathname());
if (!mb_strlen($output)) throw new \RuntimeException('Internal Server Error', 500);
return $output;
}
/**
* Runs the application.
* @param array<string, string> $args The request parameters.
*/
function run(array $args = []): void {
try {
$this->sendResponse($this->processRequest($args));
}
catch (\Throwable $e) {
$code = $e->getCode();
$this->sendResponse($e->getMessage(), $code >= 400 && $code < 600 ? $code : 500);
}
}
/**
* Sends the specified response body.
* @param string $body The response body to send to the client.
* @param int $status The status code of the response.
*/
function sendResponse(string $body, int $status = 200): void {
assert($status >= 100 && $status < 600);
http_response_code($status);
if (!headers_sent()) {
header("Content-Length: ".strlen($body));
header("Content-Type: text/plain; charset=".mb_internal_encoding());
}
echo $body;
}
/**
* Processes the specified request body.
* @param array<string, string> $args The request sent by a client.
* @return string The stripped source code corresponding to the provided file.
* @throws \Exception The requirements are not met, or an error occurred.
*/
private function processRequest(array $args): string {
if (!isset($args["file"]) || !mb_strlen($args["file"])) throw new \LogicException("Bad Request", 400);
$file = new \SplFileInfo($args["file"]);
if (!$file->isReadable()) throw new \RuntimeException("Not Found", 404);
$output = php_strip_whitespace($file->getPathname());
if (!mb_strlen($output)) throw new \RuntimeException("Internal Server Error", 500);
return $output;
}
}

+ 9
- 9
src/Tasks.php View File

@@ -6,13 +6,13 @@ use Robo\Collection\{CollectionBuilder};
/** Provides a set of [Robo](https://robo.li) tasks. */
trait Tasks {

/**
* Creates a task minifying a set of PHP scripts.
* @param string|string[] $patterns The file patterns corresponding to the input scripts.
* @return CollectionBuilder|Minifier The newly created task.
*/
protected function taskPhpMinify($patterns) {
assert(is_string($patterns) || is_array($patterns));
return $this->task(Minifier::class, $patterns);
}
/**
* Creates a task minifying a set of PHP scripts.
* @param string|string[] $patterns The file patterns corresponding to the input scripts.
* @return CollectionBuilder|Minifier The newly created task.
*/
protected function taskPhpMinify($patterns) {
assert(is_string($patterns) || is_array($patterns));
return $this->task(Minifier::class, $patterns);
}
}

+ 5
- 5
src/TransformMode.php View File

@@ -5,11 +5,11 @@ use Enum\{EnumTrait};

/** Defines the type of transformation applied by a minifier. */
final class TransformMode {
use EnumTrait;
use EnumTrait;

/** @var string Applies a fast transformation. */
const fast = 'fast';
/** @var string Applies a fast transformation. */
const fast = "fast";

/** @var string Applies a safe transformation. */
const safe = 'safe';
/** @var string Applies a safe transformation. */
const safe = "safe";
}

+ 8
- 8
src/Transformer.php View File

@@ -4,13 +4,13 @@ namespace Robo\PhpMinify;
/** Removes comments and whitespace from a PHP script. */
interface Transformer {

/** Closes this transformer and releases any resources associated with it. */
function close(): void;
/** Closes this transformer and releases any resources associated with it. */
function close(): void;

/**
* Processes a PHP script.
* @param \SplFileInfo $script The path to the PHP script.
* @return string The transformed script.
*/
function transform(\SplFileInfo $script): string;
/**
* Processes a PHP script.
* @param \SplFileInfo $script The path to the PHP script.
* @return string The transformed script.
*/
function transform(\SplFileInfo $script): string;
}

+ 1
- 1
src/index.php View File

@@ -2,5 +2,5 @@
use Robo\PhpMinify\{Server};

// Start the application.
require_once __DIR__.'/Server.php';
require_once __DIR__."/Server.php";
(new Server)->run($_GET);

+ 87
- 87
test/FastTransformerTest.php View File

@@ -7,91 +7,91 @@ use function PHPUnit\Framework\{assertThat, isFalse, isNull, isTrue, stringConta
/** @testdox Robo\PhpMinify\FastTransformer */
class FastTransformerTest extends TestCase {

/** @testdox ->close() */
function testClose(): void {
$transformer = new FastTransformer;
// It should complete without any error.
try {
$transformer->listen();
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
// It should be callable multiple times.
try {
$transformer->close();
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
}
/** @testdox ->isListening() */
function testIsListening(): void {
$transformer = new FastTransformer;
// It should return whether the server is listening.
assertThat($transformer->isListening(), isFalse());
$transformer->listen();
assertThat($transformer->isListening(), isTrue());
$transformer->close();
assertThat($transformer->isListening(), isFalse());
}
/** @testdox ->listen() */
function testListen(): void {
$transformer = new FastTransformer;
// It should complete without any error.
try {
$transformer->listen();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
// It should be callable multiple times.
try {
$transformer->listen();
$transformer->listen();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
$transformer->close();
}
/** @testdox ->transform() */
function testTransform(): void {
$script = new \SplFileInfo('test/fixtures/sample.php');
$transformer = new FastTransformer;
// It should remove the inline comments.
assertThat($transformer->transform($script), stringContains("<?= 'Hello World!' ?>"));
// It should remove the multi-line comments.
assertThat($transformer->transform($script), stringContains('namespace dummy; class Dummy'));
// It should remove the single-line comments.
assertThat($transformer->transform($script), stringContains('$className = get_class($this); return $className;'));
// It should remove the whitespace.
assertThat($transformer->transform($script), stringContains('__construct() { }'));
$transformer->close();
}
/** @testdox ->close() */
function testClose(): void {
$transformer = new FastTransformer;
// It should complete without any error.
try {
$transformer->listen();
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
// It should be callable multiple times.
try {
$transformer->close();
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
}
/** @testdox ->isListening() */
function testIsListening(): void {
$transformer = new FastTransformer;
// It should return whether the server is listening.
assertThat($transformer->isListening(), isFalse());
$transformer->listen();
assertThat($transformer->isListening(), isTrue());
$transformer->close();
assertThat($transformer->isListening(), isFalse());
}
/** @testdox ->listen() */
function testListen(): void {
$transformer = new FastTransformer;
// It should complete without any error.
try {
$transformer->listen();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
// It should be callable multiple times.
try {
$transformer->listen();
$transformer->listen();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
$transformer->close();
}
/** @testdox ->transform() */
function testTransform(): void {
$script = new \SplFileInfo("test/fixtures/sample.php");
$transformer = new FastTransformer;
// It should remove the inline comments.
assertThat($transformer->transform($script), stringContains('<?= "Hello World!" ?>'));
// It should remove the multi-line comments.
assertThat($transformer->transform($script), stringContains("namespace dummy; class Dummy"));
// It should remove the single-line comments.
assertThat($transformer->transform($script), stringContains('$className = get_class($this); return $className;'));
// It should remove the whitespace.
assertThat($transformer->transform($script), stringContains("__construct() { }"));
$transformer->close();
}
}

+ 45
- 45
test/MinifierTest.php View File

@@ -10,49 +10,49 @@ use function PHPUnit\Framework\{assertThat, fileExists, logicalAnd, stringContai

/** @testdox Robo\PhpMinify\Minifier */
class MinifierTest extends TestCase implements ContainerAwareInterface {
use ContainerAwareTrait;
use Tasks;
use TaskAccessor;
/**
* Scaffolds the collection builder.
* @return CollectionBuilder The newly created collection builder.
*/
function collectionBuilder(): CollectionBuilder {
return CollectionBuilder::create($this->getContainer(), new \Robo\Tasks);
}
/** Sets up the dependency injection container. */
function setup(): void {
$this->setContainer(Robo::createDefaultContainer(null, new NullOutput));
}
/** @testdox ->run() */
function testRun(): void {
// It should remove the comments and whitespace using the fast transformer.
$testDir = 'var/test/Minifier.run.fast';
$this->taskPhpMinify('test/fixtures')->mode(TransformMode::fast)->silent()->to($testDir)->run();
$output = new \SplFileObject("$testDir/sample.php");
assertThat($output->getPathname(), fileExists());
assertThat((string) $output->fread($output->getSize()), logicalAnd(
stringContains("<?= 'Hello World!' ?>"),
stringContains('namespace dummy; class Dummy'),
stringContains('$className = get_class($this); return $className;'),
stringContains('__construct() { }')
));
// It should remove the comments and whitespace using the safe transformer.
$testDir = 'var/test/Minifier.run.safe';
$this->taskPhpMinify('test/fixtures')->mode(TransformMode::safe)->silent()->to($testDir)->run();
$output = new \SplFileObject("$testDir/sample.php");
assertThat($output->getPathname(), fileExists());
assertThat((string) $output->fread($output->getSize()), logicalAnd(
stringContains("<?= 'Hello World!' ?>"),
stringContains('namespace dummy; class Dummy'),
stringContains('$className = get_class($this); return $className;'),
stringContains('__construct() { }')
));
}
use ContainerAwareTrait;
use Tasks;
use TaskAccessor;
/**
* Scaffolds the collection builder.
* @return CollectionBuilder The newly created collection builder.
*/
function collectionBuilder(): CollectionBuilder {
return CollectionBuilder::create($this->getContainer(), new \Robo\Tasks);
}
/** Sets up the dependency injection container. */
function setup(): void {
$this->setContainer(Robo::createDefaultContainer(null, new NullOutput));
}
/** @testdox ->run() */
function testRun(): void {
// It should remove the comments and whitespace using the fast transformer.
$testDir = "var/test/Minifier.run.fast";
$this->taskPhpMinify("test/fixtures")->mode(TransformMode::fast)->silent()->to($testDir)->run();
$output = new \SplFileObject("$testDir/sample.php");
assertThat($output->getPathname(), fileExists());
assertThat((string) $output->fread($output->getSize()), logicalAnd(
stringContains('<?= "Hello World!" ?>'),
stringContains("namespace dummy; class Dummy"),
stringContains('$className = get_class($this); return $className;'),
stringContains("__construct() { }")
));
// It should remove the comments and whitespace using the safe transformer.
$testDir = "var/test/Minifier.run.safe";
$this->taskPhpMinify("test/fixtures")->mode(TransformMode::safe)->silent()->to($testDir)->run();
$output = new \SplFileObject("$testDir/sample.php");
assertThat($output->getPathname(), fileExists());
assertThat((string) $output->fread($output->getSize()), logicalAnd(
stringContains('<?= "Hello World!" ?>'),
stringContains("namespace dummy; class Dummy"),
stringContains('$className = get_class($this); return $className;'),
stringContains("__construct() { }")
));
}
}

+ 44
- 44
test/SafeTransformerTest.php View File

@@ -7,48 +7,48 @@ use function PHPUnit\Framework\{assertThat, isNull, stringContains};
/** @testdox Robo\PhpMinify\SafeTransformer */
class SafeTransformerTest extends TestCase {

/** @testdox ->close() */
function testClose(): void {
$transformer = new SafeTransformer;
// It should complete without any error.
try {
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
// It should be callable multiple times.
try {
$transformer->close();
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
}
/** @testdox ->transform() */
function testTransform(): void {
$script = new \SplFileInfo('test/fixtures/sample.php');
$transformer = new SafeTransformer;
// It should remove the inline comments.
assertThat($transformer->transform($script), stringContains("<?= 'Hello World!' ?>"));
// It should remove the multi-line comments.
assertThat($transformer->transform($script), stringContains('namespace dummy; class Dummy'));
// It should remove the single-line comments.
assertThat($transformer->transform($script), stringContains('$className = get_class($this); return $className;'));
// It should remove the whitespace.
assertThat($transformer->transform($script), stringContains('__construct() { }'));
$transformer->close();
}
/** @testdox ->close() */
function testClose(): void {
$transformer = new SafeTransformer;
// It should complete without any error.
try {
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
// It should be callable multiple times.
try {
$transformer->close();
$transformer->close();
assertThat(null, isNull());
}
catch (\Throwable $e) {
Assert::fail($e->getMessage());
}
}
/** @testdox ->transform() */
function testTransform(): void {
$script = new \SplFileInfo("test/fixtures/sample.php");
$transformer = new SafeTransformer;
// It should remove the inline comments.
assertThat($transformer->transform($script), stringContains('<?= "Hello World!" ?>'));
// It should remove the multi-line comments.
assertThat($transformer->transform($script), stringContains("namespace dummy; class Dummy"));
// It should remove the single-line comments.
assertThat($transformer->transform($script), stringContains('$className = get_class($this); return $className;'));
// It should remove the whitespace.
assertThat($transformer->transform($script), stringContains("__construct() { }"));
$transformer->close();
}
}

+ 42
- 42
test/ServerTest.php View File

@@ -7,46 +7,46 @@ use function PHPUnit\Framework\{assertThat, isInstanceOf, logicalAnd, stringCont
/** @testdox Robo\PhpMinify\Server */
class ServerTest extends TestCase {

/** @var \ReflectionClass<Server> The object used to change the visibility of inaccessible class members. */
private static \ReflectionClass $reflection;
/** @beforeClass This method is called before the first test of this test class is run. */
static function setUpBeforeClass(): void {
self::$reflection = new \ReflectionClass(Server::class);
}
/** @testdox ->processRequest() */
function testProcessRequest(): void {
$method = self::$reflection->getMethod('processRequest');
$method->setAccessible(true);
// It should throw an exception if the input request is invalid.
try {
$method->invoke(new Server, []);
Assert::fail('Exception not thrown');
}
catch (\Throwable $e) {
assertThat($e, isInstanceOf(\LogicException::class));
}
// It should throw an exception if the requested file does not exist.
try {
$method->invoke(new Server, ['file' => 'dummy.txt']);
Assert::fail('Exception not thrown');
}
catch (\Throwable $e) {
assertThat($e, isInstanceOf(\RuntimeException::class));
}
// It should remove the comments and whitespace of the requested file.
$output = $method->invoke(new Server, ['file' => 'test/fixtures/sample.php']);
assertThat($output, logicalAnd(
stringContains("<?= 'Hello World!' ?>"),
stringContains('namespace dummy; class Dummy'),
stringContains('$className = get_class($this); return $className;'),
stringContains('__construct() { }')
));
}
/** @var \ReflectionClass<Server> The object used to change the visibility of inaccessible class members. */
private static \ReflectionClass $reflection;
/** @beforeClass This method is called before the first test of this test class is run. */
static function setUpBeforeClass(): void {
self::$reflection = new \ReflectionClass(Server::class);
}
/** @testdox ->processRequest() */
function testProcessRequest(): void {
$method = self::$reflection->getMethod("processRequest");
$method->setAccessible(true);
// It should throw an exception if the input request is invalid.
try {
$method->invoke(new Server, []);
Assert::fail("Exception not thrown");
}
catch (\Throwable $e) {
assertThat($e, isInstanceOf(\LogicException::class));
}
// It should throw an exception if the requested file does not exist.
try {
$method->invoke(new Server, ["file" => "dummy.txt"]);
Assert::fail("Exception not thrown");
}
catch (\Throwable $e) {
assertThat($e, isInstanceOf(\RuntimeException::class));
}
// It should remove the comments and whitespace of the requested file.
$output = $method->invoke(new Server, ["file" => "test/fixtures/sample.php"]);
assertThat($output, logicalAnd(
stringContains('<?= "Hello World!" ?>'),
stringContains("namespace dummy; class Dummy"),
stringContains('$className = get_class($this); return $className;'),
stringContains("__construct() { }")
));
}
}

+ 22
- 22
test/fixtures/sample.php View File

@@ -6,34 +6,34 @@ namespace dummy;
/** A dummy class. */
class Dummy {

/** Creates a new instance. */
public function __construct() {
/** Creates a new instance. */
public function __construct() {

}
}

/**
* A dummy method.
* @return string The class name.
*/
public function __toString(): string {
// A comment.
$className = get_class($this);
/**
* A dummy method.
* @return string The class name.
*/
public function __toString(): string {
// A comment.
$className = get_class($this);

// Another one.
return $className;
}
// Another one.
return $className;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Tests</title>
</head>
<head>
<meta charset="UTF-8">
<title>Tests</title>
</head>

<body>
<h1>
<?= /* An inline coment. */ 'Hello World!' ?>
</h1>
</body>
<body>
<h1>
<?= /* An inline coment. */ "Hello World!" ?>
</h1>
</body>
</html>

+ 3
- 3
tool/clean.ps1 View File

@@ -2,10 +2,10 @@
Set-StrictMode -Version Latest
Set-Location (Split-Path $PSScriptRoot)

foreach ($item in 'build', 'doc/api', 'www') {
if (Test-Path $item) { Remove-Item $item -Recurse }
foreach ($item in "build", "doc/api", "www") {
if (Test-Path $item) { Remove-Item $item -Recurse }
}

foreach ($item in Get-ChildItem var -Exclude .gitkeep) {
Remove-Item $item -Recurse
Remove-Item $item -Recurse
}

+ 1
- 1
tool/doc.ps1 View File

@@ -2,7 +2,7 @@
Set-StrictMode -Version Latest
Set-Location (Split-Path $PSScriptRoot)

$phpdoc = $IsWindows ? 'php "C:/Program Files/PHP/share/phpDocumentor.phar"' : 'phpdoc';
$phpdoc = $IsWindows ? 'php "C:/Program Files/PHP/share/phpDocumentor.phar"' : "phpdoc";
Invoke-Expression "$phpdoc --config=etc/phpdoc.xml"

if (-not (Test-Path doc/api/images)) { New-Item doc/api/images -ItemType Directory | Out-Null }


+ 1
- 1
tool/lint.ps1 View File

@@ -2,5 +2,5 @@
Set-StrictMode -Version Latest
Set-Location (Split-Path $PSScriptRoot)

php -l example/main.php
php -l example/RoboFile.php
vendor/bin/phpstan analyse --configuration=etc/phpstan.neon

+ 1
- 1
tool/upgrade.ps1 View File

@@ -6,5 +6,5 @@ git reset --hard
git fetch --all --prune
git pull --rebase

$composer = $IsWindows ? 'php "C:/Program Files/PHP/share/composer.phar"' : 'composer'
$composer = $IsWindows ? 'php "C:/Program Files/PHP/share/composer.phar"' : "composer"
Invoke-Expression "$composer update --no-interaction"

+ 10
- 10
tool/watch.ps1 View File

@@ -4,26 +4,26 @@ Set-Location (Split-Path $PSScriptRoot)
[Console]::TreatControlCAsInput = $true

$action = {
if ($EventArgs.Name -notlike '*.g.php') {
$changeType = [String] $EventArgs.ChangeType
Write-Host "'$($EventArgs.Name)' was $($changeType.ToLower()): starting a new build..."
$timeSpan = Measure-Command { tool/build.ps1 }
Write-Host "> Finished the build after $($timeSpan.TotalSeconds) seconds."
}
if ($EventArgs.Name -notlike "*.g.php") {
$changeType = [String] $EventArgs.ChangeType
Write-Host "'$($EventArgs.Name)' was $($changeType.ToLower()): starting a new build..."
$timeSpan = Measure-Command { tool/build.ps1 }
Write-Host "> Finished the build after $($timeSpan.TotalSeconds) seconds."
}
}

$watcher = New-Object System.IO.FileSystemWatcher (Resolve-Path src).Path
$watcher.EnableRaisingEvents = $true
$watcher.IncludeSubdirectories = $true

foreach ($event in 'Changed', 'Created', 'Deleted', 'Renamed') {
Register-ObjectEvent $watcher $event -Action $action | Out-Null
foreach ($event in "Changed", "Created", "Deleted", "Renamed") {
Register-ObjectEvent $watcher $event -Action $action | Out-Null
}

$console = $Host.UI.RawUI;
while ($true) {
if ($console.KeyAvailable -and ($console.ReadKey('AllowCtrlC,IncludeKeyUp,NoEcho').Character -eq 3)) { break }
Start-Sleep -Milliseconds 200
if ($console.KeyAvailable -and ($console.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character -eq 3)) { break }
Start-Sleep -Milliseconds 200
}

Get-EventSubscriber | Unregister-Event

Loading…