├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Exceptions │ └── AuditFailedException.php └── Lighthouse.php └── tests ├── Integration └── IntegrationTest.php └── Unit └── UnitTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | node_modules 4 | package.json 5 | yarn.lock 6 | config.js 7 | report.json 8 | .DS_Store 9 | .idea 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Dimitris Zavantias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lighthouse 2 | 3 | This package provide a php interface for [Google Lighthouse](https://github.com/GoogleChrome/lighthouse). 4 | 5 | ## Installation 6 | 7 | You can install the package via composer: `composer require dzava/lighthouse` 8 | 9 | Install Lighthouse `yarn add lighthouse`. Last tested with Lighthouse v8.5.1. 10 | 11 | ## Usage 12 | 13 | Here's an example that will perform the default Lighthouse audits and store the result in `report.json` (You can use the [Lighthouse Viewer](https://googlechrome.github.io/lighthouse/viewer/) to open the report): 14 | 15 | ```php 16 | use Dzava\Lighthouse\Lighthouse; 17 | 18 | (new Lighthouse()) 19 | ->setOutput('report.json') 20 | ->accessibility() 21 | ->bestPractices() 22 | ->performance() 23 | ->pwa() 24 | ->seo() 25 | ->audit('http://example.com'); 26 | ``` 27 | 28 | ### Output 29 | 30 | The `setOutput` method accepts a second argument that can be used to specify the format (json,html). 31 | If the format argument is missing then the file extension will be used to determine the output format. 32 | If the file extension does not specify an accepted format, then json will be used. 33 | 34 | You can output both the json and html reports by passing an array as the second argument. For the example 35 | the following code will create two reports `example.report.html` and `example.report.json`. 36 | 37 | ```php 38 | use Dzava\Lighthouse\Lighthouse; 39 | 40 | (new Lighthouse()) 41 | ->setOutput('example', ['html', 'json']) 42 | ->performance() 43 | ->audit('http://example.com'); 44 | ``` 45 | 46 | ### Using a custom config 47 | 48 | You can provide your own configuration file using the `withConfig` method. 49 | ```php 50 | use Dzava\Lighthouse\Lighthouse; 51 | 52 | (new Lighthouse()) 53 | ->withConfig('./my-config.js') 54 | ->audit('http://example.com'); 55 | ``` 56 | 57 | You can also pass a php array to the `withConfig` method containing your configuration. 58 | ```php 59 | use Dzava\Lighthouse\Lighthouse; 60 | 61 | (new Lighthouse()) 62 | ->withConfig([ 63 | 'extends' => 'lighthouse:default', 64 | 'settings' => [ 65 | 'onlyCategories' => ['accessibility'], 66 | ], 67 | ]) 68 | ->audit('http://example.com'); 69 | ``` 70 | **Note:** in order to use an array to specify the configuration options, php needs to be able to [create and move](https://www.php.net/manual/en/function.tmpfile.php) temporary files. 71 | 72 | Details about the configuration options can be found [here](https://github.com/GoogleChrome/lighthouse/blob/master/docs/configuration.md) 73 | 74 | ### Customizing node and Lighthouse paths 75 | 76 | If you need to manually set these paths, you can do this by calling the `setNodeBinary` and `setLighthousePath` methods. 77 | 78 | ```php 79 | use Dzava\Lighthouse\Lighthouse; 80 | 81 | (new Lighthouse()) 82 | ->setNodeBinary('/usr/bin/node') 83 | ->setLighthousePath('./node_modules/lighthouse/lighthouse-cli/index.js') 84 | ->audit('http://example.com'); 85 | ``` 86 | 87 | ### Passing flags to Chrome 88 | Use the `setChromeFlags` method to pass any flags to the Chrome instance. 89 | ```php 90 | use Dzava\Lighthouse\Lighthouse; 91 | 92 | (new Lighthouse()) 93 | // these are the default flags used 94 | ->setChromeFlags(['--headless', '--disable-gpu', '--no-sandbox']) 95 | ->audit('http://example.com'); 96 | ``` 97 | 98 | ## Troubleshooting 99 | 100 | #### Audit of 'url' failed 101 | Use the following snippet to check why the audit fails. 102 | 103 | ```php 104 | require "./vendor/autoload.php"; 105 | 106 | use Dzava\Lighthouse\Exceptions\AuditFailedException; 107 | use Dzava\Lighthouse\Lighthouse; 108 | 109 | 110 | try { 111 | (new Lighthouse()) 112 | ->performance() 113 | ->audit('http://example.com'); 114 | } catch(AuditFailedException $e) { 115 | echo $e->getOutput(); 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dzava/lighthouse", 3 | "description": "Interface for the Google Lighthouse project", 4 | "keywords": [ 5 | "lighthouse" 6 | ], 7 | "homepage": "https://github.com/dzava/lighthouse-php", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Dimitris Zavantias", 12 | "email": "dzavantias@gmail.com" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=7.4", 17 | "symfony/process": "^4.0|^5.0|^6.0", 18 | "ext-json": "*" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^8.4|^9.0", 22 | "dms/phpunit-arraysubset-asserts": "^0.2.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Dzava\\Lighthouse\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Dzava\\Lighthouse\\Tests\\": "tests/" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | tests/Unit 11 | 12 | 13 | tests/Integration 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Exceptions/AuditFailedException.php: -------------------------------------------------------------------------------- 1 | output = $output; 14 | } 15 | 16 | public function getOutput() 17 | { 18 | return $this->output; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Lighthouse.php: -------------------------------------------------------------------------------- 1 | setChromeFlags(['--headless', '--disable-gpu', '--no-sandbox']); 27 | } 28 | 29 | public function __destruct() 30 | { 31 | $this->cleanupConfig(); 32 | } 33 | 34 | /** 35 | * @param string $url 36 | * @return string 37 | * @throws AuditFailedException 38 | */ 39 | public function audit($url) 40 | { 41 | $process = new Process($this->getCommand($url)); 42 | 43 | $process->setTimeout($this->timeout)->run(null, $this->environmentVariables); 44 | 45 | if (!$process->isSuccessful()) { 46 | throw new AuditFailedException($url, $process->getErrorOutput()); 47 | } 48 | 49 | return $process->getOutput(); 50 | } 51 | 52 | /** 53 | * Enable the accessibility audit 54 | * 55 | * @param bool $enable 56 | * @return $this 57 | */ 58 | public function accessibility($enable = true) 59 | { 60 | $this->setCategory('accessibility', $enable); 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Enable the best practices audit 67 | * 68 | * @param bool $enable 69 | * @return $this 70 | */ 71 | public function bestPractices($enable = true) 72 | { 73 | $this->setCategory('best-practices', $enable); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Enable the best performance audit 80 | * 81 | * @param bool $enable 82 | * @return $this 83 | */ 84 | public function performance($enable = true) 85 | { 86 | $this->setCategory('performance', $enable); 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * Enable the progressive web app audit 93 | * 94 | * @param bool $enable 95 | * @return $this 96 | */ 97 | public function pwa($enable = true) 98 | { 99 | $this->setCategory('pwa', $enable); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * Enable the search engine optimization audit 106 | * 107 | * @param bool $enable 108 | * @return $this 109 | */ 110 | public function seo($enable = true) 111 | { 112 | $this->setCategory('seo', $enable); 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Set the lighthouse config to use 119 | * 120 | * @param string|array $path 121 | * @return $this 122 | */ 123 | public function withConfig($path) 124 | { 125 | $this->cleanupConfig(); 126 | 127 | if (is_array($path)) { 128 | $this->configPath = $this->buildConfig($path); 129 | $this->shouldCleanupConfig = true; 130 | } else { 131 | $this->configPath = $path; 132 | $this->shouldCleanupConfig = false; 133 | } 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @param string $path 140 | * @param null|string|array $format 141 | * @return $this 142 | */ 143 | public function setOutput($path, $format = null) 144 | { 145 | $this->setOption('--output-path', $path); 146 | 147 | if ($format === null) { 148 | $format = $this->guessOutputFormatFromFile($path); 149 | } 150 | 151 | if (!is_array($format)) { 152 | $format = [$format]; 153 | } 154 | 155 | $format = array_intersect($this->availableFormats, $format); 156 | 157 | $this->outputFormat = array_map(function ($format) { 158 | return "--output=$format"; 159 | }, $format); 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * @param bool|mixed $value 166 | * @param callable $callback 167 | * @param callable|null $default 168 | * @return $this|mixed 169 | */ 170 | public function when($value, callable $callback, callable $default = null) 171 | { 172 | if ($value) { 173 | return $callback($this, $value); 174 | } 175 | 176 | if ($default) { 177 | return $default($this, $value); 178 | } 179 | 180 | return $this; 181 | } 182 | 183 | /** 184 | * @param bool|mixed $value 185 | * @param callable $callback 186 | * @param callable|null $default 187 | * @return $this|mixed 188 | */ 189 | public function unless($value, callable $callback, callable $default = null) 190 | { 191 | return $this->when(!$value, $callback, $default); 192 | } 193 | 194 | /** 195 | * @param string $format 196 | * @return $this 197 | */ 198 | public function setDefaultFormat($format) 199 | { 200 | $this->defaultFormat = $format; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * @param string $path 207 | * @return $this 208 | */ 209 | public function setNodePath($path) 210 | { 211 | $this->nodePath = $path; 212 | 213 | return $this; 214 | } 215 | 216 | /** 217 | * @param string $path 218 | * @return $this 219 | */ 220 | public function setLighthousePath($path) 221 | { 222 | $this->lighthousePath = $path; 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * @param string $path 229 | * @return Lighthouse 230 | */ 231 | public function setChromePath($path) 232 | { 233 | $this->environmentVariables['CHROME_PATH'] = $path; 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Set the flags to pass to the spawned Chrome instance 240 | * 241 | * @param array|string $flags 242 | * @return $this 243 | */ 244 | public function setChromeFlags($flags) 245 | { 246 | if (is_array($flags)) { 247 | $flags = implode(' ', $flags); 248 | } 249 | 250 | $this->setOption('--chrome-flags', "'$flags'"); 251 | 252 | return $this; 253 | } 254 | 255 | /** 256 | * @param array $headers 257 | * @return $this 258 | */ 259 | public function setHeaders($headers) 260 | { 261 | if (empty($headers)) { 262 | $this->headers = []; 263 | 264 | return $this; 265 | } 266 | 267 | $headers = json_encode($headers); 268 | 269 | $this->headers = ["--extra-headers", $headers]; 270 | 271 | return $this; 272 | } 273 | 274 | /** 275 | * @param int $timeout 276 | * @return $this 277 | */ 278 | public function setTimeout($timeout) 279 | { 280 | $this->timeout = $timeout; 281 | 282 | return $this; 283 | } 284 | 285 | /** 286 | * @param string $option 287 | * @param mixed $value 288 | * @return $this 289 | */ 290 | public function setOption($option, $value = null) 291 | { 292 | if (($foundIndex = array_search($option, $this->options)) !== false) { 293 | $this->options[$foundIndex] = $option; 294 | 295 | return $this; 296 | } 297 | 298 | if ($value === null) { 299 | $this->options[] = $option; 300 | } else { 301 | $this->options[$option] = $value; 302 | } 303 | 304 | return $this; 305 | } 306 | 307 | public function getCommand($url) 308 | { 309 | $command = array_merge([ 310 | $this->nodePath, 311 | $this->lighthousePath, 312 | $url, 313 | ...$this->outputFormat, 314 | ...$this->headers, 315 | '--quiet', 316 | empty($this->categories) ? null : '--only-categories=' . implode(',', $this->categories), 317 | empty($this->configPath) ? '' : "--config-path={$this->configPath}", 318 | ], $this->processOptions()); 319 | 320 | return array_filter($command); 321 | } 322 | 323 | /** 324 | * Enable or disable a category 325 | * 326 | * @param $category 327 | * @return $this 328 | */ 329 | protected function setCategory($category, $enable) 330 | { 331 | $index = array_search($category, $this->categories); 332 | 333 | if ($index !== false) { 334 | if ($enable == false) { 335 | unset($this->categories[$index]); 336 | } 337 | } elseif ($enable) { 338 | $this->categories[] = $category; 339 | } 340 | 341 | return $this; 342 | } 343 | 344 | /** 345 | * Creates the config file used during the audit 346 | * 347 | * @param array $data 348 | * @return string The path of the config file 349 | */ 350 | protected function buildConfig($data) 351 | { 352 | $config = tmpfile(); 353 | $path = stream_get_meta_data($config)['uri']; 354 | rename($path, $path = "$path.js"); 355 | $r = 'module.exports = ' . json_encode($data); 356 | fwrite($config, $r); 357 | 358 | return $path; 359 | } 360 | 361 | /** 362 | * Convert the options array to an array that can be used 363 | * to construct the command arguments 364 | * 365 | * @return array 366 | */ 367 | protected function processOptions() 368 | { 369 | return array_map(function ($value, $option) { 370 | return is_numeric($option) ? $value : "$option=$value"; 371 | }, $this->options, array_keys($this->options)); 372 | } 373 | 374 | /** 375 | * @param $path 376 | * @return string 377 | */ 378 | protected function guessOutputFormatFromFile($path) 379 | { 380 | $format = pathinfo($path, PATHINFO_EXTENSION); 381 | 382 | if (!in_array($format, $this->availableFormats)) { 383 | $format = $this->defaultFormat; 384 | } 385 | 386 | return $format; 387 | } 388 | 389 | /** 390 | * @return $this 391 | */ 392 | protected function cleanupConfig() 393 | { 394 | if ($this->shouldCleanupConfig && $this->configPath && file_exists($this->configPath)) { 395 | unlink($this->configPath); 396 | } 397 | 398 | return $this; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /tests/Integration/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | lighthouse = (new Lighthouse()) 20 | ->setChromePath('/usr/bin/google-chrome-stable') 21 | ->setLighthousePath('./node_modules/lighthouse/lighthouse-cli/index.js'); 22 | } 23 | 24 | /** @test */ 25 | public function can_run_only_one_audit() 26 | { 27 | $report = $this->lighthouse 28 | ->performance() 29 | ->audit('http://example.com'); 30 | 31 | $this->assertReportIncludesCategory($report, 'Performance'); 32 | $this->assertReportDoesNotIncludeCategory($report, 'Progressive Web App'); 33 | } 34 | 35 | /** @test */ 36 | public function can_run_all_audits() 37 | { 38 | $report = $this->lighthouse 39 | ->accessibility() 40 | ->bestPractices() 41 | ->performance() 42 | ->pwa() 43 | ->seo() 44 | ->audit('http://example.com'); 45 | 46 | $this->assertReportIncludesCategory($report, [ 47 | 'Accessibility', 'Best Practices', 'Performance', 'Progressive Web App', 'SEO', 48 | ]); 49 | } 50 | 51 | /** @test */ 52 | public function runs_all_audits_by_default() 53 | { 54 | $report = $this->lighthouse 55 | ->audit('http://example.com'); 56 | 57 | $this->assertReportIncludesCategory($report, [ 58 | 'Accessibility', 'Best Practices', 'Performance', 'Progressive Web App', 'SEO', 59 | ]); 60 | } 61 | 62 | /** @test */ 63 | public function updates_the_config_when_a_category_is_added_or_removed() 64 | { 65 | $report = $this->lighthouse 66 | ->performance() 67 | ->audit('http://example.com'); 68 | 69 | $this->assertReportIncludesCategory($report, 'Performance'); 70 | $this->assertReportDoesNotIncludeCategory($report, 'Accessibility'); 71 | 72 | $report = $this->lighthouse 73 | ->accessibility() 74 | ->audit('http://example.com'); 75 | 76 | $this->assertReportIncludesCategory($report, 'Performance'); 77 | $this->assertReportIncludesCategory($report, 'Accessibility'); 78 | 79 | $report = $this->lighthouse 80 | ->accessibility(false) 81 | ->audit('http://example.com'); 82 | 83 | $this->assertReportIncludesCategory($report, 'Performance'); 84 | $this->assertReportDoesNotIncludeCategory($report, 'Accessibility'); 85 | } 86 | 87 | /** @test */ 88 | public function categories_override_config() 89 | { 90 | $config = $this->createLighthouseConfig('performance'); 91 | 92 | try { 93 | $report = $this->lighthouse 94 | ->withConfig($config) 95 | ->accessibility() 96 | ->performance(false) 97 | ->audit('http://example.com'); 98 | } catch (AuditFailedException $e) { 99 | echo $e->getOutput(); 100 | } 101 | 102 | file_put_contents('/tmp/report', $report); 103 | 104 | $this->assertReportIncludesCategory($report, 'Accessibility'); 105 | $this->assertReportDoesNotIncludeCategory($report, 'Performance'); 106 | } 107 | 108 | /** @test */ 109 | public function throws_an_exception_when_the_audit_fails() 110 | { 111 | $this->expectException(AuditFailedException::class); 112 | 113 | $this->lighthouse 114 | ->performance() 115 | ->audit('not-a-valid-url'); 116 | } 117 | 118 | /** 119 | * @test 120 | * @dataProvider fileOutputDataProvider 121 | */ 122 | public function outputs_to_a_file($outputPath, $content) 123 | { 124 | $this->removeTempFile($outputPath); 125 | 126 | $this->lighthouse 127 | ->setOutput($outputPath) 128 | ->performance() 129 | ->audit('http://example.com'); 130 | 131 | $this->assertFileExists($outputPath); 132 | $this->assertFileStartsWith($content, $outputPath); 133 | } 134 | 135 | /** @test */ 136 | public function outputs_both_json_and_html_reports_at_the_same_time() 137 | { 138 | $this->removeTempFile('/tmp/example.report.json')->removeTempFile('/tmp/example.report.html'); 139 | 140 | $this->lighthouse 141 | ->setOutput('/tmp/example', ['json', 'html']) 142 | ->performance() 143 | ->audit('http://example.com'); 144 | 145 | $this->assertFileExists('/tmp/example.report.html'); 146 | $this->assertFileExists('/tmp/example.report.json'); 147 | } 148 | 149 | /** @test */ 150 | public function passes_the_http_headers_to_the_requests() 151 | { 152 | $report = $this->lighthouse 153 | ->setHeaders(['Cookie' => 'monster:blue', 'Authorization' => 'Bearer: ring']) 154 | ->performance() 155 | ->audit('http://example.com'); 156 | 157 | $this->assertReportContainsHeader($report, 'Cookie', 'monster:blue'); 158 | $this->assertReportContainsHeader($report, 'Authorization', 'Bearer: ring'); 159 | } 160 | 161 | /** @test */ 162 | public function accepts_an_array_with_the_config() 163 | { 164 | $report = $this->lighthouse 165 | ->withConfig([ 166 | 'extends' => 'lighthouse:default', 167 | 'settings' => [ 168 | 'onlyCategories' => ['pwa'], 169 | ], 170 | ]) 171 | ->audit('http://example.com'); 172 | 173 | $this->assertReportIncludesCategory($report, 'Progressive Web App'); 174 | $this->assertReportDoesNotIncludeCategory($report, 'Performance'); 175 | } 176 | 177 | /** @test */ 178 | public function does_not_remove_the_provided_config_file() 179 | { 180 | $configPath = '/tmp/test-config.js'; 181 | 182 | file_put_contents($configPath, 'module.exports = ' . json_encode([ 183 | 'extends' => 'lighthouse:default', 184 | 'settings' => [ 185 | 'onlyCategories' => ['performance'], 186 | ], 187 | ])); 188 | 189 | $this->lighthouse 190 | ->withConfig($configPath) 191 | ->audit('http://example.com'); 192 | 193 | $this->lighthouse = null; 194 | 195 | $this->assertFileExists($configPath); 196 | } 197 | 198 | protected function assertReportIncludesCategory($report, $expectedCategory) 199 | { 200 | $report = json_decode($report, true); 201 | $categories = array_map(function ($category) { 202 | return $category['title']; 203 | }, $report['categories']); 204 | 205 | if (is_array($expectedCategory)) { 206 | sort($expectedCategory); 207 | sort($categories); 208 | Assert::assertArraySubset($expectedCategory, $categories); 209 | } else { 210 | $this->assertContains($expectedCategory, $categories); 211 | } 212 | } 213 | 214 | protected function assertReportDoesNotIncludeCategory($report, $expectedCategory) 215 | { 216 | $report = json_decode($report, true); 217 | $categories = array_map(function ($category) { 218 | return $category['title']; 219 | }, $report['categories']); 220 | 221 | $this->assertNotContains($expectedCategory, $categories); 222 | } 223 | 224 | protected function assertReportContainsHeader($report, $name, $value) 225 | { 226 | $report = json_decode($report, true); 227 | 228 | $headers = $report['configSettings']['extraHeaders']; 229 | $this->assertNotEmpty($headers, 'No extra headers found in report'); 230 | $this->assertArrayHasKey($name, $headers, "Header '$name' is missing from report. [" . implode(', ', $headers) . ']'); 231 | $this->assertEquals($value, $headers[$name]); 232 | } 233 | 234 | protected function removeTempFile($path) 235 | { 236 | if (file_exists($path)) { 237 | unlink($path); 238 | } 239 | 240 | return $this; 241 | } 242 | 243 | private function assertFileStartsWith($prefix, $outputPath) 244 | { 245 | $this->assertStringStartsWith( 246 | $prefix, 247 | file_get_contents($outputPath), 248 | "Failed asserting that the file '$outputPath' starts with '$prefix'" 249 | ); 250 | 251 | return $this; 252 | } 253 | 254 | public function fileOutputDataProvider() 255 | { 256 | return [ 257 | 'json' => ['/tmp/report.json', '{'], 258 | 'html' => ['/tmp/report.html', '