├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── phpcs.xml ├── phpunit.xml ├── src ├── ApiProblemRenderer.php ├── HalRenderer.php ├── Renderer.php └── SimplePsrStream.php └── tests ├── ApiProblemRendererTest.php ├── HalRendererTest.php ├── NonRewindableStream.php ├── RendererTest.php ├── Support └── SerializableClass.php └── bootstrap.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - '[0-9]+.[0-9]+.x' 7 | - 'refs/pull/*' 8 | jobs: 9 | ci: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | dependencies: 15 | - "lowest" 16 | - "highest" 17 | php-version: 18 | - "7.3" 19 | - "7.4" 20 | - "8.0" 21 | - "8.1" 22 | - "8.2" 23 | - "8.3" 24 | operating-system: 25 | - "ubuntu-latest" 26 | 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v2 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Install PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | coverage: pcov 37 | php-version: ${{ matrix.php-version }} 38 | ini-values: memory_limit=-1 39 | tools: composer:v2, cs2pr 40 | 41 | - name: Get Composer cache directory 42 | id: composer-cache 43 | run: | 44 | echo "::set-output name=dir::$(composer config cache-files-dir)" 45 | 46 | - name: Cache dependencies 47 | uses: actions/cache@v2 48 | with: 49 | path: | 50 | ${{ steps.composer-cache.outputs.dir }} 51 | vendor 52 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 53 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 54 | 55 | - name: Install lowest dependencies 56 | if: ${{ matrix.dependencies == 'lowest' }} 57 | run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" 58 | 59 | - name: Install highest dependencies 60 | if: ${{ matrix.dependencies == 'highest' }} 61 | run: "composer update --no-interaction --no-progress --no-suggest" 62 | 63 | - name: Run phpcs 64 | run: "vendor/bin/phpcs -q --report=checkstyle | cs2pr" 65 | 66 | - name: Run PHPUnit 67 | run: "vendor/bin/phpunit" 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /vendor 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Rob Allen . All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Render output based on content-type 2 | 3 | Render an array (or HAL object) to a JSON/XML/HTML PSR-7 Response based on a PSR-7 Request's Accept header. 4 | 5 | [![Build status][Master image]][Master] 6 | 7 | ## Installation 8 | 9 | `composer require akrabat/rka-content-type-renderer` 10 | 11 | ## Usage 12 | 13 | ```php 14 | // given: 15 | // $request instanceof Psr\Http\Message\RequestInterface 16 | // $response instanceof Psr\Http\Message\ResponseInterface 17 | 18 | $data = [ 19 | 'items' => [ 20 | [ 21 | 'name' => 'Alex', 22 | 'is_admin' => true, 23 | ], 24 | [ 25 | 'name' => 'Robin', 26 | 'is_admin' => false, 27 | ], 28 | ], 29 | ]; 30 | $renderer = new RKA\ContentTypeRenderer\Renderer(); 31 | $response = $renderer->render($request, $response, $data); 32 | return $response->withStatus(200); 33 | ``` 34 | 35 | The constructor takes a parameter, `$pretty` that defaults to `true`. Set to `false` to disable pretty printing. 36 | 37 | ## HalRenderer 38 | 39 | This component also supports [nocarrier/hal][hal] objects with the `HalRenderer`: 40 | 41 | ```php 42 | $hal = new Nocarrier\Hal( 43 | '/foo', 44 | [ 45 | 'items' => [ 46 | [ 47 | 'name' => 'Alex', 48 | 'is_admin' => true, 49 | ], 50 | [ 51 | 'name' => 'Robin', 52 | 'is_admin' => false, 53 | ], 54 | ], 55 | ] 56 | ); 57 | $renderer = new RKA\ContentTypeRenderer\HalRenderer(); 58 | $response = $renderer->render($request, $response, $hal); 59 | return $response->withStatus(200); 60 | ``` 61 | 62 | ## ApiRenderer 63 | 64 | This component also supports [crell/ApiProblem][ApiProblem] objects with the `ApiProblemRenderer`: 65 | 66 | ```php 67 | $problem = new Crell\ApiProblem("Something unexpected happened"); 68 | $renderer = new RKA\ContentTypeRenderer\ApiProblemRenderer(); 69 | $response = $renderer->render($request, $response, $problem); 70 | return $response->withStatus(500); 71 | ``` 72 | 73 | ## Arrays of objects 74 | 75 | If you have an array of objects, then the renderer will still work as long 76 | as the objects implement PHP's JsonSerializable interface. 77 | 78 | ## Testing 79 | 80 | * Code style: ``$ phpcs`` 81 | * Unit tests: ``$ phpunit`` 82 | * Code coverage: ``$ phpunit --coverage-html ./build`` 83 | 84 | 85 | 86 | [Master]: https://travis-ci.org/akrabat/rka-content-type-renderer 87 | [Master image]: https://secure.travis-ci.org/akrabat/rka-content-type-renderer.svg?branch=master 88 | [hal]: https://github.com/blongden/hal 89 | [ApiProblem]: https://github.com/Crell/ApiProblem 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "akrabat/rka-content-type-renderer", 3 | "description": "Render an array to a JSON/XML/HTML PSR-7 Response based on a PSR-7 Request's Accept header.", 4 | "keywords": [ 5 | "psr7", "json", "xml" 6 | ], 7 | "homepage": "http://github.com/akrabat/rka-content-type-renderer", 8 | "type": "library", 9 | "license": "BSD-3-Clause", 10 | "authors": [ 11 | { 12 | "name": "Rob Allen", 13 | "email": "rob@akrabat.com", 14 | "homepage": "http://akrabat.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.3 || ^8.0", 19 | "psr/http-message": "^1.0", 20 | "willdurand/negotiation": "^3.0" 21 | }, 22 | "suggest": { 23 | "crell/api-problem": "For creating Api-Problem (RFC7801) data that can be rendered using ApiProblemRenderer", 24 | "nocarrier/hal": "For creating HAL documents that can be rendered using HalRenderer" 25 | }, 26 | "require-dev": { 27 | "crell/api-problem": "^3.2", 28 | "nocarrier/hal": "^0.9.12", 29 | "phpunit/phpunit": "^9.0", 30 | "squizlabs/php_codesniffer": "^3.5", 31 | "laminas/laminas-diactoros": "^2.8" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "RKA\\ContentTypeRenderer\\": "src" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "d91593c6414a628210705c4b89a2ff7b", 8 | "packages": [ 9 | { 10 | "name": "psr/http-message", 11 | "version": "1.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/php-fig/http-message.git", 15 | "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", 20 | "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^7.2 || ^8.0" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.1.x-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Psr\\Http\\Message\\": "src/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "PHP-FIG", 44 | "homepage": "http://www.php-fig.org/" 45 | } 46 | ], 47 | "description": "Common interface for HTTP messages", 48 | "homepage": "https://github.com/php-fig/http-message", 49 | "keywords": [ 50 | "http", 51 | "http-message", 52 | "psr", 53 | "psr-7", 54 | "request", 55 | "response" 56 | ], 57 | "support": { 58 | "source": "https://github.com/php-fig/http-message/tree/1.1" 59 | }, 60 | "time": "2023-04-04T09:50:52+00:00" 61 | }, 62 | { 63 | "name": "willdurand/negotiation", 64 | "version": "3.1.0", 65 | "source": { 66 | "type": "git", 67 | "url": "https://github.com/willdurand/Negotiation.git", 68 | "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2" 69 | }, 70 | "dist": { 71 | "type": "zip", 72 | "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", 73 | "reference": "68e9ea0553ef6e2ee8db5c1d98829f111e623ec2", 74 | "shasum": "" 75 | }, 76 | "require": { 77 | "php": ">=7.1.0" 78 | }, 79 | "require-dev": { 80 | "symfony/phpunit-bridge": "^5.0" 81 | }, 82 | "type": "library", 83 | "extra": { 84 | "branch-alias": { 85 | "dev-master": "3.0-dev" 86 | } 87 | }, 88 | "autoload": { 89 | "psr-4": { 90 | "Negotiation\\": "src/Negotiation" 91 | } 92 | }, 93 | "notification-url": "https://packagist.org/downloads/", 94 | "license": [ 95 | "MIT" 96 | ], 97 | "authors": [ 98 | { 99 | "name": "William Durand", 100 | "email": "will+git@drnd.me" 101 | } 102 | ], 103 | "description": "Content Negotiation tools for PHP provided as a standalone library.", 104 | "homepage": "http://williamdurand.fr/Negotiation/", 105 | "keywords": [ 106 | "accept", 107 | "content", 108 | "format", 109 | "header", 110 | "negotiation" 111 | ], 112 | "support": { 113 | "issues": "https://github.com/willdurand/Negotiation/issues", 114 | "source": "https://github.com/willdurand/Negotiation/tree/3.1.0" 115 | }, 116 | "time": "2022-01-30T20:08:53+00:00" 117 | } 118 | ], 119 | "packages-dev": [ 120 | { 121 | "name": "crell/api-problem", 122 | "version": "3.6.1", 123 | "source": { 124 | "type": "git", 125 | "url": "https://github.com/Crell/ApiProblem.git", 126 | "reference": "5acb0a8cc13ea740f631a60e5e73271c18e45803" 127 | }, 128 | "dist": { 129 | "type": "zip", 130 | "url": "https://api.github.com/repos/Crell/ApiProblem/zipball/5acb0a8cc13ea740f631a60e5e73271c18e45803", 131 | "reference": "5acb0a8cc13ea740f631a60e5e73271c18e45803", 132 | "shasum": "" 133 | }, 134 | "require": { 135 | "php": "^7.1 || ^8.0" 136 | }, 137 | "require-dev": { 138 | "laminas/laminas-diactoros": "^2.0", 139 | "phpstan/phpstan": "^1.3", 140 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", 141 | "psr/http-factory": "^1.0", 142 | "psr/http-message": "1.*" 143 | }, 144 | "suggest": { 145 | "psr/http-factory": "Common interfaces for PSR-7 HTTP message factories", 146 | "psr/http-message": "Common interface for HTTP messages" 147 | }, 148 | "type": "library", 149 | "extra": { 150 | "branch-alias": { 151 | "dev-master": "2.0.x-dev" 152 | } 153 | }, 154 | "autoload": { 155 | "psr-4": { 156 | "Crell\\ApiProblem\\": "src/" 157 | } 158 | }, 159 | "notification-url": "https://packagist.org/downloads/", 160 | "license": [ 161 | "MIT" 162 | ], 163 | "authors": [ 164 | { 165 | "name": "Larry Garfield", 166 | "email": "larry@garfieldtech.com", 167 | "homepage": "http://www.garfieldtech.com/" 168 | } 169 | ], 170 | "description": "PHP wrapper for the api-problem IETF specification", 171 | "homepage": "https://github.com/Crell/ApiProblem", 172 | "keywords": [ 173 | "api-problem", 174 | "http", 175 | "json", 176 | "rest", 177 | "xml" 178 | ], 179 | "support": { 180 | "issues": "https://github.com/Crell/ApiProblem/issues", 181 | "source": "https://github.com/Crell/ApiProblem/tree/3.6.1" 182 | }, 183 | "funding": [ 184 | { 185 | "url": "https://github.com/Crell", 186 | "type": "github" 187 | } 188 | ], 189 | "time": "2022-01-04T15:47:30+00:00" 190 | }, 191 | { 192 | "name": "doctrine/instantiator", 193 | "version": "2.0.0", 194 | "source": { 195 | "type": "git", 196 | "url": "https://github.com/doctrine/instantiator.git", 197 | "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" 198 | }, 199 | "dist": { 200 | "type": "zip", 201 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", 202 | "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", 203 | "shasum": "" 204 | }, 205 | "require": { 206 | "php": "^8.1" 207 | }, 208 | "require-dev": { 209 | "doctrine/coding-standard": "^11", 210 | "ext-pdo": "*", 211 | "ext-phar": "*", 212 | "phpbench/phpbench": "^1.2", 213 | "phpstan/phpstan": "^1.9.4", 214 | "phpstan/phpstan-phpunit": "^1.3", 215 | "phpunit/phpunit": "^9.5.27", 216 | "vimeo/psalm": "^5.4" 217 | }, 218 | "type": "library", 219 | "autoload": { 220 | "psr-4": { 221 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 222 | } 223 | }, 224 | "notification-url": "https://packagist.org/downloads/", 225 | "license": [ 226 | "MIT" 227 | ], 228 | "authors": [ 229 | { 230 | "name": "Marco Pivetta", 231 | "email": "ocramius@gmail.com", 232 | "homepage": "https://ocramius.github.io/" 233 | } 234 | ], 235 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 236 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 237 | "keywords": [ 238 | "constructor", 239 | "instantiate" 240 | ], 241 | "support": { 242 | "issues": "https://github.com/doctrine/instantiator/issues", 243 | "source": "https://github.com/doctrine/instantiator/tree/2.0.0" 244 | }, 245 | "funding": [ 246 | { 247 | "url": "https://www.doctrine-project.org/sponsorship.html", 248 | "type": "custom" 249 | }, 250 | { 251 | "url": "https://www.patreon.com/phpdoctrine", 252 | "type": "patreon" 253 | }, 254 | { 255 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 256 | "type": "tidelift" 257 | } 258 | ], 259 | "time": "2022-12-30T00:23:10+00:00" 260 | }, 261 | { 262 | "name": "laminas/laminas-diactoros", 263 | "version": "2.26.0", 264 | "source": { 265 | "type": "git", 266 | "url": "https://github.com/laminas/laminas-diactoros.git", 267 | "reference": "6584d44eb8e477e89d453313b858daac6183cddc" 268 | }, 269 | "dist": { 270 | "type": "zip", 271 | "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/6584d44eb8e477e89d453313b858daac6183cddc", 272 | "reference": "6584d44eb8e477e89d453313b858daac6183cddc", 273 | "shasum": "" 274 | }, 275 | "require": { 276 | "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", 277 | "psr/http-factory": "^1.0", 278 | "psr/http-message": "^1.1" 279 | }, 280 | "conflict": { 281 | "zendframework/zend-diactoros": "*" 282 | }, 283 | "provide": { 284 | "psr/http-factory-implementation": "1.0", 285 | "psr/http-message-implementation": "1.0" 286 | }, 287 | "require-dev": { 288 | "ext-curl": "*", 289 | "ext-dom": "*", 290 | "ext-gd": "*", 291 | "ext-libxml": "*", 292 | "http-interop/http-factory-tests": "^0.9.0", 293 | "laminas/laminas-coding-standard": "^2.5", 294 | "php-http/psr7-integration-tests": "^1.2", 295 | "phpunit/phpunit": "^9.5.28", 296 | "psalm/plugin-phpunit": "^0.18.4", 297 | "vimeo/psalm": "^5.6" 298 | }, 299 | "type": "library", 300 | "extra": { 301 | "laminas": { 302 | "config-provider": "Laminas\\Diactoros\\ConfigProvider", 303 | "module": "Laminas\\Diactoros" 304 | } 305 | }, 306 | "autoload": { 307 | "files": [ 308 | "src/functions/create_uploaded_file.php", 309 | "src/functions/marshal_headers_from_sapi.php", 310 | "src/functions/marshal_method_from_sapi.php", 311 | "src/functions/marshal_protocol_version_from_sapi.php", 312 | "src/functions/marshal_uri_from_sapi.php", 313 | "src/functions/normalize_server.php", 314 | "src/functions/normalize_uploaded_files.php", 315 | "src/functions/parse_cookie_header.php", 316 | "src/functions/create_uploaded_file.legacy.php", 317 | "src/functions/marshal_headers_from_sapi.legacy.php", 318 | "src/functions/marshal_method_from_sapi.legacy.php", 319 | "src/functions/marshal_protocol_version_from_sapi.legacy.php", 320 | "src/functions/marshal_uri_from_sapi.legacy.php", 321 | "src/functions/normalize_server.legacy.php", 322 | "src/functions/normalize_uploaded_files.legacy.php", 323 | "src/functions/parse_cookie_header.legacy.php" 324 | ], 325 | "psr-4": { 326 | "Laminas\\Diactoros\\": "src/" 327 | } 328 | }, 329 | "notification-url": "https://packagist.org/downloads/", 330 | "license": [ 331 | "BSD-3-Clause" 332 | ], 333 | "description": "PSR HTTP Message implementations", 334 | "homepage": "https://laminas.dev", 335 | "keywords": [ 336 | "http", 337 | "laminas", 338 | "psr", 339 | "psr-17", 340 | "psr-7" 341 | ], 342 | "support": { 343 | "chat": "https://laminas.dev/chat", 344 | "docs": "https://docs.laminas.dev/laminas-diactoros/", 345 | "forum": "https://discourse.laminas.dev", 346 | "issues": "https://github.com/laminas/laminas-diactoros/issues", 347 | "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", 348 | "source": "https://github.com/laminas/laminas-diactoros" 349 | }, 350 | "funding": [ 351 | { 352 | "url": "https://funding.communitybridge.org/projects/laminas-project", 353 | "type": "community_bridge" 354 | } 355 | ], 356 | "time": "2023-10-29T16:17:44+00:00" 357 | }, 358 | { 359 | "name": "myclabs/deep-copy", 360 | "version": "1.11.1", 361 | "source": { 362 | "type": "git", 363 | "url": "https://github.com/myclabs/DeepCopy.git", 364 | "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" 365 | }, 366 | "dist": { 367 | "type": "zip", 368 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", 369 | "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", 370 | "shasum": "" 371 | }, 372 | "require": { 373 | "php": "^7.1 || ^8.0" 374 | }, 375 | "conflict": { 376 | "doctrine/collections": "<1.6.8", 377 | "doctrine/common": "<2.13.3 || >=3,<3.2.2" 378 | }, 379 | "require-dev": { 380 | "doctrine/collections": "^1.6.8", 381 | "doctrine/common": "^2.13.3 || ^3.2.2", 382 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 383 | }, 384 | "type": "library", 385 | "autoload": { 386 | "files": [ 387 | "src/DeepCopy/deep_copy.php" 388 | ], 389 | "psr-4": { 390 | "DeepCopy\\": "src/DeepCopy/" 391 | } 392 | }, 393 | "notification-url": "https://packagist.org/downloads/", 394 | "license": [ 395 | "MIT" 396 | ], 397 | "description": "Create deep copies (clones) of your objects", 398 | "keywords": [ 399 | "clone", 400 | "copy", 401 | "duplicate", 402 | "object", 403 | "object graph" 404 | ], 405 | "support": { 406 | "issues": "https://github.com/myclabs/DeepCopy/issues", 407 | "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" 408 | }, 409 | "funding": [ 410 | { 411 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 412 | "type": "tidelift" 413 | } 414 | ], 415 | "time": "2023-03-08T13:26:56+00:00" 416 | }, 417 | { 418 | "name": "nikic/php-parser", 419 | "version": "v5.0.2", 420 | "source": { 421 | "type": "git", 422 | "url": "https://github.com/nikic/PHP-Parser.git", 423 | "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" 424 | }, 425 | "dist": { 426 | "type": "zip", 427 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", 428 | "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", 429 | "shasum": "" 430 | }, 431 | "require": { 432 | "ext-ctype": "*", 433 | "ext-json": "*", 434 | "ext-tokenizer": "*", 435 | "php": ">=7.4" 436 | }, 437 | "require-dev": { 438 | "ircmaxell/php-yacc": "^0.0.7", 439 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" 440 | }, 441 | "bin": [ 442 | "bin/php-parse" 443 | ], 444 | "type": "library", 445 | "extra": { 446 | "branch-alias": { 447 | "dev-master": "5.0-dev" 448 | } 449 | }, 450 | "autoload": { 451 | "psr-4": { 452 | "PhpParser\\": "lib/PhpParser" 453 | } 454 | }, 455 | "notification-url": "https://packagist.org/downloads/", 456 | "license": [ 457 | "BSD-3-Clause" 458 | ], 459 | "authors": [ 460 | { 461 | "name": "Nikita Popov" 462 | } 463 | ], 464 | "description": "A PHP parser written in PHP", 465 | "keywords": [ 466 | "parser", 467 | "php" 468 | ], 469 | "support": { 470 | "issues": "https://github.com/nikic/PHP-Parser/issues", 471 | "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" 472 | }, 473 | "time": "2024-03-05T20:51:40+00:00" 474 | }, 475 | { 476 | "name": "nocarrier/hal", 477 | "version": "0.9.14", 478 | "source": { 479 | "type": "git", 480 | "url": "https://github.com/blongden/hal.git", 481 | "reference": "921be9bc987e62a1d7756aedec180795b16ce680" 482 | }, 483 | "dist": { 484 | "type": "zip", 485 | "url": "https://api.github.com/repos/blongden/hal/zipball/921be9bc987e62a1d7756aedec180795b16ce680", 486 | "reference": "921be9bc987e62a1d7756aedec180795b16ce680", 487 | "shasum": "" 488 | }, 489 | "require": { 490 | "php": ">=5.3.0", 491 | "psr/link": "~1.0" 492 | }, 493 | "require-dev": { 494 | "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" 495 | }, 496 | "type": "library", 497 | "autoload": { 498 | "psr-4": { 499 | "Nocarrier\\": "src" 500 | } 501 | }, 502 | "notification-url": "https://packagist.org/downloads/", 503 | "license": [ 504 | "MIT" 505 | ], 506 | "authors": [ 507 | { 508 | "name": "Ben Longden", 509 | "email": "ben@nocarrier.co.uk", 510 | "homepage": "http://nocarrier.co.uk" 511 | } 512 | ], 513 | "description": "application/hal builder / formatter for PHP 5.3+", 514 | "homepage": "https://github.com/blongden/hal", 515 | "keywords": [ 516 | "hal", 517 | "hypermedia", 518 | "json", 519 | "rest", 520 | "xml" 521 | ], 522 | "support": { 523 | "issues": "https://github.com/blongden/hal/issues", 524 | "source": "https://github.com/blongden/hal/tree/0.9.14" 525 | }, 526 | "time": "2021-07-08T10:14:23+00:00" 527 | }, 528 | { 529 | "name": "phar-io/manifest", 530 | "version": "2.0.4", 531 | "source": { 532 | "type": "git", 533 | "url": "https://github.com/phar-io/manifest.git", 534 | "reference": "54750ef60c58e43759730615a392c31c80e23176" 535 | }, 536 | "dist": { 537 | "type": "zip", 538 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", 539 | "reference": "54750ef60c58e43759730615a392c31c80e23176", 540 | "shasum": "" 541 | }, 542 | "require": { 543 | "ext-dom": "*", 544 | "ext-libxml": "*", 545 | "ext-phar": "*", 546 | "ext-xmlwriter": "*", 547 | "phar-io/version": "^3.0.1", 548 | "php": "^7.2 || ^8.0" 549 | }, 550 | "type": "library", 551 | "extra": { 552 | "branch-alias": { 553 | "dev-master": "2.0.x-dev" 554 | } 555 | }, 556 | "autoload": { 557 | "classmap": [ 558 | "src/" 559 | ] 560 | }, 561 | "notification-url": "https://packagist.org/downloads/", 562 | "license": [ 563 | "BSD-3-Clause" 564 | ], 565 | "authors": [ 566 | { 567 | "name": "Arne Blankerts", 568 | "email": "arne@blankerts.de", 569 | "role": "Developer" 570 | }, 571 | { 572 | "name": "Sebastian Heuer", 573 | "email": "sebastian@phpeople.de", 574 | "role": "Developer" 575 | }, 576 | { 577 | "name": "Sebastian Bergmann", 578 | "email": "sebastian@phpunit.de", 579 | "role": "Developer" 580 | } 581 | ], 582 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 583 | "support": { 584 | "issues": "https://github.com/phar-io/manifest/issues", 585 | "source": "https://github.com/phar-io/manifest/tree/2.0.4" 586 | }, 587 | "funding": [ 588 | { 589 | "url": "https://github.com/theseer", 590 | "type": "github" 591 | } 592 | ], 593 | "time": "2024-03-03T12:33:53+00:00" 594 | }, 595 | { 596 | "name": "phar-io/version", 597 | "version": "3.2.1", 598 | "source": { 599 | "type": "git", 600 | "url": "https://github.com/phar-io/version.git", 601 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 602 | }, 603 | "dist": { 604 | "type": "zip", 605 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 606 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 607 | "shasum": "" 608 | }, 609 | "require": { 610 | "php": "^7.2 || ^8.0" 611 | }, 612 | "type": "library", 613 | "autoload": { 614 | "classmap": [ 615 | "src/" 616 | ] 617 | }, 618 | "notification-url": "https://packagist.org/downloads/", 619 | "license": [ 620 | "BSD-3-Clause" 621 | ], 622 | "authors": [ 623 | { 624 | "name": "Arne Blankerts", 625 | "email": "arne@blankerts.de", 626 | "role": "Developer" 627 | }, 628 | { 629 | "name": "Sebastian Heuer", 630 | "email": "sebastian@phpeople.de", 631 | "role": "Developer" 632 | }, 633 | { 634 | "name": "Sebastian Bergmann", 635 | "email": "sebastian@phpunit.de", 636 | "role": "Developer" 637 | } 638 | ], 639 | "description": "Library for handling version information and constraints", 640 | "support": { 641 | "issues": "https://github.com/phar-io/version/issues", 642 | "source": "https://github.com/phar-io/version/tree/3.2.1" 643 | }, 644 | "time": "2022-02-21T01:04:05+00:00" 645 | }, 646 | { 647 | "name": "phpunit/php-code-coverage", 648 | "version": "9.2.31", 649 | "source": { 650 | "type": "git", 651 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 652 | "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" 653 | }, 654 | "dist": { 655 | "type": "zip", 656 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", 657 | "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", 658 | "shasum": "" 659 | }, 660 | "require": { 661 | "ext-dom": "*", 662 | "ext-libxml": "*", 663 | "ext-xmlwriter": "*", 664 | "nikic/php-parser": "^4.18 || ^5.0", 665 | "php": ">=7.3", 666 | "phpunit/php-file-iterator": "^3.0.3", 667 | "phpunit/php-text-template": "^2.0.2", 668 | "sebastian/code-unit-reverse-lookup": "^2.0.2", 669 | "sebastian/complexity": "^2.0", 670 | "sebastian/environment": "^5.1.2", 671 | "sebastian/lines-of-code": "^1.0.3", 672 | "sebastian/version": "^3.0.1", 673 | "theseer/tokenizer": "^1.2.0" 674 | }, 675 | "require-dev": { 676 | "phpunit/phpunit": "^9.3" 677 | }, 678 | "suggest": { 679 | "ext-pcov": "PHP extension that provides line coverage", 680 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 681 | }, 682 | "type": "library", 683 | "extra": { 684 | "branch-alias": { 685 | "dev-master": "9.2-dev" 686 | } 687 | }, 688 | "autoload": { 689 | "classmap": [ 690 | "src/" 691 | ] 692 | }, 693 | "notification-url": "https://packagist.org/downloads/", 694 | "license": [ 695 | "BSD-3-Clause" 696 | ], 697 | "authors": [ 698 | { 699 | "name": "Sebastian Bergmann", 700 | "email": "sebastian@phpunit.de", 701 | "role": "lead" 702 | } 703 | ], 704 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 705 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 706 | "keywords": [ 707 | "coverage", 708 | "testing", 709 | "xunit" 710 | ], 711 | "support": { 712 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 713 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", 714 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" 715 | }, 716 | "funding": [ 717 | { 718 | "url": "https://github.com/sebastianbergmann", 719 | "type": "github" 720 | } 721 | ], 722 | "time": "2024-03-02T06:37:42+00:00" 723 | }, 724 | { 725 | "name": "phpunit/php-file-iterator", 726 | "version": "3.0.6", 727 | "source": { 728 | "type": "git", 729 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 730 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" 731 | }, 732 | "dist": { 733 | "type": "zip", 734 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 735 | "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", 736 | "shasum": "" 737 | }, 738 | "require": { 739 | "php": ">=7.3" 740 | }, 741 | "require-dev": { 742 | "phpunit/phpunit": "^9.3" 743 | }, 744 | "type": "library", 745 | "extra": { 746 | "branch-alias": { 747 | "dev-master": "3.0-dev" 748 | } 749 | }, 750 | "autoload": { 751 | "classmap": [ 752 | "src/" 753 | ] 754 | }, 755 | "notification-url": "https://packagist.org/downloads/", 756 | "license": [ 757 | "BSD-3-Clause" 758 | ], 759 | "authors": [ 760 | { 761 | "name": "Sebastian Bergmann", 762 | "email": "sebastian@phpunit.de", 763 | "role": "lead" 764 | } 765 | ], 766 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 767 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 768 | "keywords": [ 769 | "filesystem", 770 | "iterator" 771 | ], 772 | "support": { 773 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 774 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" 775 | }, 776 | "funding": [ 777 | { 778 | "url": "https://github.com/sebastianbergmann", 779 | "type": "github" 780 | } 781 | ], 782 | "time": "2021-12-02T12:48:52+00:00" 783 | }, 784 | { 785 | "name": "phpunit/php-invoker", 786 | "version": "3.1.1", 787 | "source": { 788 | "type": "git", 789 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 790 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" 791 | }, 792 | "dist": { 793 | "type": "zip", 794 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 795 | "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", 796 | "shasum": "" 797 | }, 798 | "require": { 799 | "php": ">=7.3" 800 | }, 801 | "require-dev": { 802 | "ext-pcntl": "*", 803 | "phpunit/phpunit": "^9.3" 804 | }, 805 | "suggest": { 806 | "ext-pcntl": "*" 807 | }, 808 | "type": "library", 809 | "extra": { 810 | "branch-alias": { 811 | "dev-master": "3.1-dev" 812 | } 813 | }, 814 | "autoload": { 815 | "classmap": [ 816 | "src/" 817 | ] 818 | }, 819 | "notification-url": "https://packagist.org/downloads/", 820 | "license": [ 821 | "BSD-3-Clause" 822 | ], 823 | "authors": [ 824 | { 825 | "name": "Sebastian Bergmann", 826 | "email": "sebastian@phpunit.de", 827 | "role": "lead" 828 | } 829 | ], 830 | "description": "Invoke callables with a timeout", 831 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 832 | "keywords": [ 833 | "process" 834 | ], 835 | "support": { 836 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 837 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" 838 | }, 839 | "funding": [ 840 | { 841 | "url": "https://github.com/sebastianbergmann", 842 | "type": "github" 843 | } 844 | ], 845 | "time": "2020-09-28T05:58:55+00:00" 846 | }, 847 | { 848 | "name": "phpunit/php-text-template", 849 | "version": "2.0.4", 850 | "source": { 851 | "type": "git", 852 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 853 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" 854 | }, 855 | "dist": { 856 | "type": "zip", 857 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 858 | "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", 859 | "shasum": "" 860 | }, 861 | "require": { 862 | "php": ">=7.3" 863 | }, 864 | "require-dev": { 865 | "phpunit/phpunit": "^9.3" 866 | }, 867 | "type": "library", 868 | "extra": { 869 | "branch-alias": { 870 | "dev-master": "2.0-dev" 871 | } 872 | }, 873 | "autoload": { 874 | "classmap": [ 875 | "src/" 876 | ] 877 | }, 878 | "notification-url": "https://packagist.org/downloads/", 879 | "license": [ 880 | "BSD-3-Clause" 881 | ], 882 | "authors": [ 883 | { 884 | "name": "Sebastian Bergmann", 885 | "email": "sebastian@phpunit.de", 886 | "role": "lead" 887 | } 888 | ], 889 | "description": "Simple template engine.", 890 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 891 | "keywords": [ 892 | "template" 893 | ], 894 | "support": { 895 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 896 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" 897 | }, 898 | "funding": [ 899 | { 900 | "url": "https://github.com/sebastianbergmann", 901 | "type": "github" 902 | } 903 | ], 904 | "time": "2020-10-26T05:33:50+00:00" 905 | }, 906 | { 907 | "name": "phpunit/php-timer", 908 | "version": "5.0.3", 909 | "source": { 910 | "type": "git", 911 | "url": "https://github.com/sebastianbergmann/php-timer.git", 912 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" 913 | }, 914 | "dist": { 915 | "type": "zip", 916 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 917 | "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", 918 | "shasum": "" 919 | }, 920 | "require": { 921 | "php": ">=7.3" 922 | }, 923 | "require-dev": { 924 | "phpunit/phpunit": "^9.3" 925 | }, 926 | "type": "library", 927 | "extra": { 928 | "branch-alias": { 929 | "dev-master": "5.0-dev" 930 | } 931 | }, 932 | "autoload": { 933 | "classmap": [ 934 | "src/" 935 | ] 936 | }, 937 | "notification-url": "https://packagist.org/downloads/", 938 | "license": [ 939 | "BSD-3-Clause" 940 | ], 941 | "authors": [ 942 | { 943 | "name": "Sebastian Bergmann", 944 | "email": "sebastian@phpunit.de", 945 | "role": "lead" 946 | } 947 | ], 948 | "description": "Utility class for timing", 949 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 950 | "keywords": [ 951 | "timer" 952 | ], 953 | "support": { 954 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 955 | "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" 956 | }, 957 | "funding": [ 958 | { 959 | "url": "https://github.com/sebastianbergmann", 960 | "type": "github" 961 | } 962 | ], 963 | "time": "2020-10-26T13:16:10+00:00" 964 | }, 965 | { 966 | "name": "phpunit/phpunit", 967 | "version": "9.6.17", 968 | "source": { 969 | "type": "git", 970 | "url": "https://github.com/sebastianbergmann/phpunit.git", 971 | "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd" 972 | }, 973 | "dist": { 974 | "type": "zip", 975 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1a156980d78a6666721b7e8e8502fe210b587fcd", 976 | "reference": "1a156980d78a6666721b7e8e8502fe210b587fcd", 977 | "shasum": "" 978 | }, 979 | "require": { 980 | "doctrine/instantiator": "^1.3.1 || ^2", 981 | "ext-dom": "*", 982 | "ext-json": "*", 983 | "ext-libxml": "*", 984 | "ext-mbstring": "*", 985 | "ext-xml": "*", 986 | "ext-xmlwriter": "*", 987 | "myclabs/deep-copy": "^1.10.1", 988 | "phar-io/manifest": "^2.0.3", 989 | "phar-io/version": "^3.0.2", 990 | "php": ">=7.3", 991 | "phpunit/php-code-coverage": "^9.2.28", 992 | "phpunit/php-file-iterator": "^3.0.5", 993 | "phpunit/php-invoker": "^3.1.1", 994 | "phpunit/php-text-template": "^2.0.3", 995 | "phpunit/php-timer": "^5.0.2", 996 | "sebastian/cli-parser": "^1.0.1", 997 | "sebastian/code-unit": "^1.0.6", 998 | "sebastian/comparator": "^4.0.8", 999 | "sebastian/diff": "^4.0.3", 1000 | "sebastian/environment": "^5.1.3", 1001 | "sebastian/exporter": "^4.0.5", 1002 | "sebastian/global-state": "^5.0.1", 1003 | "sebastian/object-enumerator": "^4.0.3", 1004 | "sebastian/resource-operations": "^3.0.3", 1005 | "sebastian/type": "^3.2", 1006 | "sebastian/version": "^3.0.2" 1007 | }, 1008 | "suggest": { 1009 | "ext-soap": "To be able to generate mocks based on WSDL files", 1010 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 1011 | }, 1012 | "bin": [ 1013 | "phpunit" 1014 | ], 1015 | "type": "library", 1016 | "extra": { 1017 | "branch-alias": { 1018 | "dev-master": "9.6-dev" 1019 | } 1020 | }, 1021 | "autoload": { 1022 | "files": [ 1023 | "src/Framework/Assert/Functions.php" 1024 | ], 1025 | "classmap": [ 1026 | "src/" 1027 | ] 1028 | }, 1029 | "notification-url": "https://packagist.org/downloads/", 1030 | "license": [ 1031 | "BSD-3-Clause" 1032 | ], 1033 | "authors": [ 1034 | { 1035 | "name": "Sebastian Bergmann", 1036 | "email": "sebastian@phpunit.de", 1037 | "role": "lead" 1038 | } 1039 | ], 1040 | "description": "The PHP Unit Testing framework.", 1041 | "homepage": "https://phpunit.de/", 1042 | "keywords": [ 1043 | "phpunit", 1044 | "testing", 1045 | "xunit" 1046 | ], 1047 | "support": { 1048 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 1049 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy", 1050 | "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.17" 1051 | }, 1052 | "funding": [ 1053 | { 1054 | "url": "https://phpunit.de/sponsors.html", 1055 | "type": "custom" 1056 | }, 1057 | { 1058 | "url": "https://github.com/sebastianbergmann", 1059 | "type": "github" 1060 | }, 1061 | { 1062 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 1063 | "type": "tidelift" 1064 | } 1065 | ], 1066 | "time": "2024-02-23T13:14:51+00:00" 1067 | }, 1068 | { 1069 | "name": "psr/http-factory", 1070 | "version": "1.0.2", 1071 | "source": { 1072 | "type": "git", 1073 | "url": "https://github.com/php-fig/http-factory.git", 1074 | "reference": "e616d01114759c4c489f93b099585439f795fe35" 1075 | }, 1076 | "dist": { 1077 | "type": "zip", 1078 | "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", 1079 | "reference": "e616d01114759c4c489f93b099585439f795fe35", 1080 | "shasum": "" 1081 | }, 1082 | "require": { 1083 | "php": ">=7.0.0", 1084 | "psr/http-message": "^1.0 || ^2.0" 1085 | }, 1086 | "type": "library", 1087 | "extra": { 1088 | "branch-alias": { 1089 | "dev-master": "1.0.x-dev" 1090 | } 1091 | }, 1092 | "autoload": { 1093 | "psr-4": { 1094 | "Psr\\Http\\Message\\": "src/" 1095 | } 1096 | }, 1097 | "notification-url": "https://packagist.org/downloads/", 1098 | "license": [ 1099 | "MIT" 1100 | ], 1101 | "authors": [ 1102 | { 1103 | "name": "PHP-FIG", 1104 | "homepage": "https://www.php-fig.org/" 1105 | } 1106 | ], 1107 | "description": "Common interfaces for PSR-7 HTTP message factories", 1108 | "keywords": [ 1109 | "factory", 1110 | "http", 1111 | "message", 1112 | "psr", 1113 | "psr-17", 1114 | "psr-7", 1115 | "request", 1116 | "response" 1117 | ], 1118 | "support": { 1119 | "source": "https://github.com/php-fig/http-factory/tree/1.0.2" 1120 | }, 1121 | "time": "2023-04-10T20:10:41+00:00" 1122 | }, 1123 | { 1124 | "name": "psr/link", 1125 | "version": "1.1.1", 1126 | "source": { 1127 | "type": "git", 1128 | "url": "https://github.com/php-fig/link.git", 1129 | "reference": "846c25f58a1f02b93a00f2404e3626b6bf9b7807" 1130 | }, 1131 | "dist": { 1132 | "type": "zip", 1133 | "url": "https://api.github.com/repos/php-fig/link/zipball/846c25f58a1f02b93a00f2404e3626b6bf9b7807", 1134 | "reference": "846c25f58a1f02b93a00f2404e3626b6bf9b7807", 1135 | "shasum": "" 1136 | }, 1137 | "require": { 1138 | "php": ">=8.0.0" 1139 | }, 1140 | "type": "library", 1141 | "extra": { 1142 | "branch-alias": { 1143 | "dev-master": "1.0.x-dev" 1144 | } 1145 | }, 1146 | "autoload": { 1147 | "psr-4": { 1148 | "Psr\\Link\\": "src/" 1149 | } 1150 | }, 1151 | "notification-url": "https://packagist.org/downloads/", 1152 | "license": [ 1153 | "MIT" 1154 | ], 1155 | "authors": [ 1156 | { 1157 | "name": "PHP-FIG", 1158 | "homepage": "http://www.php-fig.org/" 1159 | } 1160 | ], 1161 | "description": "Common interfaces for HTTP links", 1162 | "homepage": "https://github.com/php-fig/link", 1163 | "keywords": [ 1164 | "http", 1165 | "http-link", 1166 | "link", 1167 | "psr", 1168 | "psr-13", 1169 | "rest" 1170 | ], 1171 | "support": { 1172 | "source": "https://github.com/php-fig/link/tree/1.1.1" 1173 | }, 1174 | "time": "2021-03-11T22:59:13+00:00" 1175 | }, 1176 | { 1177 | "name": "sebastian/cli-parser", 1178 | "version": "1.0.2", 1179 | "source": { 1180 | "type": "git", 1181 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 1182 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" 1183 | }, 1184 | "dist": { 1185 | "type": "zip", 1186 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", 1187 | "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", 1188 | "shasum": "" 1189 | }, 1190 | "require": { 1191 | "php": ">=7.3" 1192 | }, 1193 | "require-dev": { 1194 | "phpunit/phpunit": "^9.3" 1195 | }, 1196 | "type": "library", 1197 | "extra": { 1198 | "branch-alias": { 1199 | "dev-master": "1.0-dev" 1200 | } 1201 | }, 1202 | "autoload": { 1203 | "classmap": [ 1204 | "src/" 1205 | ] 1206 | }, 1207 | "notification-url": "https://packagist.org/downloads/", 1208 | "license": [ 1209 | "BSD-3-Clause" 1210 | ], 1211 | "authors": [ 1212 | { 1213 | "name": "Sebastian Bergmann", 1214 | "email": "sebastian@phpunit.de", 1215 | "role": "lead" 1216 | } 1217 | ], 1218 | "description": "Library for parsing CLI options", 1219 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 1220 | "support": { 1221 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 1222 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" 1223 | }, 1224 | "funding": [ 1225 | { 1226 | "url": "https://github.com/sebastianbergmann", 1227 | "type": "github" 1228 | } 1229 | ], 1230 | "time": "2024-03-02T06:27:43+00:00" 1231 | }, 1232 | { 1233 | "name": "sebastian/code-unit", 1234 | "version": "1.0.8", 1235 | "source": { 1236 | "type": "git", 1237 | "url": "https://github.com/sebastianbergmann/code-unit.git", 1238 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" 1239 | }, 1240 | "dist": { 1241 | "type": "zip", 1242 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", 1243 | "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", 1244 | "shasum": "" 1245 | }, 1246 | "require": { 1247 | "php": ">=7.3" 1248 | }, 1249 | "require-dev": { 1250 | "phpunit/phpunit": "^9.3" 1251 | }, 1252 | "type": "library", 1253 | "extra": { 1254 | "branch-alias": { 1255 | "dev-master": "1.0-dev" 1256 | } 1257 | }, 1258 | "autoload": { 1259 | "classmap": [ 1260 | "src/" 1261 | ] 1262 | }, 1263 | "notification-url": "https://packagist.org/downloads/", 1264 | "license": [ 1265 | "BSD-3-Clause" 1266 | ], 1267 | "authors": [ 1268 | { 1269 | "name": "Sebastian Bergmann", 1270 | "email": "sebastian@phpunit.de", 1271 | "role": "lead" 1272 | } 1273 | ], 1274 | "description": "Collection of value objects that represent the PHP code units", 1275 | "homepage": "https://github.com/sebastianbergmann/code-unit", 1276 | "support": { 1277 | "issues": "https://github.com/sebastianbergmann/code-unit/issues", 1278 | "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" 1279 | }, 1280 | "funding": [ 1281 | { 1282 | "url": "https://github.com/sebastianbergmann", 1283 | "type": "github" 1284 | } 1285 | ], 1286 | "time": "2020-10-26T13:08:54+00:00" 1287 | }, 1288 | { 1289 | "name": "sebastian/code-unit-reverse-lookup", 1290 | "version": "2.0.3", 1291 | "source": { 1292 | "type": "git", 1293 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1294 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" 1295 | }, 1296 | "dist": { 1297 | "type": "zip", 1298 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 1299 | "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", 1300 | "shasum": "" 1301 | }, 1302 | "require": { 1303 | "php": ">=7.3" 1304 | }, 1305 | "require-dev": { 1306 | "phpunit/phpunit": "^9.3" 1307 | }, 1308 | "type": "library", 1309 | "extra": { 1310 | "branch-alias": { 1311 | "dev-master": "2.0-dev" 1312 | } 1313 | }, 1314 | "autoload": { 1315 | "classmap": [ 1316 | "src/" 1317 | ] 1318 | }, 1319 | "notification-url": "https://packagist.org/downloads/", 1320 | "license": [ 1321 | "BSD-3-Clause" 1322 | ], 1323 | "authors": [ 1324 | { 1325 | "name": "Sebastian Bergmann", 1326 | "email": "sebastian@phpunit.de" 1327 | } 1328 | ], 1329 | "description": "Looks up which function or method a line of code belongs to", 1330 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1331 | "support": { 1332 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 1333 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" 1334 | }, 1335 | "funding": [ 1336 | { 1337 | "url": "https://github.com/sebastianbergmann", 1338 | "type": "github" 1339 | } 1340 | ], 1341 | "time": "2020-09-28T05:30:19+00:00" 1342 | }, 1343 | { 1344 | "name": "sebastian/comparator", 1345 | "version": "4.0.8", 1346 | "source": { 1347 | "type": "git", 1348 | "url": "https://github.com/sebastianbergmann/comparator.git", 1349 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a" 1350 | }, 1351 | "dist": { 1352 | "type": "zip", 1353 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", 1354 | "reference": "fa0f136dd2334583309d32b62544682ee972b51a", 1355 | "shasum": "" 1356 | }, 1357 | "require": { 1358 | "php": ">=7.3", 1359 | "sebastian/diff": "^4.0", 1360 | "sebastian/exporter": "^4.0" 1361 | }, 1362 | "require-dev": { 1363 | "phpunit/phpunit": "^9.3" 1364 | }, 1365 | "type": "library", 1366 | "extra": { 1367 | "branch-alias": { 1368 | "dev-master": "4.0-dev" 1369 | } 1370 | }, 1371 | "autoload": { 1372 | "classmap": [ 1373 | "src/" 1374 | ] 1375 | }, 1376 | "notification-url": "https://packagist.org/downloads/", 1377 | "license": [ 1378 | "BSD-3-Clause" 1379 | ], 1380 | "authors": [ 1381 | { 1382 | "name": "Sebastian Bergmann", 1383 | "email": "sebastian@phpunit.de" 1384 | }, 1385 | { 1386 | "name": "Jeff Welch", 1387 | "email": "whatthejeff@gmail.com" 1388 | }, 1389 | { 1390 | "name": "Volker Dusch", 1391 | "email": "github@wallbash.com" 1392 | }, 1393 | { 1394 | "name": "Bernhard Schussek", 1395 | "email": "bschussek@2bepublished.at" 1396 | } 1397 | ], 1398 | "description": "Provides the functionality to compare PHP values for equality", 1399 | "homepage": "https://github.com/sebastianbergmann/comparator", 1400 | "keywords": [ 1401 | "comparator", 1402 | "compare", 1403 | "equality" 1404 | ], 1405 | "support": { 1406 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 1407 | "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" 1408 | }, 1409 | "funding": [ 1410 | { 1411 | "url": "https://github.com/sebastianbergmann", 1412 | "type": "github" 1413 | } 1414 | ], 1415 | "time": "2022-09-14T12:41:17+00:00" 1416 | }, 1417 | { 1418 | "name": "sebastian/complexity", 1419 | "version": "2.0.3", 1420 | "source": { 1421 | "type": "git", 1422 | "url": "https://github.com/sebastianbergmann/complexity.git", 1423 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" 1424 | }, 1425 | "dist": { 1426 | "type": "zip", 1427 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", 1428 | "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", 1429 | "shasum": "" 1430 | }, 1431 | "require": { 1432 | "nikic/php-parser": "^4.18 || ^5.0", 1433 | "php": ">=7.3" 1434 | }, 1435 | "require-dev": { 1436 | "phpunit/phpunit": "^9.3" 1437 | }, 1438 | "type": "library", 1439 | "extra": { 1440 | "branch-alias": { 1441 | "dev-master": "2.0-dev" 1442 | } 1443 | }, 1444 | "autoload": { 1445 | "classmap": [ 1446 | "src/" 1447 | ] 1448 | }, 1449 | "notification-url": "https://packagist.org/downloads/", 1450 | "license": [ 1451 | "BSD-3-Clause" 1452 | ], 1453 | "authors": [ 1454 | { 1455 | "name": "Sebastian Bergmann", 1456 | "email": "sebastian@phpunit.de", 1457 | "role": "lead" 1458 | } 1459 | ], 1460 | "description": "Library for calculating the complexity of PHP code units", 1461 | "homepage": "https://github.com/sebastianbergmann/complexity", 1462 | "support": { 1463 | "issues": "https://github.com/sebastianbergmann/complexity/issues", 1464 | "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" 1465 | }, 1466 | "funding": [ 1467 | { 1468 | "url": "https://github.com/sebastianbergmann", 1469 | "type": "github" 1470 | } 1471 | ], 1472 | "time": "2023-12-22T06:19:30+00:00" 1473 | }, 1474 | { 1475 | "name": "sebastian/diff", 1476 | "version": "4.0.6", 1477 | "source": { 1478 | "type": "git", 1479 | "url": "https://github.com/sebastianbergmann/diff.git", 1480 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" 1481 | }, 1482 | "dist": { 1483 | "type": "zip", 1484 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", 1485 | "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", 1486 | "shasum": "" 1487 | }, 1488 | "require": { 1489 | "php": ">=7.3" 1490 | }, 1491 | "require-dev": { 1492 | "phpunit/phpunit": "^9.3", 1493 | "symfony/process": "^4.2 || ^5" 1494 | }, 1495 | "type": "library", 1496 | "extra": { 1497 | "branch-alias": { 1498 | "dev-master": "4.0-dev" 1499 | } 1500 | }, 1501 | "autoload": { 1502 | "classmap": [ 1503 | "src/" 1504 | ] 1505 | }, 1506 | "notification-url": "https://packagist.org/downloads/", 1507 | "license": [ 1508 | "BSD-3-Clause" 1509 | ], 1510 | "authors": [ 1511 | { 1512 | "name": "Sebastian Bergmann", 1513 | "email": "sebastian@phpunit.de" 1514 | }, 1515 | { 1516 | "name": "Kore Nordmann", 1517 | "email": "mail@kore-nordmann.de" 1518 | } 1519 | ], 1520 | "description": "Diff implementation", 1521 | "homepage": "https://github.com/sebastianbergmann/diff", 1522 | "keywords": [ 1523 | "diff", 1524 | "udiff", 1525 | "unidiff", 1526 | "unified diff" 1527 | ], 1528 | "support": { 1529 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1530 | "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" 1531 | }, 1532 | "funding": [ 1533 | { 1534 | "url": "https://github.com/sebastianbergmann", 1535 | "type": "github" 1536 | } 1537 | ], 1538 | "time": "2024-03-02T06:30:58+00:00" 1539 | }, 1540 | { 1541 | "name": "sebastian/environment", 1542 | "version": "5.1.5", 1543 | "source": { 1544 | "type": "git", 1545 | "url": "https://github.com/sebastianbergmann/environment.git", 1546 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" 1547 | }, 1548 | "dist": { 1549 | "type": "zip", 1550 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", 1551 | "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", 1552 | "shasum": "" 1553 | }, 1554 | "require": { 1555 | "php": ">=7.3" 1556 | }, 1557 | "require-dev": { 1558 | "phpunit/phpunit": "^9.3" 1559 | }, 1560 | "suggest": { 1561 | "ext-posix": "*" 1562 | }, 1563 | "type": "library", 1564 | "extra": { 1565 | "branch-alias": { 1566 | "dev-master": "5.1-dev" 1567 | } 1568 | }, 1569 | "autoload": { 1570 | "classmap": [ 1571 | "src/" 1572 | ] 1573 | }, 1574 | "notification-url": "https://packagist.org/downloads/", 1575 | "license": [ 1576 | "BSD-3-Clause" 1577 | ], 1578 | "authors": [ 1579 | { 1580 | "name": "Sebastian Bergmann", 1581 | "email": "sebastian@phpunit.de" 1582 | } 1583 | ], 1584 | "description": "Provides functionality to handle HHVM/PHP environments", 1585 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1586 | "keywords": [ 1587 | "Xdebug", 1588 | "environment", 1589 | "hhvm" 1590 | ], 1591 | "support": { 1592 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1593 | "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" 1594 | }, 1595 | "funding": [ 1596 | { 1597 | "url": "https://github.com/sebastianbergmann", 1598 | "type": "github" 1599 | } 1600 | ], 1601 | "time": "2023-02-03T06:03:51+00:00" 1602 | }, 1603 | { 1604 | "name": "sebastian/exporter", 1605 | "version": "4.0.6", 1606 | "source": { 1607 | "type": "git", 1608 | "url": "https://github.com/sebastianbergmann/exporter.git", 1609 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" 1610 | }, 1611 | "dist": { 1612 | "type": "zip", 1613 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", 1614 | "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", 1615 | "shasum": "" 1616 | }, 1617 | "require": { 1618 | "php": ">=7.3", 1619 | "sebastian/recursion-context": "^4.0" 1620 | }, 1621 | "require-dev": { 1622 | "ext-mbstring": "*", 1623 | "phpunit/phpunit": "^9.3" 1624 | }, 1625 | "type": "library", 1626 | "extra": { 1627 | "branch-alias": { 1628 | "dev-master": "4.0-dev" 1629 | } 1630 | }, 1631 | "autoload": { 1632 | "classmap": [ 1633 | "src/" 1634 | ] 1635 | }, 1636 | "notification-url": "https://packagist.org/downloads/", 1637 | "license": [ 1638 | "BSD-3-Clause" 1639 | ], 1640 | "authors": [ 1641 | { 1642 | "name": "Sebastian Bergmann", 1643 | "email": "sebastian@phpunit.de" 1644 | }, 1645 | { 1646 | "name": "Jeff Welch", 1647 | "email": "whatthejeff@gmail.com" 1648 | }, 1649 | { 1650 | "name": "Volker Dusch", 1651 | "email": "github@wallbash.com" 1652 | }, 1653 | { 1654 | "name": "Adam Harvey", 1655 | "email": "aharvey@php.net" 1656 | }, 1657 | { 1658 | "name": "Bernhard Schussek", 1659 | "email": "bschussek@gmail.com" 1660 | } 1661 | ], 1662 | "description": "Provides the functionality to export PHP variables for visualization", 1663 | "homepage": "https://www.github.com/sebastianbergmann/exporter", 1664 | "keywords": [ 1665 | "export", 1666 | "exporter" 1667 | ], 1668 | "support": { 1669 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1670 | "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" 1671 | }, 1672 | "funding": [ 1673 | { 1674 | "url": "https://github.com/sebastianbergmann", 1675 | "type": "github" 1676 | } 1677 | ], 1678 | "time": "2024-03-02T06:33:00+00:00" 1679 | }, 1680 | { 1681 | "name": "sebastian/global-state", 1682 | "version": "5.0.7", 1683 | "source": { 1684 | "type": "git", 1685 | "url": "https://github.com/sebastianbergmann/global-state.git", 1686 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" 1687 | }, 1688 | "dist": { 1689 | "type": "zip", 1690 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", 1691 | "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", 1692 | "shasum": "" 1693 | }, 1694 | "require": { 1695 | "php": ">=7.3", 1696 | "sebastian/object-reflector": "^2.0", 1697 | "sebastian/recursion-context": "^4.0" 1698 | }, 1699 | "require-dev": { 1700 | "ext-dom": "*", 1701 | "phpunit/phpunit": "^9.3" 1702 | }, 1703 | "suggest": { 1704 | "ext-uopz": "*" 1705 | }, 1706 | "type": "library", 1707 | "extra": { 1708 | "branch-alias": { 1709 | "dev-master": "5.0-dev" 1710 | } 1711 | }, 1712 | "autoload": { 1713 | "classmap": [ 1714 | "src/" 1715 | ] 1716 | }, 1717 | "notification-url": "https://packagist.org/downloads/", 1718 | "license": [ 1719 | "BSD-3-Clause" 1720 | ], 1721 | "authors": [ 1722 | { 1723 | "name": "Sebastian Bergmann", 1724 | "email": "sebastian@phpunit.de" 1725 | } 1726 | ], 1727 | "description": "Snapshotting of global state", 1728 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1729 | "keywords": [ 1730 | "global state" 1731 | ], 1732 | "support": { 1733 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1734 | "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" 1735 | }, 1736 | "funding": [ 1737 | { 1738 | "url": "https://github.com/sebastianbergmann", 1739 | "type": "github" 1740 | } 1741 | ], 1742 | "time": "2024-03-02T06:35:11+00:00" 1743 | }, 1744 | { 1745 | "name": "sebastian/lines-of-code", 1746 | "version": "1.0.4", 1747 | "source": { 1748 | "type": "git", 1749 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1750 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" 1751 | }, 1752 | "dist": { 1753 | "type": "zip", 1754 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", 1755 | "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", 1756 | "shasum": "" 1757 | }, 1758 | "require": { 1759 | "nikic/php-parser": "^4.18 || ^5.0", 1760 | "php": ">=7.3" 1761 | }, 1762 | "require-dev": { 1763 | "phpunit/phpunit": "^9.3" 1764 | }, 1765 | "type": "library", 1766 | "extra": { 1767 | "branch-alias": { 1768 | "dev-master": "1.0-dev" 1769 | } 1770 | }, 1771 | "autoload": { 1772 | "classmap": [ 1773 | "src/" 1774 | ] 1775 | }, 1776 | "notification-url": "https://packagist.org/downloads/", 1777 | "license": [ 1778 | "BSD-3-Clause" 1779 | ], 1780 | "authors": [ 1781 | { 1782 | "name": "Sebastian Bergmann", 1783 | "email": "sebastian@phpunit.de", 1784 | "role": "lead" 1785 | } 1786 | ], 1787 | "description": "Library for counting the lines of code in PHP source code", 1788 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1789 | "support": { 1790 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 1791 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" 1792 | }, 1793 | "funding": [ 1794 | { 1795 | "url": "https://github.com/sebastianbergmann", 1796 | "type": "github" 1797 | } 1798 | ], 1799 | "time": "2023-12-22T06:20:34+00:00" 1800 | }, 1801 | { 1802 | "name": "sebastian/object-enumerator", 1803 | "version": "4.0.4", 1804 | "source": { 1805 | "type": "git", 1806 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1807 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" 1808 | }, 1809 | "dist": { 1810 | "type": "zip", 1811 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", 1812 | "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", 1813 | "shasum": "" 1814 | }, 1815 | "require": { 1816 | "php": ">=7.3", 1817 | "sebastian/object-reflector": "^2.0", 1818 | "sebastian/recursion-context": "^4.0" 1819 | }, 1820 | "require-dev": { 1821 | "phpunit/phpunit": "^9.3" 1822 | }, 1823 | "type": "library", 1824 | "extra": { 1825 | "branch-alias": { 1826 | "dev-master": "4.0-dev" 1827 | } 1828 | }, 1829 | "autoload": { 1830 | "classmap": [ 1831 | "src/" 1832 | ] 1833 | }, 1834 | "notification-url": "https://packagist.org/downloads/", 1835 | "license": [ 1836 | "BSD-3-Clause" 1837 | ], 1838 | "authors": [ 1839 | { 1840 | "name": "Sebastian Bergmann", 1841 | "email": "sebastian@phpunit.de" 1842 | } 1843 | ], 1844 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1845 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1846 | "support": { 1847 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1848 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" 1849 | }, 1850 | "funding": [ 1851 | { 1852 | "url": "https://github.com/sebastianbergmann", 1853 | "type": "github" 1854 | } 1855 | ], 1856 | "time": "2020-10-26T13:12:34+00:00" 1857 | }, 1858 | { 1859 | "name": "sebastian/object-reflector", 1860 | "version": "2.0.4", 1861 | "source": { 1862 | "type": "git", 1863 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1864 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" 1865 | }, 1866 | "dist": { 1867 | "type": "zip", 1868 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1869 | "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", 1870 | "shasum": "" 1871 | }, 1872 | "require": { 1873 | "php": ">=7.3" 1874 | }, 1875 | "require-dev": { 1876 | "phpunit/phpunit": "^9.3" 1877 | }, 1878 | "type": "library", 1879 | "extra": { 1880 | "branch-alias": { 1881 | "dev-master": "2.0-dev" 1882 | } 1883 | }, 1884 | "autoload": { 1885 | "classmap": [ 1886 | "src/" 1887 | ] 1888 | }, 1889 | "notification-url": "https://packagist.org/downloads/", 1890 | "license": [ 1891 | "BSD-3-Clause" 1892 | ], 1893 | "authors": [ 1894 | { 1895 | "name": "Sebastian Bergmann", 1896 | "email": "sebastian@phpunit.de" 1897 | } 1898 | ], 1899 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1900 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1901 | "support": { 1902 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1903 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" 1904 | }, 1905 | "funding": [ 1906 | { 1907 | "url": "https://github.com/sebastianbergmann", 1908 | "type": "github" 1909 | } 1910 | ], 1911 | "time": "2020-10-26T13:14:26+00:00" 1912 | }, 1913 | { 1914 | "name": "sebastian/recursion-context", 1915 | "version": "4.0.5", 1916 | "source": { 1917 | "type": "git", 1918 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1919 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" 1920 | }, 1921 | "dist": { 1922 | "type": "zip", 1923 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", 1924 | "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", 1925 | "shasum": "" 1926 | }, 1927 | "require": { 1928 | "php": ">=7.3" 1929 | }, 1930 | "require-dev": { 1931 | "phpunit/phpunit": "^9.3" 1932 | }, 1933 | "type": "library", 1934 | "extra": { 1935 | "branch-alias": { 1936 | "dev-master": "4.0-dev" 1937 | } 1938 | }, 1939 | "autoload": { 1940 | "classmap": [ 1941 | "src/" 1942 | ] 1943 | }, 1944 | "notification-url": "https://packagist.org/downloads/", 1945 | "license": [ 1946 | "BSD-3-Clause" 1947 | ], 1948 | "authors": [ 1949 | { 1950 | "name": "Sebastian Bergmann", 1951 | "email": "sebastian@phpunit.de" 1952 | }, 1953 | { 1954 | "name": "Jeff Welch", 1955 | "email": "whatthejeff@gmail.com" 1956 | }, 1957 | { 1958 | "name": "Adam Harvey", 1959 | "email": "aharvey@php.net" 1960 | } 1961 | ], 1962 | "description": "Provides functionality to recursively process PHP variables", 1963 | "homepage": "https://github.com/sebastianbergmann/recursion-context", 1964 | "support": { 1965 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1966 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" 1967 | }, 1968 | "funding": [ 1969 | { 1970 | "url": "https://github.com/sebastianbergmann", 1971 | "type": "github" 1972 | } 1973 | ], 1974 | "time": "2023-02-03T06:07:39+00:00" 1975 | }, 1976 | { 1977 | "name": "sebastian/resource-operations", 1978 | "version": "3.0.4", 1979 | "source": { 1980 | "type": "git", 1981 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1982 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" 1983 | }, 1984 | "dist": { 1985 | "type": "zip", 1986 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", 1987 | "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", 1988 | "shasum": "" 1989 | }, 1990 | "require": { 1991 | "php": ">=7.3" 1992 | }, 1993 | "require-dev": { 1994 | "phpunit/phpunit": "^9.0" 1995 | }, 1996 | "type": "library", 1997 | "extra": { 1998 | "branch-alias": { 1999 | "dev-main": "3.0-dev" 2000 | } 2001 | }, 2002 | "autoload": { 2003 | "classmap": [ 2004 | "src/" 2005 | ] 2006 | }, 2007 | "notification-url": "https://packagist.org/downloads/", 2008 | "license": [ 2009 | "BSD-3-Clause" 2010 | ], 2011 | "authors": [ 2012 | { 2013 | "name": "Sebastian Bergmann", 2014 | "email": "sebastian@phpunit.de" 2015 | } 2016 | ], 2017 | "description": "Provides a list of PHP built-in functions that operate on resources", 2018 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 2019 | "support": { 2020 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" 2021 | }, 2022 | "funding": [ 2023 | { 2024 | "url": "https://github.com/sebastianbergmann", 2025 | "type": "github" 2026 | } 2027 | ], 2028 | "time": "2024-03-14T16:00:52+00:00" 2029 | }, 2030 | { 2031 | "name": "sebastian/type", 2032 | "version": "3.2.1", 2033 | "source": { 2034 | "type": "git", 2035 | "url": "https://github.com/sebastianbergmann/type.git", 2036 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" 2037 | }, 2038 | "dist": { 2039 | "type": "zip", 2040 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", 2041 | "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", 2042 | "shasum": "" 2043 | }, 2044 | "require": { 2045 | "php": ">=7.3" 2046 | }, 2047 | "require-dev": { 2048 | "phpunit/phpunit": "^9.5" 2049 | }, 2050 | "type": "library", 2051 | "extra": { 2052 | "branch-alias": { 2053 | "dev-master": "3.2-dev" 2054 | } 2055 | }, 2056 | "autoload": { 2057 | "classmap": [ 2058 | "src/" 2059 | ] 2060 | }, 2061 | "notification-url": "https://packagist.org/downloads/", 2062 | "license": [ 2063 | "BSD-3-Clause" 2064 | ], 2065 | "authors": [ 2066 | { 2067 | "name": "Sebastian Bergmann", 2068 | "email": "sebastian@phpunit.de", 2069 | "role": "lead" 2070 | } 2071 | ], 2072 | "description": "Collection of value objects that represent the types of the PHP type system", 2073 | "homepage": "https://github.com/sebastianbergmann/type", 2074 | "support": { 2075 | "issues": "https://github.com/sebastianbergmann/type/issues", 2076 | "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" 2077 | }, 2078 | "funding": [ 2079 | { 2080 | "url": "https://github.com/sebastianbergmann", 2081 | "type": "github" 2082 | } 2083 | ], 2084 | "time": "2023-02-03T06:13:03+00:00" 2085 | }, 2086 | { 2087 | "name": "sebastian/version", 2088 | "version": "3.0.2", 2089 | "source": { 2090 | "type": "git", 2091 | "url": "https://github.com/sebastianbergmann/version.git", 2092 | "reference": "c6c1022351a901512170118436c764e473f6de8c" 2093 | }, 2094 | "dist": { 2095 | "type": "zip", 2096 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", 2097 | "reference": "c6c1022351a901512170118436c764e473f6de8c", 2098 | "shasum": "" 2099 | }, 2100 | "require": { 2101 | "php": ">=7.3" 2102 | }, 2103 | "type": "library", 2104 | "extra": { 2105 | "branch-alias": { 2106 | "dev-master": "3.0-dev" 2107 | } 2108 | }, 2109 | "autoload": { 2110 | "classmap": [ 2111 | "src/" 2112 | ] 2113 | }, 2114 | "notification-url": "https://packagist.org/downloads/", 2115 | "license": [ 2116 | "BSD-3-Clause" 2117 | ], 2118 | "authors": [ 2119 | { 2120 | "name": "Sebastian Bergmann", 2121 | "email": "sebastian@phpunit.de", 2122 | "role": "lead" 2123 | } 2124 | ], 2125 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 2126 | "homepage": "https://github.com/sebastianbergmann/version", 2127 | "support": { 2128 | "issues": "https://github.com/sebastianbergmann/version/issues", 2129 | "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" 2130 | }, 2131 | "funding": [ 2132 | { 2133 | "url": "https://github.com/sebastianbergmann", 2134 | "type": "github" 2135 | } 2136 | ], 2137 | "time": "2020-09-28T06:39:44+00:00" 2138 | }, 2139 | { 2140 | "name": "squizlabs/php_codesniffer", 2141 | "version": "3.9.0", 2142 | "source": { 2143 | "type": "git", 2144 | "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", 2145 | "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" 2146 | }, 2147 | "dist": { 2148 | "type": "zip", 2149 | "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", 2150 | "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", 2151 | "shasum": "" 2152 | }, 2153 | "require": { 2154 | "ext-simplexml": "*", 2155 | "ext-tokenizer": "*", 2156 | "ext-xmlwriter": "*", 2157 | "php": ">=5.4.0" 2158 | }, 2159 | "require-dev": { 2160 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" 2161 | }, 2162 | "bin": [ 2163 | "bin/phpcbf", 2164 | "bin/phpcs" 2165 | ], 2166 | "type": "library", 2167 | "extra": { 2168 | "branch-alias": { 2169 | "dev-master": "3.x-dev" 2170 | } 2171 | }, 2172 | "notification-url": "https://packagist.org/downloads/", 2173 | "license": [ 2174 | "BSD-3-Clause" 2175 | ], 2176 | "authors": [ 2177 | { 2178 | "name": "Greg Sherwood", 2179 | "role": "Former lead" 2180 | }, 2181 | { 2182 | "name": "Juliette Reinders Folmer", 2183 | "role": "Current lead" 2184 | }, 2185 | { 2186 | "name": "Contributors", 2187 | "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" 2188 | } 2189 | ], 2190 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 2191 | "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", 2192 | "keywords": [ 2193 | "phpcs", 2194 | "standards", 2195 | "static analysis" 2196 | ], 2197 | "support": { 2198 | "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", 2199 | "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", 2200 | "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", 2201 | "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" 2202 | }, 2203 | "funding": [ 2204 | { 2205 | "url": "https://github.com/PHPCSStandards", 2206 | "type": "github" 2207 | }, 2208 | { 2209 | "url": "https://github.com/jrfnl", 2210 | "type": "github" 2211 | }, 2212 | { 2213 | "url": "https://opencollective.com/php_codesniffer", 2214 | "type": "open_collective" 2215 | } 2216 | ], 2217 | "time": "2024-02-16T15:06:51+00:00" 2218 | }, 2219 | { 2220 | "name": "theseer/tokenizer", 2221 | "version": "1.2.3", 2222 | "source": { 2223 | "type": "git", 2224 | "url": "https://github.com/theseer/tokenizer.git", 2225 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" 2226 | }, 2227 | "dist": { 2228 | "type": "zip", 2229 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 2230 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 2231 | "shasum": "" 2232 | }, 2233 | "require": { 2234 | "ext-dom": "*", 2235 | "ext-tokenizer": "*", 2236 | "ext-xmlwriter": "*", 2237 | "php": "^7.2 || ^8.0" 2238 | }, 2239 | "type": "library", 2240 | "autoload": { 2241 | "classmap": [ 2242 | "src/" 2243 | ] 2244 | }, 2245 | "notification-url": "https://packagist.org/downloads/", 2246 | "license": [ 2247 | "BSD-3-Clause" 2248 | ], 2249 | "authors": [ 2250 | { 2251 | "name": "Arne Blankerts", 2252 | "email": "arne@blankerts.de", 2253 | "role": "Developer" 2254 | } 2255 | ], 2256 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 2257 | "support": { 2258 | "issues": "https://github.com/theseer/tokenizer/issues", 2259 | "source": "https://github.com/theseer/tokenizer/tree/1.2.3" 2260 | }, 2261 | "funding": [ 2262 | { 2263 | "url": "https://github.com/theseer", 2264 | "type": "github" 2265 | } 2266 | ], 2267 | "time": "2024-03-03T12:36:25+00:00" 2268 | } 2269 | ], 2270 | "aliases": [], 2271 | "minimum-stability": "stable", 2272 | "stability-flags": [], 2273 | "prefer-stable": false, 2274 | "prefer-lowest": false, 2275 | "platform": { 2276 | "php": "^7.3 || ^8.0" 2277 | }, 2278 | "platform-dev": [], 2279 | "plugin-api-version": "2.6.0" 2280 | } 2281 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | src 13 | tests 14 | 15 | 16 | 17 | tests/NonRewindableStream.php 18 | 19 | 20 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | 24 | ./tests/ 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ApiProblemRenderer.php: -------------------------------------------------------------------------------- 1 | pretty = (bool)$pretty; 27 | } 28 | 29 | public function render(RequestInterface $request, ResponseInterface $response, $problem) 30 | { 31 | // Look for API Problem specific media types first. If none, then find preferred format 32 | $mediaType = $this->determineMediaType($request->getHeaderLine('Accept')); 33 | if ($mediaType) { 34 | list ($_, $format) = explode('+', $mediaType); 35 | } else { 36 | $format = $this->determinePeferredFormat($request->getHeaderLine('Accept'), ['json', 'xml'], 'json'); 37 | } 38 | 39 | 40 | // set the ProblemAPi content type for JSON or XML 41 | $output = $this->renderOutput($format, $problem); 42 | $contentType = 'application/problem+' . $format; 43 | 44 | $response = $this->writeBody($response, $output); 45 | $response = $response->withHeader('Content-type', $contentType); 46 | 47 | if ($problem->getStatus() >= 100) { 48 | $response = $response->withStatus($problem->getStatus()); 49 | } 50 | 51 | return $response; 52 | } 53 | 54 | protected function renderOutput($format, $problem) 55 | { 56 | if (!$problem instanceof ApiProblem) { 57 | throw new RuntimeException('Data is not an ApiProblem object'); 58 | } 59 | 60 | if ($format == 'xml') { 61 | return $problem->asXml($this->pretty); 62 | } 63 | 64 | return $problem->asJson($this->pretty); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/HalRenderer.php: -------------------------------------------------------------------------------- 1 | pretty = (bool)$pretty; 27 | } 28 | 29 | public function render(RequestInterface $request, ResponseInterface $response, $data) 30 | { 31 | // Look for HAL specific media types first. If none, then find preferred format 32 | $mediaType = $this->determineMediaType($request->getHeaderLine('Accept')); 33 | if ($mediaType) { 34 | list ($_, $format) = explode('+', $mediaType); 35 | } else { 36 | $format = $this->determinePeferredFormat( 37 | $request->getHeaderLine('Accept'), 38 | ['json', 'xml', 'html'], 39 | 'json' 40 | ); 41 | } 42 | 43 | $output = $this->renderOutput($format, $data); 44 | if ($format == 'html') { 45 | $contentType = 'text/html'; 46 | } else { 47 | $contentType = 'application/hal+' . $format; 48 | } 49 | 50 | $response = $this->writeBody($response, $output); 51 | $response = $response->withHeader('Content-type', $contentType); 52 | 53 | return $response; 54 | } 55 | 56 | protected function renderOutput($format, $data) 57 | { 58 | if (!$data instanceof Hal) { 59 | throw new RuntimeException('Data is not a Hal object'); 60 | } 61 | 62 | switch ($format) { 63 | case 'html': 64 | $data = json_decode($data->asJson(), true); 65 | $output = $this->renderHtml($data); 66 | break; 67 | 68 | case 'xml': 69 | $output = $data->asXml(); 70 | break; 71 | 72 | case 'json': 73 | $output = $data->asJson($this->pretty); 74 | break; 75 | 76 | default: 77 | throw new RuntimeException("Unknown content type $contentType"); 78 | } 79 | 80 | return $output; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Renderer.php: -------------------------------------------------------------------------------- 1 | ['array', 'JsonSerializable'], 23 | 'json' => ['scalar','array', 'JsonSerializable'], 24 | 'html' => ['scalar','array', 'JsonSerializable'] 25 | ]; 26 | protected $xmlRootElementName = 'root'; 27 | protected $htmlPrefix; 28 | protected $htmlPostfix; 29 | protected $jsonOptions = 0; 30 | 31 | public function __construct($pretty = true) 32 | { 33 | $this->pretty = (bool)$pretty; 34 | } 35 | 36 | public function render(RequestInterface $request, ResponseInterface $response, $data) 37 | { 38 | $mediaType = $this->determineMediaType($request->getHeaderLine('Accept')); 39 | 40 | $mediaSubType = explode('/', $mediaType)[1]; 41 | $dataIsValidForMediatype = $this->isDataValidForMediaType($mediaSubType, $data); 42 | if (!$dataIsValidForMediatype) { 43 | throw new RuntimeException('Data for mediaType ' . $mediaType . ' must be ' 44 | . implode(' or ', $this->mediaSubtypesToAllowedDataTypesMap[$mediaSubType])); 45 | } 46 | 47 | $output = $this->renderOutput($mediaType, $data); 48 | $response = $this->writeBody($response, $output); 49 | $response = $response->withHeader('Content-type', $mediaType); 50 | 51 | return $response; 52 | } 53 | 54 | protected function isDataValidForMediaType($mediaSubType, $data) 55 | { 56 | $allowedDataTypes = $this->mediaSubtypesToAllowedDataTypesMap[$mediaSubType]; 57 | 58 | foreach ($allowedDataTypes as $allowedDataType) { 59 | switch ($allowedDataType) { 60 | case 'scalar': 61 | if (is_scalar($data)) { 62 | return true; 63 | } 64 | break; 65 | case 'array': 66 | if (is_array($data)) { 67 | return true; 68 | } 69 | break; 70 | case 'JsonSerializable': 71 | if ($data instanceof \JsonSerializable) { 72 | return true; 73 | } 74 | break; 75 | } 76 | } 77 | 78 | return false; 79 | } 80 | 81 | protected function renderOutput($mediaType, $data) 82 | { 83 | switch ($mediaType) { 84 | case 'text/html': 85 | $data = json_decode(json_encode($data), true); 86 | $output = $this->renderHtml($data); 87 | break; 88 | 89 | case 'application/xml': 90 | case 'text/xml': 91 | $data = json_decode(json_encode($data), true); 92 | $output = $this->renderXml($data); 93 | break; 94 | 95 | case 'application/json': 96 | $options = $this->getJsonOptions(); 97 | $output = json_encode($data, $options); 98 | break; 99 | 100 | default: 101 | throw new RuntimeException("Unknown media type $mediaType"); 102 | } 103 | 104 | return $output; 105 | } 106 | 107 | protected function writeBody($response, $output) 108 | { 109 | $body = $response->getBody(); 110 | if (!$body->isWritable()) { 111 | // the response's body is not writable (or doesn't exist) 112 | // so create our own 113 | $body = new SimplePsrStream(fopen('php://temp', 'r+')); 114 | } 115 | try { 116 | $body->rewind(); 117 | } catch (\RuntimeException $e) { 118 | // could not rewind the stream, therefore use our own. 119 | $body = new SimplePsrStream(fopen('php://temp', 'r+')); 120 | } 121 | $body->write($output); 122 | 123 | return $response->withBody($body); 124 | } 125 | 126 | /** 127 | * Render Array as HTML (thanks to joind.in's -api project!) 128 | * 129 | * This code is cribbed from https://github.com/joindin/joindin-api/blob/master/src/views/HtmlView.php 130 | * 131 | * @return string 132 | */ 133 | protected function renderHtml($data) 134 | { 135 | $html = $this->getHtmlPrefix(); 136 | $html .= $this->arrayToHtml($data); 137 | $html .= $this->getHtmlPostfix(); 138 | 139 | return $html; 140 | } 141 | 142 | /** 143 | * Recursively render an array to an HTML list 144 | * 145 | * @param mixed $content data to be rendered 146 | * 147 | * @return null 148 | */ 149 | protected function arrayToHtml($content) 150 | { 151 | // scalar types can be return directly 152 | if (is_scalar($content)) { 153 | return $content; 154 | } 155 | 156 | $html = "\n"; 179 | 180 | return $html; 181 | } 182 | 183 | /** 184 | * Read the accept header and determine which media type we know about 185 | * is wanted. 186 | * 187 | * @param string $acceptHeader Accept header from request 188 | * @return string 189 | */ 190 | protected function determineMediaType($acceptHeader) 191 | { 192 | if (!empty($acceptHeader)) { 193 | $negotiator = new Negotiator(); 194 | $mediaType = $negotiator->getBest($acceptHeader, $this->knownMediaTypes); 195 | 196 | if ($mediaType) { 197 | return $mediaType->getValue(); 198 | } 199 | } 200 | 201 | return $this->getDefaultMediaType(); 202 | } 203 | 204 | /** 205 | * Read the accept header and work out which format is preferred 206 | * 207 | * @param string $acceptHeader Accept header from request 208 | * @param array $allowedFormats Array of formats that are preferred 209 | * @param string $default Default format to return if no allowedFormats are found 210 | * @return string 211 | */ 212 | public function determinePeferredFormat($acceptHeader, $allowedFormats = ['json', 'xml', 'html'], $default = 'json') 213 | { 214 | if (empty($acceptHeader)) { 215 | return $default; 216 | } 217 | 218 | $negotiator = new Negotiator(); 219 | try { 220 | $elements = $negotiator->getOrderedElements($acceptHeader); 221 | } catch (InvalidMediaType $e) { 222 | return $default; 223 | } 224 | 225 | foreach ($elements as $element) { 226 | $subpart = $element->getSubPart(); 227 | foreach ($allowedFormats as $format) { 228 | if (stripos($subpart, $format) !== false) { 229 | return $format; 230 | } 231 | } 232 | } 233 | 234 | return $default; 235 | } 236 | 237 | /** 238 | * Getter for defaultMediaType 239 | * 240 | * @return string 241 | */ 242 | public function getDefaultMediaType() 243 | { 244 | return $this->defaultMediaType; 245 | } 246 | 247 | /** 248 | * Setter for defaultMediaType 249 | * 250 | * @param string $defaultMediaType Value to set 251 | * @return self 252 | */ 253 | public function setDefaultMediaType($defaultMediaType) 254 | { 255 | $this->defaultMediaType = $defaultMediaType; 256 | return $this; 257 | } 258 | 259 | /** 260 | * Getter for htmlPrefix 261 | * 262 | * @return mixed 263 | */ 264 | public function getHtmlPrefix() 265 | { 266 | if ($this->htmlPrefix === null) { 267 | $this->htmlPrefix = << 269 | 270 | 271 | 272 | 273 | 289 | 290 | 291 | HTML; 292 | } 293 | return $this->htmlPrefix; 294 | } 295 | 296 | /** 297 | * Setter for htmlPrefix 298 | * 299 | * @param mixed $htmlPrefix Value to set 300 | * @return self 301 | */ 302 | public function setHtmlPrefix($htmlPrefix) 303 | { 304 | $this->htmlPrefix = $htmlPrefix; 305 | return $this; 306 | } 307 | 308 | /** 309 | * Getter for htmlPostfix 310 | * 311 | * @return mixed 312 | */ 313 | public function getHtmlPostfix() 314 | { 315 | if ($this->htmlPostfix === null) { 316 | $this->htmlPostfix = << 318 | 319 | 320 | HTML; 321 | } 322 | return $this->htmlPostfix; 323 | } 324 | 325 | /** 326 | * Setter for htmlPostfix 327 | * 328 | * @param mixed $htmlPostfix Value to set 329 | * @return self 330 | */ 331 | public function setHtmlPostfix($htmlPostfix) 332 | { 333 | $this->htmlPostfix = $htmlPostfix; 334 | return $this; 335 | } 336 | 337 | /** 338 | * Getter for xmlRootElementName 339 | * 340 | * @return string 341 | */ 342 | public function getXmlRootElementName() 343 | { 344 | return $this->xmlRootElementName; 345 | } 346 | 347 | /** 348 | * Setter for xmlRootElementName 349 | * 350 | * @param string $xmlRootElementName 351 | * @return self 352 | */ 353 | public function setXmlRootElementName($xmlRootElementName) 354 | { 355 | $this->xmlRootElementName = $xmlRootElementName; 356 | return $this; 357 | } 358 | 359 | /** 360 | * Render Array as XML 361 | * 362 | * @return string 363 | */ 364 | protected function renderXml($data) 365 | { 366 | $xml = $this->arrayToXml($data); 367 | 368 | $dom = new \DOMDocument('1.0'); 369 | $dom->preserveWhiteSpace = false; 370 | $dom->formatOutput = $this->pretty; 371 | $dom->loadXML($xml->asXML()); 372 | 373 | return $dom->saveXML(); 374 | } 375 | 376 | /** 377 | * Simple Array to XML conversion 378 | * Based on http://www.codeproject.com/Questions/553031/JSONplusTOplusXMLplusconvertionpluswithplusphp 379 | * 380 | * @param array $data Data to convert 381 | * @param SimpleXMLElement $xmlElement XMLElement 382 | * @return SimpleXMLElement 383 | */ 384 | protected function arrayToXml($data, $xmlElement = null) 385 | { 386 | if ($xmlElement === null) { 387 | $rootElementName = $this->getXmlRootElementName(); 388 | $xmlElement = new \SimpleXMLElement("<$rootElementName>"); 389 | } 390 | 391 | foreach ($data as $key => $value) { 392 | if (is_array($value)) { 393 | if (!is_numeric($key)) { 394 | $subnode = $xmlElement->addChild("$key"); 395 | 396 | if (count($value) > 1 && is_array($value)) { 397 | $jump = false; 398 | $count = 1; 399 | foreach ($value as $k => $v) { 400 | if (is_array($v)) { 401 | if ($count++ > 1) { 402 | $subnode = $xmlElement->addChild("$key"); 403 | } 404 | 405 | $this->arrayToXml($v, $subnode); 406 | $jump = true; 407 | } 408 | } 409 | if ($jump) { 410 | goto LE; 411 | } 412 | $this->arrayToXml($value, $subnode); 413 | } else { 414 | $this->arrayToXml($value, $subnode); 415 | } 416 | } else { 417 | $this->arrayToXml($value, $xmlElement); 418 | } 419 | } else { 420 | if (is_bool($value)) { 421 | $value = (int)$value; 422 | } 423 | if (is_numeric($key)) { 424 | $key = "_$key"; 425 | } 426 | $xmlElement->addChild("$key", "$value"); 427 | } 428 | 429 | LE: ; 430 | } 431 | 432 | return $xmlElement; 433 | } 434 | 435 | /** 436 | * Setter for jsonOptions 437 | * 438 | * @param int $options 439 | * @return self 440 | */ 441 | public function setJsonOptions($options) 442 | { 443 | $this->jsonOptions = $options; 444 | return $this; 445 | } 446 | 447 | /** 448 | * Getter for jsonOptions 449 | * 450 | * @return int 451 | */ 452 | public function getJsonOptions() 453 | { 454 | $options = $this->jsonOptions; 455 | if ($this->pretty) { 456 | $options = $options | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES; 457 | } 458 | return $options; 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /src/SimplePsrStream.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 30 | } 31 | 32 | /** 33 | * Reads all data from the stream into a string, from the beginning to end. 34 | * 35 | * This method MUST attempt to seek to the beginning of the stream before 36 | * reading data and read the stream until the end is reached. 37 | * 38 | * Warning: This could attempt to load a large amount of data into memory. 39 | * 40 | * This method MUST NOT raise an exception in order to conform with PHP's 41 | * string casting operations. 42 | * 43 | * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring 44 | * @return string 45 | */ 46 | public function __toString() 47 | { 48 | if (!$this->stream) { 49 | return ''; 50 | } 51 | 52 | try { 53 | $this->rewind(); 54 | return $this->getContents(); 55 | } catch (RuntimeException $e) { 56 | return ''; 57 | } 58 | } 59 | 60 | /** 61 | * Closes the stream and any underlying resources. 62 | * 63 | * @return void 64 | */ 65 | public function close() 66 | { 67 | if ($this->stream) { 68 | fclose($this->stream); 69 | } 70 | $this->stream = null; 71 | } 72 | 73 | /** 74 | * Separates any underlying resources from the stream. 75 | * 76 | * After the stream has been detached, the stream is in an unusable state. 77 | * 78 | * @return resource|null Underlying PHP stream, if any 79 | */ 80 | public function detach() 81 | { 82 | $stream = $this->stream; 83 | $this->stream = null; 84 | 85 | return $stream; 86 | } 87 | 88 | /** 89 | * Get the size of the stream if known. 90 | * 91 | * @return int|null Returns the size in bytes if known, or null if unknown. 92 | */ 93 | public function getSize() 94 | { 95 | if (!$this->stream) { 96 | return null; 97 | } 98 | 99 | $stats = fstat($this->stream); 100 | if (isset($stats['size'])) { 101 | return $stats['size']; 102 | } 103 | 104 | return null; 105 | } 106 | 107 | /** 108 | * Returns the current position of the file read/write pointer 109 | * 110 | * @return int Position of the file pointer 111 | * @throws \RuntimeException on error. 112 | */ 113 | public function tell() 114 | { 115 | if (!$this->stream) { 116 | throw new RuntimeException('No stream'); 117 | } 118 | 119 | $position = ftell($this->stream); 120 | if (!$position) { 121 | throw new RuntimeException('Cannot get current position'); 122 | } 123 | 124 | return $position; 125 | } 126 | 127 | /** 128 | * Returns true if the stream is at the end of the stream. 129 | * 130 | * @return bool 131 | */ 132 | public function eof() 133 | { 134 | if (!$this->stream) { 135 | return true; 136 | } 137 | 138 | return feof($this->stream); 139 | } 140 | 141 | /** 142 | * Returns whether or not the stream is seekable. 143 | * 144 | * @return bool 145 | */ 146 | public function isSeekable() 147 | { 148 | if ($this->stream) { 149 | $meta = $this->getMetadata(); 150 | return $meta['seekable']; 151 | } 152 | 153 | return false; 154 | } 155 | 156 | /** 157 | * Seek to a position in the stream. 158 | * 159 | * @link http://www.php.net/manual/en/function.fseek.php 160 | * @param int $offset Stream offset 161 | * @param int $whence Specifies how the cursor position will be calculated 162 | * based on the seek offset. Valid values are identical to the built-in 163 | * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to 164 | * offset bytes SEEK_CUR: Set position to current location plus offset 165 | * SEEK_END: Set position to end-of-stream plus offset. 166 | * @throws \RuntimeException on failure. 167 | */ 168 | public function seek($offset, $whence = SEEK_SET) 169 | { 170 | if (!$this->stream) { 171 | throw new RuntimeException("No stream attached"); 172 | } 173 | 174 | if (!$this->isSeekable() || fseek($this->stream, $offset, $whence) === -1) { 175 | throw new RuntimeException('Could not seek in stream'); 176 | } 177 | } 178 | 179 | /** 180 | * Seek to the beginning of the stream. 181 | * 182 | * If the stream is not seekable, this method will raise an exception; 183 | * otherwise, it will perform a seek(0). 184 | * 185 | * @see seek() 186 | * @link http://www.php.net/manual/en/function.fseek.php 187 | * @throws \RuntimeException on failure. 188 | */ 189 | public function rewind() 190 | { 191 | if (!$this->stream) { 192 | throw new RuntimeException("No stream attached"); 193 | } 194 | 195 | if (!$this->isSeekable() || rewind($this->stream) === false) { 196 | throw new RuntimeException('Could not rewind stream'); 197 | } 198 | } 199 | 200 | /** 201 | * Returns whether or not the stream is writable. 202 | * 203 | * @return bool 204 | */ 205 | public function isWritable() 206 | { 207 | if ($this->stream) { 208 | $meta = $this->getMetadata(); 209 | $writableModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+']; 210 | foreach ($writableModes as $mode) { 211 | if (strpos($meta['mode'], $mode) === 0) { 212 | return true; 213 | } 214 | } 215 | } 216 | return false; 217 | } 218 | 219 | /** 220 | * Write data to the stream. 221 | * 222 | * @param string $string The string that is to be written. 223 | * @return int Returns the number of bytes written to the stream. 224 | * @throws \RuntimeException on failure. 225 | */ 226 | public function write($string) 227 | { 228 | if (!$this->stream) { 229 | throw new RuntimeException("No stream attached"); 230 | } 231 | 232 | if (!$this->isWritable() || ($written = fwrite($this->stream, $string)) === false) { 233 | throw new RuntimeException('Could not write to stream'); 234 | } 235 | 236 | return $written; 237 | } 238 | 239 | /** 240 | * Returns whether or not the stream is readable. 241 | * 242 | * @return bool 243 | */ 244 | public function isReadable() 245 | { 246 | if ($this->stream) { 247 | $meta = $this->getMetadata(); 248 | $readableModes = ['r', 'r+', 'w+', 'a+', 'x+', 'c+']; 249 | foreach ($readableModes as $mode) { 250 | if (strpos($meta['mode'], $mode) === 0) { 251 | return true; 252 | } 253 | } 254 | } 255 | 256 | return false; 257 | } 258 | 259 | /** 260 | * Read data from the stream. 261 | * 262 | * @param int $length Read up to $length bytes from the object and return 263 | * them. Fewer than $length bytes may be returned if underlying stream 264 | * call returns fewer bytes. 265 | * @return string Returns the data read from the stream, or an empty string 266 | * if no bytes are available. 267 | * @throws \RuntimeException if an error occurs. 268 | */ 269 | public function read($length) 270 | { 271 | if (!$this->stream) { 272 | throw new RuntimeException("No stream attached"); 273 | } 274 | 275 | if (!$this->isReadable() || ($data = fread($this->stream, $length)) === false) { 276 | throw new RuntimeException('Could not read from stream'); 277 | } 278 | 279 | return $data; 280 | } 281 | 282 | /** 283 | * Returns the remaining contents in a string 284 | * 285 | * @return string 286 | * @throws \RuntimeException if unable to read or an error occurs while 287 | * reading. 288 | */ 289 | public function getContents() 290 | { 291 | if (!$this->stream) { 292 | throw new RuntimeException("No stream attached"); 293 | } 294 | 295 | if (!$this->isReadable() || ($contents = stream_get_contents($this->stream)) === false) { 296 | throw new RuntimeException('Could not get contents of stream'); 297 | } 298 | 299 | return $contents; 300 | } 301 | 302 | /** 303 | * Get stream metadata as an associative array or retrieve a specific key. 304 | * 305 | * The keys returned are identical to the keys returned from PHP's 306 | * stream_get_meta_data() function. 307 | * 308 | * @link http://php.net/manual/en/function.stream-get-meta-data.php 309 | * @param string $key Specific metadata to retrieve. 310 | * @return array|mixed|null Returns an associative array if no key is 311 | * provided. Returns a specific key value if a key is provided and the 312 | * value is found, or null if the key is not found. 313 | */ 314 | public function getMetadata($key = null) 315 | { 316 | $meta = stream_get_meta_data($this->stream); 317 | if ($key === null) { 318 | return $meta; 319 | } 320 | 321 | return isset($meta[$key]) ? $meta[$key] : null; 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /tests/ApiProblemRendererTest.php: -------------------------------------------------------------------------------- 1 | withUri(new Uri('http://example.com')) 25 | ->withAddedHeader('Accept', $mediaType); 26 | 27 | $response = new Response(); 28 | 29 | $response = $renderer->render($request, $response, $problem); 30 | 31 | $this->assertSame($expectedMediaType, $response->getHeaderLine('Content-Type')); 32 | $this->assertSame($expectedBody, (string)$response->getBody()); 33 | 34 | if ($problem->getStatus()) { 35 | $this->assertSame($problem->getStatus(), $response->getStatusCode()); 36 | } 37 | } 38 | 39 | /** 40 | * Data provider for testRenderer() 41 | * 42 | * Array format: 43 | * 0 => Accept header media type in Request 44 | * 1 => Data array to be rendered 45 | * 2 => Expected media type in Response 46 | * 3 => Expected body string in Response 47 | * 48 | * @return array 49 | */ 50 | public function rendererProvider() 51 | { 52 | $problem = new ApiProblem("foo"); 53 | $problem->setStatus(400); 54 | 55 | $outputData = [ 56 | 'title' => 'foo', 57 | 'type' => 'about:blank', 58 | 'status' => 400, 59 | ]; 60 | 61 | 62 | $expectedJson = json_encode($outputData); 63 | $expectedPrettyJson = json_encode($outputData, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); 64 | 65 | $expectedXML = '' . PHP_EOL 66 | . 'fooabout:blank400' 67 | . PHP_EOL; 68 | 69 | return [ 70 | ['application/hal+json', $problem, 'application/problem+json', $expectedJson, false], 71 | ['application/json', $problem, 'application/problem+json', $expectedJson, false], 72 | ['vnd.foo/anything+json', $problem, 'application/problem+json', $expectedJson, false], 73 | ['application/json', $problem, 'application/problem+json', $expectedPrettyJson, true], 74 | ['application/hal+xml', $problem, 'application/problem+xml', $expectedXML, false], 75 | ['application/xml', $problem, 'application/problem+xml', $expectedXML, false], 76 | ['text/xml', $problem, 'application/problem+xml', $expectedXML, false], 77 | ['vnd.foo/anything+xml', $problem, 'application/problem+xml', $expectedXML, false], 78 | ['text/html', $problem, 'application/problem+json', $expectedJson, false], 79 | 80 | // Specific media type wins 81 | ['application/hal+json,application/problem+xml', $problem, 'application/problem+xml', $expectedXML, false], 82 | ]; 83 | } 84 | 85 | /** 86 | * The data has to be an ApiProblem object 87 | */ 88 | public function testCaseWhenDataIsNotAnApiProblemObject() 89 | { 90 | $problem = 'Alex'; 91 | 92 | $request = (new Request()) 93 | ->withUri(new Uri('http://example.com')) 94 | ->withAddedHeader('Accept', 'application/json'); 95 | $response = new Response(); 96 | $renderer = new Renderer(); 97 | 98 | $this->expectException(RuntimeException::class); 99 | $this->expectExceptionMessage('Data is not an ApiProblem object'); 100 | 101 | $response = $renderer->render($request, $response, $problem); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/HalRendererTest.php: -------------------------------------------------------------------------------- 1 | withUri(new Uri('http://example.com')) 25 | ->withAddedHeader('Accept', $mediaType); 26 | 27 | $response = new Response(); 28 | 29 | $response = $renderer->render($request, $response, $data); 30 | 31 | $this->assertSame($expectedMediaType, $response->getHeaderLine('Content-Type')); 32 | $this->assertSame($expectedBody, (string)$response->getBody()); 33 | } 34 | 35 | /** 36 | * Data provider for testRenderer() 37 | * 38 | * Array format: 39 | * 0 => Accept header media type in Request 40 | * 1 => Data array to be rendered 41 | * 2 => Expected media type in Response 42 | * 3 => Expected body string in Response 43 | * 44 | * @return array 45 | */ 46 | public function rendererProvider() 47 | { 48 | $items = [ 49 | [ 50 | 'name' => 'Alex', 51 | 'is_admin' => true, 52 | ], 53 | [ 54 | 'name' => 'Robin', 55 | 'is_admin' => false, 56 | ], 57 | ]; 58 | 59 | $data = new Hal( 60 | '/foo', 61 | [ 62 | 'items' => $items, 63 | ] 64 | ); 65 | 66 | $expectedJson = json_encode([ 67 | 'items' => $items, 68 | '_links' => [ 69 | 'self' => [ 70 | 'href' => '/foo', 71 | ], 72 | ] 73 | ]); 74 | 75 | $expectedPrettyJson = json_encode([ 76 | 'items' => $items, 77 | '_links' => [ 78 | 'self' => [ 79 | 'href' => '/foo', 80 | ], 81 | ] 82 | ], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); 83 | 84 | $expectedXML = '' . PHP_EOL 85 | . 'Alex1' 86 | . 'Robin0' 87 | . PHP_EOL; 88 | 89 | $expectedHTML = ' 90 | 91 | 92 | 93 | 94 | 110 | 111 | 133 | 134 | 135 | '; 136 | 137 | return [ 138 | ['application/hal+json', $data, 'application/hal+json', $expectedJson, false], 139 | ['application/anything+json', $data, 'application/hal+json', $expectedJson, false], 140 | ['application/json', $data, 'application/hal+json', $expectedJson, false], 141 | ['application/json', $data, 'application/hal+json', $expectedPrettyJson, true], 142 | ['application/hal+xml', $data, 'application/hal+xml', $expectedXML, false], 143 | ['application/anything+xml', $data, 'application/hal+xml', $expectedXML, false], 144 | ['application/xml', $data, 'application/hal+xml', $expectedXML, false], 145 | ['text/xml', $data, 'application/hal+xml', $expectedXML, false], 146 | ['text/html', $data, 'text/html', $expectedHTML, false], 147 | 148 | // specific media type wins 149 | ['application/xml,application/hal+json', $data, 'application/hal+json', $expectedJson, false], 150 | ]; 151 | } 152 | 153 | 154 | /** 155 | * The data has to be a Hal object 156 | */ 157 | public function testCaseWhenDataIsNotAHalObject() 158 | { 159 | $data = 'Alex'; 160 | 161 | $request = (new Request()) 162 | ->withUri(new Uri('http://example.com')) 163 | ->withAddedHeader('Accept', 'application/json'); 164 | $response = new Response(); 165 | $renderer = new Renderer(); 166 | 167 | $this->expectException(RuntimeException::class); 168 | $this->expectExceptionMessage('Data is not a Hal object'); 169 | 170 | $response = $renderer->render($request, $response, $data); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /tests/NonRewindableStream.php: -------------------------------------------------------------------------------- 1 | determinePeferredFormat($acceptHeader, $allowedFormats, $defaultFormat); 43 | 44 | $this->assertSame($expectedFormat, $format); 45 | } 46 | 47 | 48 | /** 49 | * Test that a given array is rendered to the correct media type 50 | * 51 | * @dataProvider rendererProvider 52 | */ 53 | public function testRenderer($pretty, $mediaType, $data, $expectedMediaType, $expectedBody, $defaultMediaType) 54 | { 55 | $renderer = new Renderer($pretty); 56 | if ($defaultMediaType) { 57 | $renderer->setDefaultMediaType($defaultMediaType); 58 | } 59 | 60 | $request = (new Request()) 61 | ->withUri(new Uri('http://example.com')) 62 | ->withAddedHeader('Accept', $mediaType); 63 | 64 | $response = new Response(); 65 | 66 | $response = $renderer->render($request, $response, $data); 67 | 68 | $this->assertSame($expectedMediaType, $response->getHeaderLine('Content-Type')); 69 | $this->assertSame($expectedBody, (string)$response->getBody()); 70 | $this->assertInstanceOf(Stream::class, $response->getBody()); 71 | } 72 | 73 | /** 74 | * Data provider for testRenderer() 75 | * 76 | * Array format: 77 | * 0 => Accept header media type in Request 78 | * 1 => Data array to be rendered 79 | * 2 => Expected media type in Response 80 | * 3 => Expected body string in Response 81 | * 4 => Default media type for Renderer 82 | * 83 | * @return array 84 | */ 85 | public function rendererProvider() 86 | { 87 | 88 | $dataArray = [ 89 | 'items' => [ 90 | [ 91 | 'name' => 'Alex', 92 | 'is_admin' => true, 93 | ], 94 | [ 95 | 'name' => 'Robin', 96 | 'is_admin' => false, 97 | 'link' => 'http://example.com', 98 | ], 99 | ], 100 | ]; 101 | 102 | $dataScalar = 'Hello World'; 103 | 104 | $dataSerializableClass = new Support\SerializableClass($dataArray); 105 | 106 | $expectedJson = json_encode($dataArray, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 107 | $expectedNonPrettyJson = json_encode($dataArray); 108 | 109 | $expectedScalarJson = '"Hello World"'; 110 | 111 | $expectedXML = ' 112 | 113 | 114 | Alex 115 | 1 116 | 117 | 118 | Robin 119 | 0 120 | http://example.com 121 | 122 | 123 | '; 124 | 125 | $expectedNonPrettyXML = '' 126 | . "\n" 127 | .'Alex1Robin' 128 | . '0http://example.com' 129 | . "\n"; 130 | 131 | $expectedXML2 = ' 132 | 133 | <_0>1 134 | bar 135 | <_1>3 136 | 137 | '; 138 | 139 | $expectedHTML = ' 140 | 141 | 142 | 143 | 144 | 160 | 161 | 177 | 178 | 179 | '; 180 | 181 | $expectedScalarHTML = ' 182 | 183 | 184 | 185 | 186 | 202 | 203 | Hello World 204 | 205 | '; 206 | 207 | $expectedXML2 = ' 208 | 209 | <_0>1 210 | bar 211 | <_1>3 212 | 213 | '; 214 | 215 | return [ 216 | [true, 'application/json', $dataArray, 'application/json', $expectedJson, null], 217 | [true, 'application/json', $dataScalar, 'application/json', $expectedScalarJson, null], 218 | [true, 'application/json', $dataSerializableClass, 'application/json', $expectedJson, null], 219 | [true, 'application/xml', $dataArray, 'application/xml', $expectedXML, null], 220 | [true, 'application/xml', $dataSerializableClass, 'application/xml', $expectedXML, null], 221 | [true, 'text/xml', $dataArray, 'text/xml', $expectedXML, null], 222 | [true, 'text/xml', $dataSerializableClass, 'text/xml', $expectedXML, null], 223 | [true, 'text/html', $dataArray, 'text/html', $expectedHTML, null], 224 | [true, 'text/html', $dataScalar, 'text/html', $expectedScalarHTML, null], 225 | [true, 'text/html', $dataSerializableClass, 'text/html', $expectedHTML, null], 226 | 227 | // default to JSON for unknown media type 228 | [true, 'text/csv', $dataArray, 'application/json', $expectedJson, null], 229 | 230 | // default to HTML in this case for unknown media type 231 | [true, 'text/csv', $dataArray, 'text/html', $expectedHTML, 'text/html'], 232 | 233 | // Numeric array indexes can cause trouble for XML 234 | [true, 'text/xml', [[1], 'foo' => 'bar', 3], 'text/xml', $expectedXML2, null], 235 | 236 | // Pretty can be turned off 237 | [false, 'application/json', $dataArray, 'application/json', $expectedNonPrettyJson, null], 238 | [false, 'application/xml', $dataArray, 'application/xml', $expectedNonPrettyXML, null], 239 | ]; 240 | } 241 | 242 | /** 243 | * Test that given data type, which is not allowed by given media type, throws an exception 244 | * 245 | * @dataProvider rendererErrorsProvider 246 | */ 247 | public function testRendererErrors($mediaType, $data, $error) 248 | { 249 | $request = (new Request()) 250 | ->withUri(new Uri('http://example.com')) 251 | ->withAddedHeader('Accept', $mediaType); 252 | $response = new Response(); 253 | $renderer = new Renderer(); 254 | 255 | $this->expectException(RuntimeException::class); 256 | $this->expectExceptionMessage($error); 257 | 258 | $response = $renderer->render($request, $response, $data); 259 | } 260 | 261 | /** 262 | * Data provider for testRendererErrors() 263 | * 264 | * Array format: 265 | * 0 => Accept header media type in Request 266 | * 1 => Data array to be rendered 267 | * 2 => Expected error string 268 | * 269 | * @return array 270 | */ 271 | public function rendererErrorsProvider() 272 | { 273 | $class = new \stdClass(); 274 | $scalar = 'Hello World'; 275 | $ressource = fopen('php://input', 'r'); 276 | 277 | $xmlError = 'Data for mediaType text/xml must be array or JsonSerializable'; 278 | $htmlError = 'Data for mediaType text/html must be scalar or array or JsonSerializable'; 279 | $jsonError = 'Data for mediaType application/json must be scalar or array or JsonSerializable'; 280 | 281 | return [ 282 | ['application/json', $class, $jsonError], 283 | ['application/json', $ressource, $jsonError], 284 | ['text/xml', $class, $xmlError], 285 | ['text/xml', $scalar, $xmlError], 286 | ['text/xml', $ressource, $xmlError], 287 | ['text/html', $class, $htmlError], 288 | ['text/html', $ressource, $htmlError] 289 | ]; 290 | } 291 | 292 | /** 293 | * Can change the surrounding HTML 294 | */ 295 | public function testCustomHtml() 296 | { 297 | $data = [ 298 | 'items' => [ 299 | [ 300 | 'name' => 'Alex', 301 | 'is_admin' => true, 302 | ], 303 | ], 304 | ]; 305 | 306 | $request = (new Request()) 307 | ->withUri(new Uri('http://example.com')) 308 | ->withAddedHeader('Accept', 'text/html'); 309 | $response = new Response(); 310 | $renderer = new Renderer(); 311 | 312 | $renderer->setHtmlPrefix(''); 313 | $renderer->setHtmlPostfix(''); 314 | 315 | $response = $renderer->render($request, $response, $data); 316 | 317 | $expectedBody = ' 327 | '; 328 | 329 | $this->assertSame('text/html', $response->getHeaderLine('Content-Type')); 330 | $this->assertSame($expectedBody, (string)$response->getBody()); 331 | } 332 | 333 | /** 334 | * Change root element name 335 | */ 336 | public function testXmlWithCustomRootElement() 337 | { 338 | $dataArray = [ 339 | 'items' => [ 340 | [ 341 | 'name' => 'Alex', 342 | 'is_admin' => true, 343 | ], 344 | [ 345 | 'name' => 'Robin', 346 | 'is_admin' => false, 347 | 'link' => 'http://example.com', 348 | ], 349 | ], 350 | ]; 351 | 352 | $expectedXMLCustomRoot = ' 353 | 354 | 355 | Alex 356 | 1 357 | 358 | 359 | Robin 360 | 0 361 | http://example.com 362 | 363 | 364 | '; 365 | $request = (new Request()) 366 | ->withUri(new Uri('http://example.com')) 367 | ->withAddedHeader('Accept', 'application/xml'); 368 | 369 | $response = new Response(); 370 | $renderer = new Renderer(); 371 | 372 | $renderer->setXmlRootElementName('users'); 373 | 374 | $response = $renderer->render($request, $response, $dataArray); 375 | 376 | $this->assertSame('application/xml', $response->getHeaderLine('Content-Type')); 377 | $this->assertSame($expectedXMLCustomRoot, (string)$response->getBody()); 378 | } 379 | 380 | /** 381 | * If the stream in the Response is not writable, then we need to replace 382 | * it with our own SimplePsrStream 383 | */ 384 | public function testUseOurOwnStreamIfCurrentOneIsNotWritable() 385 | { 386 | $data = [ 387 | 'items' => [ 388 | [ 389 | 'name' => 'Alex', 390 | 'is_admin' => true, 391 | ], 392 | [ 393 | 'name' => 'Robin', 394 | 'is_admin' => false, 395 | ], 396 | ], 397 | ]; 398 | 399 | $request = (new Request()) 400 | ->withUri(new Uri('http://example.com')) 401 | ->withAddedHeader('Accept', 'application/json'); 402 | 403 | $response = new Response(); 404 | $response = $response->withBody(new Stream('php://temp', 'r')); 405 | 406 | $renderer = new Renderer(); 407 | $response = $renderer->render($request, $response, $data); 408 | 409 | $this->assertSame('application/json', $response->getHeaderLine('Content-Type')); 410 | $this->assertSame(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), (string)$response->getBody()); 411 | $this->assertInstanceOf('RKA\ContentTypeRenderer\SimplePsrStream', $response->getBody()); 412 | } 413 | 414 | /** 415 | * If the stream in the Response cannot be rewound, then we need to replace 416 | * it with our own SimplePsrStream 417 | */ 418 | public function testUseOurOwnStreamIfCurrentOneIsNotRewindable() 419 | { 420 | $data = [ 421 | 'items' => [ 422 | [ 423 | 'name' => 'Alex', 424 | 'is_admin' => true, 425 | ], 426 | [ 427 | 'name' => 'Robin', 428 | 'is_admin' => false, 429 | ], 430 | ], 431 | ]; 432 | 433 | $request = (new Request()) 434 | ->withUri(new Uri('http://example.com')) 435 | ->withAddedHeader('Accept', 'application/json'); 436 | 437 | stream_wrapper_register("norewind", NonRewindableStream::class); 438 | 439 | $response = new Response(); 440 | $response = $response->withBody(new Stream('norewind://temp', 'a')); 441 | 442 | $renderer = new Renderer(); 443 | $response = $renderer->render($request, $response, $data); 444 | 445 | $this->assertSame('application/json', $response->getHeaderLine('Content-Type')); 446 | $this->assertSame(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), (string)$response->getBody()); 447 | $this->assertInstanceOf('RKA\ContentTypeRenderer\SimplePsrStream', $response->getBody()); 448 | } 449 | 450 | public function testJsonOptions() 451 | { 452 | $data = [ 453 | [ 454 | 'name' => 'Alex', 455 | 'is_admin' => true, 456 | ], 457 | [ 458 | 'name' => 'Robin', 459 | 'is_admin' => false, 460 | ], 461 | ]; 462 | 463 | $renderer = new Renderer(true); 464 | $renderer->setJsonOptions(JSON_FORCE_OBJECT); 465 | 466 | $request = (new Request()) 467 | ->withUri(new Uri('http://example.com')) 468 | ->withAddedHeader('Accept', 'application/json'); 469 | 470 | $response = new Response(); 471 | $response = $renderer->render($request, $response, $data); 472 | 473 | $expectedBody = json_encode($data, JSON_FORCE_OBJECT | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 474 | $this->assertSame($expectedBody, (string)$response->getBody()); 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /tests/Support/SerializableClass.php: -------------------------------------------------------------------------------- 1 | data = $data; 11 | } 12 | 13 | #[\ReturnTypeWillChange] 14 | public function jsonSerialize() 15 | { 16 | return $this->data; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | addPsr4('RKA\\ContentTypeRenderer\\Tests\\', __DIR__); 6 | --------------------------------------------------------------------------------