├── .github └── workflows │ └── tests.yml ├── CHANGELOG.md ├── LICENSE ├── composer.json ├── ecs.php ├── rector.php └── src ├── ServerTiming ├── QueryTimer.php ├── Stopwatch.php └── StopwatchInterface.php └── ServerTimingMiddleware.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | strategy: 6 | matrix: 7 | operating-system: [ubuntu-latest] 8 | php-versions: ["7.2", "7.3", "7.4", "8.0", "8.1"] 9 | runs-on: ${{ matrix.operating-system }} 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | - name: Setup PHP and extensions 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: ${{ matrix.php-versions }} 17 | extensions: gmp 18 | coverage: xdebug 19 | #coverage: pcov 20 | - name: Set composer cache 21 | id: composer-cache 22 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 23 | - name: Cache composer dependencies 24 | uses: actions/cache@v2 25 | with: 26 | path: ${{ steps.composer-cache.outputs.dir }} 27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 28 | #key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 29 | restore-keys: ${{ runner.os }}-composer- 30 | - name: Install dependencies 31 | run: composer install --no-progress --prefer-dist --optimize-autoloader 32 | - name: Run linter 33 | run: make lint 34 | - name: Run unit tests 35 | run: make unit 36 | - name: Run static analysis 37 | run: make static 38 | - name: Upload coverage 39 | uses: codecov/codecov-action@v1 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | 6 | ## [0.11.0](https://github.com/tuupola/server-timing-middleware/compare/0.10.0...master) - unreleased 7 | 8 | ### Changed 9 | - PHP 7.2 is now minimum requirement ([#21](https://github.com/tuupola/server-timing-middleware/pull/21)). 10 | 11 | ## [0.10.0](https://github.com/tuupola/server-timing-middleware/compare/0.9.1...0.10.0) - 2022-05-13 12 | 13 | ### Added 14 | - Support for `symfony/stopwatch:^6.0` ([#20](https://github.com/tuupola/server-timing-middleware/pull/20)). 15 | 16 | ### Removed 17 | - Support for `symfony/stopwatch:^3.0` ([#20](https://github.com/tuupola/server-timing-middleware/pull/20)). 18 | - Support for `tuupola/callable-handler:^0.3.0` ([#20](https://github.com/tuupola/server-timing-middleware/pull/20)). 19 | 20 | ## [0.9.1](https://github.com/tuupola/server-timing-middleware/compare/0.9.0...0.9.1) - 2021-04-05 21 | 22 | ### Added 23 | - Support for `symfony/stopwatch:^5.0` ([#15](https://github.com/tuupola/server-timing-middleware/pull/15)). 24 | 25 | ## [0.9.0](https://github.com/tuupola/server-timing-middleware/compare/0.8.2...0.9.0) - 2020-12-01 26 | 27 | ### Added 28 | - Allow installing with PHP 8 ([#11](https://github.com/tuupola/server-timing-middleware/pull/11)). 29 | 30 | ## [0.8.2](https://github.com/tuupola/server-timing-middleware/compare/0.8.1...0.8.2) - 2018-10-23 31 | ### Added 32 | - Support for `tuupola/callable-handler:^1.0`. 33 | 34 | ## [0.8.1](https://github.com/tuupola/server-timing-middleware/compare/0.8.0...0.8.1) - 2018-08-08 35 | ### Changed 36 | - Use stable version of PSR-17 in tests. 37 | 38 | ## [0.8.0](https://github.com/tuupola/server-timing-middleware/compare/0.7.0...0.8.0) - 2018-04-24 39 | ### Changed 40 | - New header format as implemented in Chrome 66 ([#5](https://github.com/tuupola/server-timing-middleware/issues/5)) ([#8](https://github.com/tuupola/server-timing-middleware/pull/8)) 41 | - Removed unused options from `Tuupola\Middleware\ServerTiming` constructor. 42 | 43 | 44 | ## [0.7.0](https://github.com/tuupola/server-timing-middleware/compare/0.6.0...0.7.0) - 2018-01-25 45 | ### Added 46 | - Support for the [approved version of PSR-15](https://github.com/php-fig/http-server-middleware). 47 | 48 | ## [0.6.0](https://github.com/tuupola/server-timing-middleware/compare/0.5.0...0.6.0) - 2017-12-27 49 | ### Added 50 | - Support for the [latest version of PSR-15](https://github.com/http-interop/http-server-middleware). 51 | - Possibility to rename or disable default timings via options array. 52 | ```php 53 | $app->add(new ServerTimingMiddleware($stopwatch, [ 54 | "bootstrap" => "Startup", 55 | "process" => null, 56 | "total" => "Sum" 57 | ]); 58 | ```` 59 | 60 | ### Changed 61 | - Classname changed from ServerTiming to ServerTimingMiddleware. 62 | - ServerTimingMiddleware is now declared final. 63 | - PSR-7 double pass is now supported via [tuupola/callable-handler](https://github.com/tuupola/callable-handler) library. 64 | - PHP 7.1 is now minimum requirement. 65 | 66 | ### Removed 67 | - PSR-15 is now PHP 7.x only. Support for PHP 5.X was removed. 68 | 69 | ## [0.5.0](https://github.com/tuupola/server-timing-middleware/compare/0.4.0...0.5.0) - 2017-07-30 70 | ### Added 71 | - StopwatchInterface to enable custom stopwatch implementations ([#3](https://github.com/tuupola/server-timing-middleware/pull/3)). 72 | 73 | ### Changed 74 | - Stopwatch instance is now protected instead of private ([#3](https://github.com/tuupola/server-timing-middleware/pull/3)). 75 | 76 | ## [0.4.0](https://github.com/tuupola/server-timing-middleware/compare/0.3.0...0.4.0) - 2017-05-11 77 | ### Changed 78 | - Values are now in [milliseconds]((https://codereview.chromium.org/2689833002)) as required by Chrome 58. 79 | 80 | ## [0.3.0](https://github.com/tuupola/server-timing-middleware/compare/0.3.0...0.2.0) - 2017-03-09 81 | ### Added 82 | - `QueryTimer` class for Doctrine DBAL which can be used to automatically get SQL timings. 83 | 84 | ## 0.2.0 - 2017-03-08 85 | Initial realese. Supports both PSR-7 and PSR-15 style middlewares. Both have unit tests. However PSR-15 has not really been tested in production. 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2022 Mika Tuupola 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tuupola/server-timing-middleware", 3 | "description": "PSR-7 and PSR-15 server timing middleware", 4 | "keywords": [ 5 | "PSR-7", 6 | "PSR-15", 7 | "middleware" 8 | ], 9 | "homepage": "https://github.com/tuupola/server-timing-middleware", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Mika Tuupola", 14 | "email": "tuupola@appelsiini.net", 15 | "homepage": "https://appelsiini.net/", 16 | "role": "Developer" 17 | } 18 | ], 19 | "config": { 20 | "sort-packages": true 21 | }, 22 | "require": { 23 | "php": "^7.2|^8.0", 24 | "phpstan/phpstan": "^1.8", 25 | "psr/http-server-middleware": "^1.0", 26 | "symfony/stopwatch": "^4.0|^5.0|^6.0", 27 | "tuupola/callable-handler": "^1.0" 28 | }, 29 | "require-dev": { 30 | "doctrine/dbal": "^2.0", 31 | "equip/dispatch": "^2.0", 32 | "laminas/laminas-diactoros": "^2.4", 33 | "overtrue/phplint": "^0.2.0", 34 | "phpunit/phpunit": "^7.0|^8.0|^9.0", 35 | "psr/http-message": "^1.0.1", 36 | "rector/rector": "^0.14.0", 37 | "symplify/easy-coding-standard": "^11.1", 38 | "tuupola/http-factory": "^1.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Tuupola\\Middleware\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Tuupola\\Middleware\\": "tests" 48 | } 49 | }, 50 | "suggest": { 51 | "doctrine/dbal": "If you want to use the DBAL query timer." 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([__DIR__ . '/src', __DIR__ . '/tests']); 11 | 12 | $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, [ 13 | 'syntax' => 'short', 14 | ]); 15 | 16 | $ecsConfig->sets([ 17 | SetList::SPACES, 18 | SetList::ARRAY, 19 | SetList::DOCBLOCK, 20 | SetList::PSR_12, 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 12 | __DIR__ . "/src", 13 | __DIR__ . "/tests", 14 | __DIR__ . "/demo", 15 | ]); 16 | 17 | $rectorConfig->sets([ 18 | LevelSetList::UP_TO_PHP_72, 19 | // SetList::CODE_QUALITY, 20 | // SetList::DEAD_CODE, 21 | // SetList::PRIVATIZATION, 22 | // SetList::NAMING, 23 | // SetList::TYPE_DECLARATION, 24 | // SetList::EARLY_RETURN, 25 | // SetList::TYPE_DECLARATION_STRICT, 26 | // PHPUnitSetList::PHPUNIT_CODE_QUALITY, 27 | // PHPUnitSetList::PHPUNIT_90, 28 | // SetList::CODING_STYLE, 29 | ]); 30 | }; 31 | -------------------------------------------------------------------------------- /src/ServerTiming/QueryTimer.php: -------------------------------------------------------------------------------- 1 | stopwatch = $stopwatch; 47 | } 48 | 49 | /** 50 | * @param mixed[] $params 51 | * @param mixed[] $types 52 | */ 53 | public function startQuery($sql, array $params = null, array $types = null): void 54 | { 55 | $this->stopwatch->start("SQL"); 56 | } 57 | 58 | public function stopQuery(): void 59 | { 60 | $this->stopwatch->stop("SQL"); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ServerTiming/Stopwatch.php: -------------------------------------------------------------------------------- 1 | stopwatch = new SymfonyStopWatch(); 63 | } 64 | 65 | public function start(string $key): StopwatchInterface 66 | { 67 | $this->stopwatch->start($key); 68 | array_push($this->keys, $key); 69 | return $this; 70 | } 71 | 72 | public function stop(string $key): StopwatchInterface 73 | { 74 | if ($this->stopwatch->isStarted($key)) { 75 | $event = $this->stopwatch->stop($key); 76 | $duration = $event->getDuration(); 77 | $this->memory = $event->getMemory(); 78 | $this->set($key, (int) $duration); 79 | } 80 | return $this; 81 | } 82 | 83 | public function stopAll(): StopwatchInterface 84 | { 85 | foreach ($this->keys as $key) { 86 | $this->stop($key); 87 | } 88 | return $this; 89 | } 90 | 91 | /** 92 | * @return mixed 93 | */ 94 | public function closure(string $key, Closure $function) 95 | { 96 | $this->start($key); 97 | $return = $function(); 98 | $this->stop($key); 99 | return $return; 100 | } 101 | 102 | public function set(string $key, $value): StopwatchInterface 103 | { 104 | /* Allow calling $timing->set("fly", function () {...}) */ 105 | if ($value instanceof Closure) { 106 | $this->closure($key, $value); 107 | } else { 108 | $this->values[$key] = $value; 109 | } 110 | return $this; 111 | } 112 | 113 | public function get(string $key): ?int 114 | { 115 | if (isset($this->values[$key])) { 116 | return $this->values[$key]; 117 | } 118 | return null; 119 | } 120 | 121 | public function stopwatch(): SymfonyStopWatch 122 | { 123 | return $this->stopwatch; 124 | } 125 | 126 | public function memory(): ?int 127 | { 128 | return $this->memory; 129 | } 130 | 131 | /** 132 | * @return int[] 133 | */ 134 | public function values(): array 135 | { 136 | return $this->values; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/ServerTiming/StopwatchInterface.php: -------------------------------------------------------------------------------- 1 | start = $_SERVER["REQUEST_TIME_FLOAT"] ?? microtime(true); 80 | 81 | if (null === $stopwatch) { 82 | $stopwatch = new Stopwatch(); 83 | } 84 | $this->stopwatch = $stopwatch; 85 | 86 | /* Store passed in options overwriting any defaults. */ 87 | $this->hydrate($options); 88 | } 89 | 90 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 91 | { 92 | /* Time spent from starting the request to entering this middleware. */ 93 | if ($this->bootstrap) { 94 | $bootstrap = (microtime(true) - $this->start) * 1000; 95 | $this->stopwatch->set($this->bootstrap, (int) $bootstrap); 96 | } 97 | 98 | /* Call all the other middlewares. */ 99 | if ($this->process) { 100 | $this->stopwatch->start($this->process); 101 | } 102 | $response = $handler->handle($request); 103 | if ($this->process) { 104 | $this->stopwatch->stop($this->process); 105 | } 106 | 107 | /* Time spent from starting the request to exiting last middleware. */ 108 | if ($this->total) { 109 | $total = (microtime(true) - $this->start) * 1000; 110 | $this->stopwatch->set($this->total, (int) $total); 111 | } 112 | $this->stopwatch->stopAll(); 113 | 114 | return $response->withHeader( 115 | "Server-Timing", 116 | $this->generateHeader($this->stopwatch->values()) 117 | ); 118 | } 119 | 120 | /** 121 | * @param int[] $values 122 | */ 123 | private function generateHeader(array $values): string 124 | { 125 | /* https://tools.ietf.org/html/rfc7230#section-3.2.6 */ 126 | $regex = "/[^[:alnum:]!#$%&\'*\/+\-.^_`|~]/"; 127 | $header = ""; 128 | foreach ($values as $description => $timing) { 129 | if (preg_match($regex, $description)) { 130 | $token = preg_replace($regex, "", $description); 131 | if (null !== $token) { 132 | $token = strtolower(trim($token, "-")); 133 | $header .= sprintf('%s;dur=%d;desc="%s", ', $token, $timing, $description); 134 | } 135 | } else { 136 | $header .= sprintf("%s;dur=%d, ", $description, $timing); 137 | } 138 | }; 139 | return $header = (string) preg_replace("/, $/", "", $header); 140 | } 141 | 142 | /** 143 | * Hydrate all options from the given array. 144 | * 145 | * @param mixed[] $data 146 | */ 147 | private function hydrate(array $data = []): void 148 | { 149 | foreach ($data as $key => $value) { 150 | /* https://github.com/facebook/hhvm/issues/6368 */ 151 | $key = str_replace(".", " ", $key); 152 | $method = "set" . ucwords($key); 153 | $method = str_replace(" ", "", $method); 154 | if (method_exists($this, $method)) { 155 | /* Try to use setter */ 156 | /** @phpstan-ignore-next-line */ 157 | call_user_func([$this, $method], $value); 158 | } else { 159 | /* Or fallback to setting option directly */ 160 | $this->{$key} = $value; 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Set description for bootstrap or null to disable. 167 | */ 168 | private function setBootstrap(?string $bootstrap): void 169 | { 170 | $this->bootstrap = $bootstrap; 171 | } 172 | 173 | /** 174 | * Set description for process or null to disable. 175 | */ 176 | private function setProcess(?string $process): void 177 | { 178 | $this->process = $process; 179 | } 180 | 181 | /** 182 | * Set description for total or null to disable. 183 | */ 184 | private function setTotal(?string $total): void 185 | { 186 | $this->total = $total; 187 | } 188 | } 189 | --------------------------------------------------------------------------------