├── .gitignore ├── LICENSE ├── composer.json ├── composer.lock ├── phpunit.xml ├── readme.md ├── src ├── Message │ ├── AbstractMessage.php │ ├── Enum │ │ ├── RequestMethod.php │ │ └── RtspVersion.php │ ├── MessageException.php │ ├── MessageFactory.php │ ├── Request.php │ ├── RequestParser.php │ └── Response.php ├── Middleware │ ├── AutoContentLength.php │ ├── AutoCseq.php │ ├── Log.php │ └── MiddlewareStack.php ├── Server.php └── ServerException.php └── tests ├── ConnectionStub.php ├── Message └── RequestParserTest.php ├── Middleware ├── AutoContentLengthTest.php ├── AutoCseqTest.php ├── LogTest.php └── MiddlewareStackTest.php ├── Mock ├── CallNextMiddleware.php ├── ParametrizedResponseMiddleware.php └── RejectMiddleware.php ├── ServerTest.php ├── SocketServerStub.php ├── bootstrap.php ├── server.php └── serverWithMiddlewares.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /vendor 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Samuel CHEMLA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpbg/rtsp", 3 | "description": "Basic RTSP 1.0 server built on top of react", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "rtsp", 8 | "react", 9 | "server" 10 | ], 11 | "authors": [ 12 | { 13 | "name": "Samuel CHEMLA", 14 | "email": "chemla.samuel@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "react/socket": "^1.1", 20 | "react/promise": "^2.7", 21 | "psr/log": "^1.0", 22 | "myclabs/php-enum": "^1.5", 23 | "evenement/evenement": "^3.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "PhpBg\\Rtsp\\": "src" 28 | } 29 | }, 30 | "require-dev": { 31 | "phpunit/phpunit": "^6.5", 32 | "mockery/mockery": "^1.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "438f358546ee93dde483319cf9c665af", 8 | "packages": [ 9 | { 10 | "name": "evenement/evenement", 11 | "version": "v3.0.1", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/igorw/evenement.git", 15 | "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", 20 | "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^6.0" 28 | }, 29 | "type": "library", 30 | "autoload": { 31 | "psr-0": { 32 | "Evenement": "src" 33 | } 34 | }, 35 | "notification-url": "https://packagist.org/downloads/", 36 | "license": [ 37 | "MIT" 38 | ], 39 | "authors": [ 40 | { 41 | "name": "Igor Wiedler", 42 | "email": "igor@wiedler.ch" 43 | } 44 | ], 45 | "description": "Événement is a very simple event dispatching library for PHP", 46 | "keywords": [ 47 | "event-dispatcher", 48 | "event-emitter" 49 | ], 50 | "time": "2017-07-23T21:35:13+00:00" 51 | }, 52 | { 53 | "name": "myclabs/php-enum", 54 | "version": "1.6.4", 55 | "source": { 56 | "type": "git", 57 | "url": "https://github.com/myclabs/php-enum.git", 58 | "reference": "550d2334d77f91b0816a5cbd6965272fe20146b8" 59 | }, 60 | "dist": { 61 | "type": "zip", 62 | "url": "https://api.github.com/repos/myclabs/php-enum/zipball/550d2334d77f91b0816a5cbd6965272fe20146b8", 63 | "reference": "550d2334d77f91b0816a5cbd6965272fe20146b8", 64 | "shasum": "" 65 | }, 66 | "require": { 67 | "php": ">=5.4" 68 | }, 69 | "require-dev": { 70 | "phpunit/phpunit": "^4.8.35|^5.7|^6.0", 71 | "squizlabs/php_codesniffer": "1.*" 72 | }, 73 | "type": "library", 74 | "autoload": { 75 | "psr-4": { 76 | "MyCLabs\\Enum\\": "src/" 77 | } 78 | }, 79 | "notification-url": "https://packagist.org/downloads/", 80 | "license": [ 81 | "MIT" 82 | ], 83 | "authors": [ 84 | { 85 | "name": "PHP Enum contributors", 86 | "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" 87 | } 88 | ], 89 | "description": "PHP Enum implementation", 90 | "homepage": "http://github.com/myclabs/php-enum", 91 | "keywords": [ 92 | "enum" 93 | ], 94 | "time": "2018-10-30T14:36:18+00:00" 95 | }, 96 | { 97 | "name": "psr/log", 98 | "version": "1.1.0", 99 | "source": { 100 | "type": "git", 101 | "url": "https://github.com/php-fig/log.git", 102 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" 103 | }, 104 | "dist": { 105 | "type": "zip", 106 | "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 107 | "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", 108 | "shasum": "" 109 | }, 110 | "require": { 111 | "php": ">=5.3.0" 112 | }, 113 | "type": "library", 114 | "extra": { 115 | "branch-alias": { 116 | "dev-master": "1.0.x-dev" 117 | } 118 | }, 119 | "autoload": { 120 | "psr-4": { 121 | "Psr\\Log\\": "Psr/Log/" 122 | } 123 | }, 124 | "notification-url": "https://packagist.org/downloads/", 125 | "license": [ 126 | "MIT" 127 | ], 128 | "authors": [ 129 | { 130 | "name": "PHP-FIG", 131 | "homepage": "http://www.php-fig.org/" 132 | } 133 | ], 134 | "description": "Common interface for logging libraries", 135 | "homepage": "https://github.com/php-fig/log", 136 | "keywords": [ 137 | "log", 138 | "psr", 139 | "psr-3" 140 | ], 141 | "time": "2018-11-20T15:27:04+00:00" 142 | }, 143 | { 144 | "name": "react/cache", 145 | "version": "v0.5.0", 146 | "source": { 147 | "type": "git", 148 | "url": "https://github.com/reactphp/cache.git", 149 | "reference": "7d7da7fb7574d471904ba357b39bbf110ccdbf66" 150 | }, 151 | "dist": { 152 | "type": "zip", 153 | "url": "https://api.github.com/repos/reactphp/cache/zipball/7d7da7fb7574d471904ba357b39bbf110ccdbf66", 154 | "reference": "7d7da7fb7574d471904ba357b39bbf110ccdbf66", 155 | "shasum": "" 156 | }, 157 | "require": { 158 | "php": ">=5.3.0", 159 | "react/promise": "~2.0|~1.1" 160 | }, 161 | "require-dev": { 162 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" 163 | }, 164 | "type": "library", 165 | "autoload": { 166 | "psr-4": { 167 | "React\\Cache\\": "src/" 168 | } 169 | }, 170 | "notification-url": "https://packagist.org/downloads/", 171 | "license": [ 172 | "MIT" 173 | ], 174 | "description": "Async, Promise-based cache interface for ReactPHP", 175 | "keywords": [ 176 | "cache", 177 | "caching", 178 | "promise", 179 | "reactphp" 180 | ], 181 | "time": "2018-06-25T12:52:40+00:00" 182 | }, 183 | { 184 | "name": "react/dns", 185 | "version": "v0.4.16", 186 | "source": { 187 | "type": "git", 188 | "url": "https://github.com/reactphp/dns.git", 189 | "reference": "0a0bedfec72b38406413c6ea01e1c015bd0bf72b" 190 | }, 191 | "dist": { 192 | "type": "zip", 193 | "url": "https://api.github.com/repos/reactphp/dns/zipball/0a0bedfec72b38406413c6ea01e1c015bd0bf72b", 194 | "reference": "0a0bedfec72b38406413c6ea01e1c015bd0bf72b", 195 | "shasum": "" 196 | }, 197 | "require": { 198 | "php": ">=5.3.0", 199 | "react/cache": "^0.5 || ^0.4 || ^0.3", 200 | "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", 201 | "react/promise": "^2.1 || ^1.2.1", 202 | "react/promise-timer": "^1.2", 203 | "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.5" 204 | }, 205 | "require-dev": { 206 | "clue/block-react": "^1.2", 207 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" 208 | }, 209 | "type": "library", 210 | "autoload": { 211 | "psr-4": { 212 | "React\\Dns\\": "src" 213 | } 214 | }, 215 | "notification-url": "https://packagist.org/downloads/", 216 | "license": [ 217 | "MIT" 218 | ], 219 | "description": "Async DNS resolver for ReactPHP", 220 | "keywords": [ 221 | "async", 222 | "dns", 223 | "dns-resolver", 224 | "reactphp" 225 | ], 226 | "time": "2018-11-11T11:21:13+00:00" 227 | }, 228 | { 229 | "name": "react/event-loop", 230 | "version": "v1.0.0", 231 | "source": { 232 | "type": "git", 233 | "url": "https://github.com/reactphp/event-loop.git", 234 | "reference": "0266aff7aa7b0613b1f38a723e14a0ebc55cfca3" 235 | }, 236 | "dist": { 237 | "type": "zip", 238 | "url": "https://api.github.com/repos/reactphp/event-loop/zipball/0266aff7aa7b0613b1f38a723e14a0ebc55cfca3", 239 | "reference": "0266aff7aa7b0613b1f38a723e14a0ebc55cfca3", 240 | "shasum": "" 241 | }, 242 | "require": { 243 | "php": ">=5.3.0" 244 | }, 245 | "require-dev": { 246 | "phpunit/phpunit": "~4.8.35 || ^5.7 || ^6.4" 247 | }, 248 | "suggest": { 249 | "ext-event": "~1.0 for ExtEventLoop", 250 | "ext-pcntl": "For signal handling support when using the StreamSelectLoop" 251 | }, 252 | "type": "library", 253 | "autoload": { 254 | "psr-4": { 255 | "React\\EventLoop\\": "src" 256 | } 257 | }, 258 | "notification-url": "https://packagist.org/downloads/", 259 | "license": [ 260 | "MIT" 261 | ], 262 | "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", 263 | "keywords": [ 264 | "asynchronous", 265 | "event-loop" 266 | ], 267 | "time": "2018-07-11T14:37:46+00:00" 268 | }, 269 | { 270 | "name": "react/promise", 271 | "version": "v2.7.0", 272 | "source": { 273 | "type": "git", 274 | "url": "https://github.com/reactphp/promise.git", 275 | "reference": "f4edc2581617431aea50430749db55cc3fc031b3" 276 | }, 277 | "dist": { 278 | "type": "zip", 279 | "url": "https://api.github.com/repos/reactphp/promise/zipball/f4edc2581617431aea50430749db55cc3fc031b3", 280 | "reference": "f4edc2581617431aea50430749db55cc3fc031b3", 281 | "shasum": "" 282 | }, 283 | "require": { 284 | "php": ">=5.4.0" 285 | }, 286 | "require-dev": { 287 | "phpunit/phpunit": "~4.8" 288 | }, 289 | "type": "library", 290 | "autoload": { 291 | "psr-4": { 292 | "React\\Promise\\": "src/" 293 | }, 294 | "files": [ 295 | "src/functions_include.php" 296 | ] 297 | }, 298 | "notification-url": "https://packagist.org/downloads/", 299 | "license": [ 300 | "MIT" 301 | ], 302 | "authors": [ 303 | { 304 | "name": "Jan Sorgalla", 305 | "email": "jsorgalla@gmail.com" 306 | } 307 | ], 308 | "description": "A lightweight implementation of CommonJS Promises/A for PHP", 309 | "keywords": [ 310 | "promise", 311 | "promises" 312 | ], 313 | "time": "2018-06-13T15:59:06+00:00" 314 | }, 315 | { 316 | "name": "react/promise-timer", 317 | "version": "v1.5.0", 318 | "source": { 319 | "type": "git", 320 | "url": "https://github.com/reactphp/promise-timer.git", 321 | "reference": "a11206938ca2394dc7bb368f5da25cd4533fa603" 322 | }, 323 | "dist": { 324 | "type": "zip", 325 | "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/a11206938ca2394dc7bb368f5da25cd4533fa603", 326 | "reference": "a11206938ca2394dc7bb368f5da25cd4533fa603", 327 | "shasum": "" 328 | }, 329 | "require": { 330 | "php": ">=5.3", 331 | "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", 332 | "react/promise": "^2.7.0 || ^1.2.1" 333 | }, 334 | "require-dev": { 335 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" 336 | }, 337 | "type": "library", 338 | "autoload": { 339 | "psr-4": { 340 | "React\\Promise\\Timer\\": "src/" 341 | }, 342 | "files": [ 343 | "src/functions.php" 344 | ] 345 | }, 346 | "notification-url": "https://packagist.org/downloads/", 347 | "license": [ 348 | "MIT" 349 | ], 350 | "authors": [ 351 | { 352 | "name": "Christian Lück", 353 | "email": "christian@lueck.tv" 354 | } 355 | ], 356 | "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", 357 | "homepage": "https://github.com/reactphp/promise-timer", 358 | "keywords": [ 359 | "async", 360 | "event-loop", 361 | "promise", 362 | "reactphp", 363 | "timeout", 364 | "timer" 365 | ], 366 | "time": "2018-06-13T16:45:37+00:00" 367 | }, 368 | { 369 | "name": "react/socket", 370 | "version": "v1.1.0", 371 | "source": { 372 | "type": "git", 373 | "url": "https://github.com/reactphp/socket.git", 374 | "reference": "34381d9282d12670eb56b45981aad82e033ed58f" 375 | }, 376 | "dist": { 377 | "type": "zip", 378 | "url": "https://api.github.com/repos/reactphp/socket/zipball/34381d9282d12670eb56b45981aad82e033ed58f", 379 | "reference": "34381d9282d12670eb56b45981aad82e033ed58f", 380 | "shasum": "" 381 | }, 382 | "require": { 383 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 384 | "php": ">=5.3.0", 385 | "react/dns": "^0.4.13", 386 | "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", 387 | "react/promise": "^2.6.0 || ^1.2.1", 388 | "react/promise-timer": "^1.4.0", 389 | "react/stream": "^1.0 || ^0.7.1" 390 | }, 391 | "require-dev": { 392 | "clue/block-react": "^1.2", 393 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" 394 | }, 395 | "type": "library", 396 | "autoload": { 397 | "psr-4": { 398 | "React\\Socket\\": "src" 399 | } 400 | }, 401 | "notification-url": "https://packagist.org/downloads/", 402 | "license": [ 403 | "MIT" 404 | ], 405 | "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", 406 | "keywords": [ 407 | "Connection", 408 | "Socket", 409 | "async", 410 | "reactphp", 411 | "stream" 412 | ], 413 | "time": "2018-10-01T12:20:53+00:00" 414 | }, 415 | { 416 | "name": "react/stream", 417 | "version": "v1.0.0", 418 | "source": { 419 | "type": "git", 420 | "url": "https://github.com/reactphp/stream.git", 421 | "reference": "fdd0140f42805d65bf9687636503db0b326d2244" 422 | }, 423 | "dist": { 424 | "type": "zip", 425 | "url": "https://api.github.com/repos/reactphp/stream/zipball/fdd0140f42805d65bf9687636503db0b326d2244", 426 | "reference": "fdd0140f42805d65bf9687636503db0b326d2244", 427 | "shasum": "" 428 | }, 429 | "require": { 430 | "evenement/evenement": "^3.0 || ^2.0 || ^1.0", 431 | "php": ">=5.3.8", 432 | "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" 433 | }, 434 | "require-dev": { 435 | "clue/stream-filter": "~1.2", 436 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" 437 | }, 438 | "type": "library", 439 | "autoload": { 440 | "psr-4": { 441 | "React\\Stream\\": "src" 442 | } 443 | }, 444 | "notification-url": "https://packagist.org/downloads/", 445 | "license": [ 446 | "MIT" 447 | ], 448 | "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", 449 | "keywords": [ 450 | "event-driven", 451 | "io", 452 | "non-blocking", 453 | "pipe", 454 | "reactphp", 455 | "readable", 456 | "stream", 457 | "writable" 458 | ], 459 | "time": "2018-07-11T14:38:16+00:00" 460 | } 461 | ], 462 | "packages-dev": [ 463 | { 464 | "name": "doctrine/instantiator", 465 | "version": "1.1.0", 466 | "source": { 467 | "type": "git", 468 | "url": "https://github.com/doctrine/instantiator.git", 469 | "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" 470 | }, 471 | "dist": { 472 | "type": "zip", 473 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", 474 | "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", 475 | "shasum": "" 476 | }, 477 | "require": { 478 | "php": "^7.1" 479 | }, 480 | "require-dev": { 481 | "athletic/athletic": "~0.1.8", 482 | "ext-pdo": "*", 483 | "ext-phar": "*", 484 | "phpunit/phpunit": "^6.2.3", 485 | "squizlabs/php_codesniffer": "^3.0.2" 486 | }, 487 | "type": "library", 488 | "extra": { 489 | "branch-alias": { 490 | "dev-master": "1.2.x-dev" 491 | } 492 | }, 493 | "autoload": { 494 | "psr-4": { 495 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 496 | } 497 | }, 498 | "notification-url": "https://packagist.org/downloads/", 499 | "license": [ 500 | "MIT" 501 | ], 502 | "authors": [ 503 | { 504 | "name": "Marco Pivetta", 505 | "email": "ocramius@gmail.com", 506 | "homepage": "http://ocramius.github.com/" 507 | } 508 | ], 509 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 510 | "homepage": "https://github.com/doctrine/instantiator", 511 | "keywords": [ 512 | "constructor", 513 | "instantiate" 514 | ], 515 | "time": "2017-07-22T11:58:36+00:00" 516 | }, 517 | { 518 | "name": "hamcrest/hamcrest-php", 519 | "version": "v2.0.0", 520 | "source": { 521 | "type": "git", 522 | "url": "https://github.com/hamcrest/hamcrest-php.git", 523 | "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad" 524 | }, 525 | "dist": { 526 | "type": "zip", 527 | "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/776503d3a8e85d4f9a1148614f95b7a608b046ad", 528 | "reference": "776503d3a8e85d4f9a1148614f95b7a608b046ad", 529 | "shasum": "" 530 | }, 531 | "require": { 532 | "php": "^5.3|^7.0" 533 | }, 534 | "replace": { 535 | "cordoval/hamcrest-php": "*", 536 | "davedevelopment/hamcrest-php": "*", 537 | "kodova/hamcrest-php": "*" 538 | }, 539 | "require-dev": { 540 | "phpunit/php-file-iterator": "1.3.3", 541 | "phpunit/phpunit": "~4.0", 542 | "satooshi/php-coveralls": "^1.0" 543 | }, 544 | "type": "library", 545 | "extra": { 546 | "branch-alias": { 547 | "dev-master": "2.0-dev" 548 | } 549 | }, 550 | "autoload": { 551 | "classmap": [ 552 | "hamcrest" 553 | ] 554 | }, 555 | "notification-url": "https://packagist.org/downloads/", 556 | "license": [ 557 | "BSD" 558 | ], 559 | "description": "This is the PHP port of Hamcrest Matchers", 560 | "keywords": [ 561 | "test" 562 | ], 563 | "time": "2016-01-20T08:20:44+00:00" 564 | }, 565 | { 566 | "name": "mockery/mockery", 567 | "version": "1.2.2", 568 | "source": { 569 | "type": "git", 570 | "url": "https://github.com/mockery/mockery.git", 571 | "reference": "0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2" 572 | }, 573 | "dist": { 574 | "type": "zip", 575 | "url": "https://api.github.com/repos/mockery/mockery/zipball/0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2", 576 | "reference": "0eb0b48c3f07b3b89f5169ce005b7d05b18cf1d2", 577 | "shasum": "" 578 | }, 579 | "require": { 580 | "hamcrest/hamcrest-php": "~2.0", 581 | "lib-pcre": ">=7.0", 582 | "php": ">=5.6.0" 583 | }, 584 | "require-dev": { 585 | "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0" 586 | }, 587 | "type": "library", 588 | "extra": { 589 | "branch-alias": { 590 | "dev-master": "1.0.x-dev" 591 | } 592 | }, 593 | "autoload": { 594 | "psr-0": { 595 | "Mockery": "library/" 596 | } 597 | }, 598 | "notification-url": "https://packagist.org/downloads/", 599 | "license": [ 600 | "BSD-3-Clause" 601 | ], 602 | "authors": [ 603 | { 604 | "name": "Pádraic Brady", 605 | "email": "padraic.brady@gmail.com", 606 | "homepage": "http://blog.astrumfutura.com" 607 | }, 608 | { 609 | "name": "Dave Marshall", 610 | "email": "dave.marshall@atstsolutions.co.uk", 611 | "homepage": "http://davedevelopment.co.uk" 612 | } 613 | ], 614 | "description": "Mockery is a simple yet flexible PHP mock object framework", 615 | "homepage": "https://github.com/mockery/mockery", 616 | "keywords": [ 617 | "BDD", 618 | "TDD", 619 | "library", 620 | "mock", 621 | "mock objects", 622 | "mockery", 623 | "stub", 624 | "test", 625 | "test double", 626 | "testing" 627 | ], 628 | "time": "2019-02-13T09:37:52+00:00" 629 | }, 630 | { 631 | "name": "myclabs/deep-copy", 632 | "version": "1.8.1", 633 | "source": { 634 | "type": "git", 635 | "url": "https://github.com/myclabs/DeepCopy.git", 636 | "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8" 637 | }, 638 | "dist": { 639 | "type": "zip", 640 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", 641 | "reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8", 642 | "shasum": "" 643 | }, 644 | "require": { 645 | "php": "^7.1" 646 | }, 647 | "replace": { 648 | "myclabs/deep-copy": "self.version" 649 | }, 650 | "require-dev": { 651 | "doctrine/collections": "^1.0", 652 | "doctrine/common": "^2.6", 653 | "phpunit/phpunit": "^7.1" 654 | }, 655 | "type": "library", 656 | "autoload": { 657 | "psr-4": { 658 | "DeepCopy\\": "src/DeepCopy/" 659 | }, 660 | "files": [ 661 | "src/DeepCopy/deep_copy.php" 662 | ] 663 | }, 664 | "notification-url": "https://packagist.org/downloads/", 665 | "license": [ 666 | "MIT" 667 | ], 668 | "description": "Create deep copies (clones) of your objects", 669 | "keywords": [ 670 | "clone", 671 | "copy", 672 | "duplicate", 673 | "object", 674 | "object graph" 675 | ], 676 | "time": "2018-06-11T23:09:50+00:00" 677 | }, 678 | { 679 | "name": "phar-io/manifest", 680 | "version": "1.0.1", 681 | "source": { 682 | "type": "git", 683 | "url": "https://github.com/phar-io/manifest.git", 684 | "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" 685 | }, 686 | "dist": { 687 | "type": "zip", 688 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", 689 | "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", 690 | "shasum": "" 691 | }, 692 | "require": { 693 | "ext-dom": "*", 694 | "ext-phar": "*", 695 | "phar-io/version": "^1.0.1", 696 | "php": "^5.6 || ^7.0" 697 | }, 698 | "type": "library", 699 | "extra": { 700 | "branch-alias": { 701 | "dev-master": "1.0.x-dev" 702 | } 703 | }, 704 | "autoload": { 705 | "classmap": [ 706 | "src/" 707 | ] 708 | }, 709 | "notification-url": "https://packagist.org/downloads/", 710 | "license": [ 711 | "BSD-3-Clause" 712 | ], 713 | "authors": [ 714 | { 715 | "name": "Arne Blankerts", 716 | "email": "arne@blankerts.de", 717 | "role": "Developer" 718 | }, 719 | { 720 | "name": "Sebastian Heuer", 721 | "email": "sebastian@phpeople.de", 722 | "role": "Developer" 723 | }, 724 | { 725 | "name": "Sebastian Bergmann", 726 | "email": "sebastian@phpunit.de", 727 | "role": "Developer" 728 | } 729 | ], 730 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 731 | "time": "2017-03-05T18:14:27+00:00" 732 | }, 733 | { 734 | "name": "phar-io/version", 735 | "version": "1.0.1", 736 | "source": { 737 | "type": "git", 738 | "url": "https://github.com/phar-io/version.git", 739 | "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" 740 | }, 741 | "dist": { 742 | "type": "zip", 743 | "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", 744 | "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", 745 | "shasum": "" 746 | }, 747 | "require": { 748 | "php": "^5.6 || ^7.0" 749 | }, 750 | "type": "library", 751 | "autoload": { 752 | "classmap": [ 753 | "src/" 754 | ] 755 | }, 756 | "notification-url": "https://packagist.org/downloads/", 757 | "license": [ 758 | "BSD-3-Clause" 759 | ], 760 | "authors": [ 761 | { 762 | "name": "Arne Blankerts", 763 | "email": "arne@blankerts.de", 764 | "role": "Developer" 765 | }, 766 | { 767 | "name": "Sebastian Heuer", 768 | "email": "sebastian@phpeople.de", 769 | "role": "Developer" 770 | }, 771 | { 772 | "name": "Sebastian Bergmann", 773 | "email": "sebastian@phpunit.de", 774 | "role": "Developer" 775 | } 776 | ], 777 | "description": "Library for handling version information and constraints", 778 | "time": "2017-03-05T17:38:23+00:00" 779 | }, 780 | { 781 | "name": "phpdocumentor/reflection-common", 782 | "version": "1.0.1", 783 | "source": { 784 | "type": "git", 785 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 786 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" 787 | }, 788 | "dist": { 789 | "type": "zip", 790 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 791 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 792 | "shasum": "" 793 | }, 794 | "require": { 795 | "php": ">=5.5" 796 | }, 797 | "require-dev": { 798 | "phpunit/phpunit": "^4.6" 799 | }, 800 | "type": "library", 801 | "extra": { 802 | "branch-alias": { 803 | "dev-master": "1.0.x-dev" 804 | } 805 | }, 806 | "autoload": { 807 | "psr-4": { 808 | "phpDocumentor\\Reflection\\": [ 809 | "src" 810 | ] 811 | } 812 | }, 813 | "notification-url": "https://packagist.org/downloads/", 814 | "license": [ 815 | "MIT" 816 | ], 817 | "authors": [ 818 | { 819 | "name": "Jaap van Otterdijk", 820 | "email": "opensource@ijaap.nl" 821 | } 822 | ], 823 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 824 | "homepage": "http://www.phpdoc.org", 825 | "keywords": [ 826 | "FQSEN", 827 | "phpDocumentor", 828 | "phpdoc", 829 | "reflection", 830 | "static analysis" 831 | ], 832 | "time": "2017-09-11T18:02:19+00:00" 833 | }, 834 | { 835 | "name": "phpdocumentor/reflection-docblock", 836 | "version": "4.3.0", 837 | "source": { 838 | "type": "git", 839 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 840 | "reference": "94fd0001232e47129dd3504189fa1c7225010d08" 841 | }, 842 | "dist": { 843 | "type": "zip", 844 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", 845 | "reference": "94fd0001232e47129dd3504189fa1c7225010d08", 846 | "shasum": "" 847 | }, 848 | "require": { 849 | "php": "^7.0", 850 | "phpdocumentor/reflection-common": "^1.0.0", 851 | "phpdocumentor/type-resolver": "^0.4.0", 852 | "webmozart/assert": "^1.0" 853 | }, 854 | "require-dev": { 855 | "doctrine/instantiator": "~1.0.5", 856 | "mockery/mockery": "^1.0", 857 | "phpunit/phpunit": "^6.4" 858 | }, 859 | "type": "library", 860 | "extra": { 861 | "branch-alias": { 862 | "dev-master": "4.x-dev" 863 | } 864 | }, 865 | "autoload": { 866 | "psr-4": { 867 | "phpDocumentor\\Reflection\\": [ 868 | "src/" 869 | ] 870 | } 871 | }, 872 | "notification-url": "https://packagist.org/downloads/", 873 | "license": [ 874 | "MIT" 875 | ], 876 | "authors": [ 877 | { 878 | "name": "Mike van Riel", 879 | "email": "me@mikevanriel.com" 880 | } 881 | ], 882 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 883 | "time": "2017-11-30T07:14:17+00:00" 884 | }, 885 | { 886 | "name": "phpdocumentor/type-resolver", 887 | "version": "0.4.0", 888 | "source": { 889 | "type": "git", 890 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 891 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" 892 | }, 893 | "dist": { 894 | "type": "zip", 895 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", 896 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", 897 | "shasum": "" 898 | }, 899 | "require": { 900 | "php": "^5.5 || ^7.0", 901 | "phpdocumentor/reflection-common": "^1.0" 902 | }, 903 | "require-dev": { 904 | "mockery/mockery": "^0.9.4", 905 | "phpunit/phpunit": "^5.2||^4.8.24" 906 | }, 907 | "type": "library", 908 | "extra": { 909 | "branch-alias": { 910 | "dev-master": "1.0.x-dev" 911 | } 912 | }, 913 | "autoload": { 914 | "psr-4": { 915 | "phpDocumentor\\Reflection\\": [ 916 | "src/" 917 | ] 918 | } 919 | }, 920 | "notification-url": "https://packagist.org/downloads/", 921 | "license": [ 922 | "MIT" 923 | ], 924 | "authors": [ 925 | { 926 | "name": "Mike van Riel", 927 | "email": "me@mikevanriel.com" 928 | } 929 | ], 930 | "time": "2017-07-14T14:27:02+00:00" 931 | }, 932 | { 933 | "name": "phpspec/prophecy", 934 | "version": "1.8.0", 935 | "source": { 936 | "type": "git", 937 | "url": "https://github.com/phpspec/prophecy.git", 938 | "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" 939 | }, 940 | "dist": { 941 | "type": "zip", 942 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", 943 | "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", 944 | "shasum": "" 945 | }, 946 | "require": { 947 | "doctrine/instantiator": "^1.0.2", 948 | "php": "^5.3|^7.0", 949 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", 950 | "sebastian/comparator": "^1.1|^2.0|^3.0", 951 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 952 | }, 953 | "require-dev": { 954 | "phpspec/phpspec": "^2.5|^3.2", 955 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 956 | }, 957 | "type": "library", 958 | "extra": { 959 | "branch-alias": { 960 | "dev-master": "1.8.x-dev" 961 | } 962 | }, 963 | "autoload": { 964 | "psr-0": { 965 | "Prophecy\\": "src/" 966 | } 967 | }, 968 | "notification-url": "https://packagist.org/downloads/", 969 | "license": [ 970 | "MIT" 971 | ], 972 | "authors": [ 973 | { 974 | "name": "Konstantin Kudryashov", 975 | "email": "ever.zet@gmail.com", 976 | "homepage": "http://everzet.com" 977 | }, 978 | { 979 | "name": "Marcello Duarte", 980 | "email": "marcello.duarte@gmail.com" 981 | } 982 | ], 983 | "description": "Highly opinionated mocking framework for PHP 5.3+", 984 | "homepage": "https://github.com/phpspec/prophecy", 985 | "keywords": [ 986 | "Double", 987 | "Dummy", 988 | "fake", 989 | "mock", 990 | "spy", 991 | "stub" 992 | ], 993 | "time": "2018-08-05T17:53:17+00:00" 994 | }, 995 | { 996 | "name": "phpunit/php-code-coverage", 997 | "version": "5.3.2", 998 | "source": { 999 | "type": "git", 1000 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 1001 | "reference": "c89677919c5dd6d3b3852f230a663118762218ac" 1002 | }, 1003 | "dist": { 1004 | "type": "zip", 1005 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", 1006 | "reference": "c89677919c5dd6d3b3852f230a663118762218ac", 1007 | "shasum": "" 1008 | }, 1009 | "require": { 1010 | "ext-dom": "*", 1011 | "ext-xmlwriter": "*", 1012 | "php": "^7.0", 1013 | "phpunit/php-file-iterator": "^1.4.2", 1014 | "phpunit/php-text-template": "^1.2.1", 1015 | "phpunit/php-token-stream": "^2.0.1", 1016 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 1017 | "sebastian/environment": "^3.0", 1018 | "sebastian/version": "^2.0.1", 1019 | "theseer/tokenizer": "^1.1" 1020 | }, 1021 | "require-dev": { 1022 | "phpunit/phpunit": "^6.0" 1023 | }, 1024 | "suggest": { 1025 | "ext-xdebug": "^2.5.5" 1026 | }, 1027 | "type": "library", 1028 | "extra": { 1029 | "branch-alias": { 1030 | "dev-master": "5.3.x-dev" 1031 | } 1032 | }, 1033 | "autoload": { 1034 | "classmap": [ 1035 | "src/" 1036 | ] 1037 | }, 1038 | "notification-url": "https://packagist.org/downloads/", 1039 | "license": [ 1040 | "BSD-3-Clause" 1041 | ], 1042 | "authors": [ 1043 | { 1044 | "name": "Sebastian Bergmann", 1045 | "email": "sebastian@phpunit.de", 1046 | "role": "lead" 1047 | } 1048 | ], 1049 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 1050 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 1051 | "keywords": [ 1052 | "coverage", 1053 | "testing", 1054 | "xunit" 1055 | ], 1056 | "time": "2018-04-06T15:36:58+00:00" 1057 | }, 1058 | { 1059 | "name": "phpunit/php-file-iterator", 1060 | "version": "1.4.5", 1061 | "source": { 1062 | "type": "git", 1063 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 1064 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" 1065 | }, 1066 | "dist": { 1067 | "type": "zip", 1068 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", 1069 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", 1070 | "shasum": "" 1071 | }, 1072 | "require": { 1073 | "php": ">=5.3.3" 1074 | }, 1075 | "type": "library", 1076 | "extra": { 1077 | "branch-alias": { 1078 | "dev-master": "1.4.x-dev" 1079 | } 1080 | }, 1081 | "autoload": { 1082 | "classmap": [ 1083 | "src/" 1084 | ] 1085 | }, 1086 | "notification-url": "https://packagist.org/downloads/", 1087 | "license": [ 1088 | "BSD-3-Clause" 1089 | ], 1090 | "authors": [ 1091 | { 1092 | "name": "Sebastian Bergmann", 1093 | "email": "sb@sebastian-bergmann.de", 1094 | "role": "lead" 1095 | } 1096 | ], 1097 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 1098 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 1099 | "keywords": [ 1100 | "filesystem", 1101 | "iterator" 1102 | ], 1103 | "time": "2017-11-27T13:52:08+00:00" 1104 | }, 1105 | { 1106 | "name": "phpunit/php-text-template", 1107 | "version": "1.2.1", 1108 | "source": { 1109 | "type": "git", 1110 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 1111 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 1112 | }, 1113 | "dist": { 1114 | "type": "zip", 1115 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1116 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 1117 | "shasum": "" 1118 | }, 1119 | "require": { 1120 | "php": ">=5.3.3" 1121 | }, 1122 | "type": "library", 1123 | "autoload": { 1124 | "classmap": [ 1125 | "src/" 1126 | ] 1127 | }, 1128 | "notification-url": "https://packagist.org/downloads/", 1129 | "license": [ 1130 | "BSD-3-Clause" 1131 | ], 1132 | "authors": [ 1133 | { 1134 | "name": "Sebastian Bergmann", 1135 | "email": "sebastian@phpunit.de", 1136 | "role": "lead" 1137 | } 1138 | ], 1139 | "description": "Simple template engine.", 1140 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 1141 | "keywords": [ 1142 | "template" 1143 | ], 1144 | "time": "2015-06-21T13:50:34+00:00" 1145 | }, 1146 | { 1147 | "name": "phpunit/php-timer", 1148 | "version": "1.0.9", 1149 | "source": { 1150 | "type": "git", 1151 | "url": "https://github.com/sebastianbergmann/php-timer.git", 1152 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" 1153 | }, 1154 | "dist": { 1155 | "type": "zip", 1156 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 1157 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 1158 | "shasum": "" 1159 | }, 1160 | "require": { 1161 | "php": "^5.3.3 || ^7.0" 1162 | }, 1163 | "require-dev": { 1164 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 1165 | }, 1166 | "type": "library", 1167 | "extra": { 1168 | "branch-alias": { 1169 | "dev-master": "1.0-dev" 1170 | } 1171 | }, 1172 | "autoload": { 1173 | "classmap": [ 1174 | "src/" 1175 | ] 1176 | }, 1177 | "notification-url": "https://packagist.org/downloads/", 1178 | "license": [ 1179 | "BSD-3-Clause" 1180 | ], 1181 | "authors": [ 1182 | { 1183 | "name": "Sebastian Bergmann", 1184 | "email": "sb@sebastian-bergmann.de", 1185 | "role": "lead" 1186 | } 1187 | ], 1188 | "description": "Utility class for timing", 1189 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 1190 | "keywords": [ 1191 | "timer" 1192 | ], 1193 | "time": "2017-02-26T11:10:40+00:00" 1194 | }, 1195 | { 1196 | "name": "phpunit/php-token-stream", 1197 | "version": "2.0.2", 1198 | "source": { 1199 | "type": "git", 1200 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 1201 | "reference": "791198a2c6254db10131eecfe8c06670700904db" 1202 | }, 1203 | "dist": { 1204 | "type": "zip", 1205 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", 1206 | "reference": "791198a2c6254db10131eecfe8c06670700904db", 1207 | "shasum": "" 1208 | }, 1209 | "require": { 1210 | "ext-tokenizer": "*", 1211 | "php": "^7.0" 1212 | }, 1213 | "require-dev": { 1214 | "phpunit/phpunit": "^6.2.4" 1215 | }, 1216 | "type": "library", 1217 | "extra": { 1218 | "branch-alias": { 1219 | "dev-master": "2.0-dev" 1220 | } 1221 | }, 1222 | "autoload": { 1223 | "classmap": [ 1224 | "src/" 1225 | ] 1226 | }, 1227 | "notification-url": "https://packagist.org/downloads/", 1228 | "license": [ 1229 | "BSD-3-Clause" 1230 | ], 1231 | "authors": [ 1232 | { 1233 | "name": "Sebastian Bergmann", 1234 | "email": "sebastian@phpunit.de" 1235 | } 1236 | ], 1237 | "description": "Wrapper around PHP's tokenizer extension.", 1238 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 1239 | "keywords": [ 1240 | "tokenizer" 1241 | ], 1242 | "time": "2017-11-27T05:48:46+00:00" 1243 | }, 1244 | { 1245 | "name": "phpunit/phpunit", 1246 | "version": "6.5.13", 1247 | "source": { 1248 | "type": "git", 1249 | "url": "https://github.com/sebastianbergmann/phpunit.git", 1250 | "reference": "0973426fb012359b2f18d3bd1e90ef1172839693" 1251 | }, 1252 | "dist": { 1253 | "type": "zip", 1254 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0973426fb012359b2f18d3bd1e90ef1172839693", 1255 | "reference": "0973426fb012359b2f18d3bd1e90ef1172839693", 1256 | "shasum": "" 1257 | }, 1258 | "require": { 1259 | "ext-dom": "*", 1260 | "ext-json": "*", 1261 | "ext-libxml": "*", 1262 | "ext-mbstring": "*", 1263 | "ext-xml": "*", 1264 | "myclabs/deep-copy": "^1.6.1", 1265 | "phar-io/manifest": "^1.0.1", 1266 | "phar-io/version": "^1.0", 1267 | "php": "^7.0", 1268 | "phpspec/prophecy": "^1.7", 1269 | "phpunit/php-code-coverage": "^5.3", 1270 | "phpunit/php-file-iterator": "^1.4.3", 1271 | "phpunit/php-text-template": "^1.2.1", 1272 | "phpunit/php-timer": "^1.0.9", 1273 | "phpunit/phpunit-mock-objects": "^5.0.9", 1274 | "sebastian/comparator": "^2.1", 1275 | "sebastian/diff": "^2.0", 1276 | "sebastian/environment": "^3.1", 1277 | "sebastian/exporter": "^3.1", 1278 | "sebastian/global-state": "^2.0", 1279 | "sebastian/object-enumerator": "^3.0.3", 1280 | "sebastian/resource-operations": "^1.0", 1281 | "sebastian/version": "^2.0.1" 1282 | }, 1283 | "conflict": { 1284 | "phpdocumentor/reflection-docblock": "3.0.2", 1285 | "phpunit/dbunit": "<3.0" 1286 | }, 1287 | "require-dev": { 1288 | "ext-pdo": "*" 1289 | }, 1290 | "suggest": { 1291 | "ext-xdebug": "*", 1292 | "phpunit/php-invoker": "^1.1" 1293 | }, 1294 | "bin": [ 1295 | "phpunit" 1296 | ], 1297 | "type": "library", 1298 | "extra": { 1299 | "branch-alias": { 1300 | "dev-master": "6.5.x-dev" 1301 | } 1302 | }, 1303 | "autoload": { 1304 | "classmap": [ 1305 | "src/" 1306 | ] 1307 | }, 1308 | "notification-url": "https://packagist.org/downloads/", 1309 | "license": [ 1310 | "BSD-3-Clause" 1311 | ], 1312 | "authors": [ 1313 | { 1314 | "name": "Sebastian Bergmann", 1315 | "email": "sebastian@phpunit.de", 1316 | "role": "lead" 1317 | } 1318 | ], 1319 | "description": "The PHP Unit Testing framework.", 1320 | "homepage": "https://phpunit.de/", 1321 | "keywords": [ 1322 | "phpunit", 1323 | "testing", 1324 | "xunit" 1325 | ], 1326 | "time": "2018-09-08T15:10:43+00:00" 1327 | }, 1328 | { 1329 | "name": "phpunit/phpunit-mock-objects", 1330 | "version": "5.0.10", 1331 | "source": { 1332 | "type": "git", 1333 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 1334 | "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" 1335 | }, 1336 | "dist": { 1337 | "type": "zip", 1338 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", 1339 | "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", 1340 | "shasum": "" 1341 | }, 1342 | "require": { 1343 | "doctrine/instantiator": "^1.0.5", 1344 | "php": "^7.0", 1345 | "phpunit/php-text-template": "^1.2.1", 1346 | "sebastian/exporter": "^3.1" 1347 | }, 1348 | "conflict": { 1349 | "phpunit/phpunit": "<6.0" 1350 | }, 1351 | "require-dev": { 1352 | "phpunit/phpunit": "^6.5.11" 1353 | }, 1354 | "suggest": { 1355 | "ext-soap": "*" 1356 | }, 1357 | "type": "library", 1358 | "extra": { 1359 | "branch-alias": { 1360 | "dev-master": "5.0.x-dev" 1361 | } 1362 | }, 1363 | "autoload": { 1364 | "classmap": [ 1365 | "src/" 1366 | ] 1367 | }, 1368 | "notification-url": "https://packagist.org/downloads/", 1369 | "license": [ 1370 | "BSD-3-Clause" 1371 | ], 1372 | "authors": [ 1373 | { 1374 | "name": "Sebastian Bergmann", 1375 | "email": "sebastian@phpunit.de", 1376 | "role": "lead" 1377 | } 1378 | ], 1379 | "description": "Mock Object library for PHPUnit", 1380 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 1381 | "keywords": [ 1382 | "mock", 1383 | "xunit" 1384 | ], 1385 | "time": "2018-08-09T05:50:03+00:00" 1386 | }, 1387 | { 1388 | "name": "sebastian/code-unit-reverse-lookup", 1389 | "version": "1.0.1", 1390 | "source": { 1391 | "type": "git", 1392 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1393 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 1394 | }, 1395 | "dist": { 1396 | "type": "zip", 1397 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1398 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 1399 | "shasum": "" 1400 | }, 1401 | "require": { 1402 | "php": "^5.6 || ^7.0" 1403 | }, 1404 | "require-dev": { 1405 | "phpunit/phpunit": "^5.7 || ^6.0" 1406 | }, 1407 | "type": "library", 1408 | "extra": { 1409 | "branch-alias": { 1410 | "dev-master": "1.0.x-dev" 1411 | } 1412 | }, 1413 | "autoload": { 1414 | "classmap": [ 1415 | "src/" 1416 | ] 1417 | }, 1418 | "notification-url": "https://packagist.org/downloads/", 1419 | "license": [ 1420 | "BSD-3-Clause" 1421 | ], 1422 | "authors": [ 1423 | { 1424 | "name": "Sebastian Bergmann", 1425 | "email": "sebastian@phpunit.de" 1426 | } 1427 | ], 1428 | "description": "Looks up which function or method a line of code belongs to", 1429 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1430 | "time": "2017-03-04T06:30:41+00:00" 1431 | }, 1432 | { 1433 | "name": "sebastian/comparator", 1434 | "version": "2.1.3", 1435 | "source": { 1436 | "type": "git", 1437 | "url": "https://github.com/sebastianbergmann/comparator.git", 1438 | "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" 1439 | }, 1440 | "dist": { 1441 | "type": "zip", 1442 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", 1443 | "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", 1444 | "shasum": "" 1445 | }, 1446 | "require": { 1447 | "php": "^7.0", 1448 | "sebastian/diff": "^2.0 || ^3.0", 1449 | "sebastian/exporter": "^3.1" 1450 | }, 1451 | "require-dev": { 1452 | "phpunit/phpunit": "^6.4" 1453 | }, 1454 | "type": "library", 1455 | "extra": { 1456 | "branch-alias": { 1457 | "dev-master": "2.1.x-dev" 1458 | } 1459 | }, 1460 | "autoload": { 1461 | "classmap": [ 1462 | "src/" 1463 | ] 1464 | }, 1465 | "notification-url": "https://packagist.org/downloads/", 1466 | "license": [ 1467 | "BSD-3-Clause" 1468 | ], 1469 | "authors": [ 1470 | { 1471 | "name": "Jeff Welch", 1472 | "email": "whatthejeff@gmail.com" 1473 | }, 1474 | { 1475 | "name": "Volker Dusch", 1476 | "email": "github@wallbash.com" 1477 | }, 1478 | { 1479 | "name": "Bernhard Schussek", 1480 | "email": "bschussek@2bepublished.at" 1481 | }, 1482 | { 1483 | "name": "Sebastian Bergmann", 1484 | "email": "sebastian@phpunit.de" 1485 | } 1486 | ], 1487 | "description": "Provides the functionality to compare PHP values for equality", 1488 | "homepage": "https://github.com/sebastianbergmann/comparator", 1489 | "keywords": [ 1490 | "comparator", 1491 | "compare", 1492 | "equality" 1493 | ], 1494 | "time": "2018-02-01T13:46:46+00:00" 1495 | }, 1496 | { 1497 | "name": "sebastian/diff", 1498 | "version": "2.0.1", 1499 | "source": { 1500 | "type": "git", 1501 | "url": "https://github.com/sebastianbergmann/diff.git", 1502 | "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" 1503 | }, 1504 | "dist": { 1505 | "type": "zip", 1506 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", 1507 | "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", 1508 | "shasum": "" 1509 | }, 1510 | "require": { 1511 | "php": "^7.0" 1512 | }, 1513 | "require-dev": { 1514 | "phpunit/phpunit": "^6.2" 1515 | }, 1516 | "type": "library", 1517 | "extra": { 1518 | "branch-alias": { 1519 | "dev-master": "2.0-dev" 1520 | } 1521 | }, 1522 | "autoload": { 1523 | "classmap": [ 1524 | "src/" 1525 | ] 1526 | }, 1527 | "notification-url": "https://packagist.org/downloads/", 1528 | "license": [ 1529 | "BSD-3-Clause" 1530 | ], 1531 | "authors": [ 1532 | { 1533 | "name": "Kore Nordmann", 1534 | "email": "mail@kore-nordmann.de" 1535 | }, 1536 | { 1537 | "name": "Sebastian Bergmann", 1538 | "email": "sebastian@phpunit.de" 1539 | } 1540 | ], 1541 | "description": "Diff implementation", 1542 | "homepage": "https://github.com/sebastianbergmann/diff", 1543 | "keywords": [ 1544 | "diff" 1545 | ], 1546 | "time": "2017-08-03T08:09:46+00:00" 1547 | }, 1548 | { 1549 | "name": "sebastian/environment", 1550 | "version": "3.1.0", 1551 | "source": { 1552 | "type": "git", 1553 | "url": "https://github.com/sebastianbergmann/environment.git", 1554 | "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" 1555 | }, 1556 | "dist": { 1557 | "type": "zip", 1558 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", 1559 | "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", 1560 | "shasum": "" 1561 | }, 1562 | "require": { 1563 | "php": "^7.0" 1564 | }, 1565 | "require-dev": { 1566 | "phpunit/phpunit": "^6.1" 1567 | }, 1568 | "type": "library", 1569 | "extra": { 1570 | "branch-alias": { 1571 | "dev-master": "3.1.x-dev" 1572 | } 1573 | }, 1574 | "autoload": { 1575 | "classmap": [ 1576 | "src/" 1577 | ] 1578 | }, 1579 | "notification-url": "https://packagist.org/downloads/", 1580 | "license": [ 1581 | "BSD-3-Clause" 1582 | ], 1583 | "authors": [ 1584 | { 1585 | "name": "Sebastian Bergmann", 1586 | "email": "sebastian@phpunit.de" 1587 | } 1588 | ], 1589 | "description": "Provides functionality to handle HHVM/PHP environments", 1590 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1591 | "keywords": [ 1592 | "Xdebug", 1593 | "environment", 1594 | "hhvm" 1595 | ], 1596 | "time": "2017-07-01T08:51:00+00:00" 1597 | }, 1598 | { 1599 | "name": "sebastian/exporter", 1600 | "version": "3.1.0", 1601 | "source": { 1602 | "type": "git", 1603 | "url": "https://github.com/sebastianbergmann/exporter.git", 1604 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" 1605 | }, 1606 | "dist": { 1607 | "type": "zip", 1608 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", 1609 | "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", 1610 | "shasum": "" 1611 | }, 1612 | "require": { 1613 | "php": "^7.0", 1614 | "sebastian/recursion-context": "^3.0" 1615 | }, 1616 | "require-dev": { 1617 | "ext-mbstring": "*", 1618 | "phpunit/phpunit": "^6.0" 1619 | }, 1620 | "type": "library", 1621 | "extra": { 1622 | "branch-alias": { 1623 | "dev-master": "3.1.x-dev" 1624 | } 1625 | }, 1626 | "autoload": { 1627 | "classmap": [ 1628 | "src/" 1629 | ] 1630 | }, 1631 | "notification-url": "https://packagist.org/downloads/", 1632 | "license": [ 1633 | "BSD-3-Clause" 1634 | ], 1635 | "authors": [ 1636 | { 1637 | "name": "Jeff Welch", 1638 | "email": "whatthejeff@gmail.com" 1639 | }, 1640 | { 1641 | "name": "Volker Dusch", 1642 | "email": "github@wallbash.com" 1643 | }, 1644 | { 1645 | "name": "Bernhard Schussek", 1646 | "email": "bschussek@2bepublished.at" 1647 | }, 1648 | { 1649 | "name": "Sebastian Bergmann", 1650 | "email": "sebastian@phpunit.de" 1651 | }, 1652 | { 1653 | "name": "Adam Harvey", 1654 | "email": "aharvey@php.net" 1655 | } 1656 | ], 1657 | "description": "Provides the functionality to export PHP variables for visualization", 1658 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1659 | "keywords": [ 1660 | "export", 1661 | "exporter" 1662 | ], 1663 | "time": "2017-04-03T13:19:02+00:00" 1664 | }, 1665 | { 1666 | "name": "sebastian/global-state", 1667 | "version": "2.0.0", 1668 | "source": { 1669 | "type": "git", 1670 | "url": "https://github.com/sebastianbergmann/global-state.git", 1671 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" 1672 | }, 1673 | "dist": { 1674 | "type": "zip", 1675 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1676 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1677 | "shasum": "" 1678 | }, 1679 | "require": { 1680 | "php": "^7.0" 1681 | }, 1682 | "require-dev": { 1683 | "phpunit/phpunit": "^6.0" 1684 | }, 1685 | "suggest": { 1686 | "ext-uopz": "*" 1687 | }, 1688 | "type": "library", 1689 | "extra": { 1690 | "branch-alias": { 1691 | "dev-master": "2.0-dev" 1692 | } 1693 | }, 1694 | "autoload": { 1695 | "classmap": [ 1696 | "src/" 1697 | ] 1698 | }, 1699 | "notification-url": "https://packagist.org/downloads/", 1700 | "license": [ 1701 | "BSD-3-Clause" 1702 | ], 1703 | "authors": [ 1704 | { 1705 | "name": "Sebastian Bergmann", 1706 | "email": "sebastian@phpunit.de" 1707 | } 1708 | ], 1709 | "description": "Snapshotting of global state", 1710 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1711 | "keywords": [ 1712 | "global state" 1713 | ], 1714 | "time": "2017-04-27T15:39:26+00:00" 1715 | }, 1716 | { 1717 | "name": "sebastian/object-enumerator", 1718 | "version": "3.0.3", 1719 | "source": { 1720 | "type": "git", 1721 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1722 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" 1723 | }, 1724 | "dist": { 1725 | "type": "zip", 1726 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1727 | "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", 1728 | "shasum": "" 1729 | }, 1730 | "require": { 1731 | "php": "^7.0", 1732 | "sebastian/object-reflector": "^1.1.1", 1733 | "sebastian/recursion-context": "^3.0" 1734 | }, 1735 | "require-dev": { 1736 | "phpunit/phpunit": "^6.0" 1737 | }, 1738 | "type": "library", 1739 | "extra": { 1740 | "branch-alias": { 1741 | "dev-master": "3.0.x-dev" 1742 | } 1743 | }, 1744 | "autoload": { 1745 | "classmap": [ 1746 | "src/" 1747 | ] 1748 | }, 1749 | "notification-url": "https://packagist.org/downloads/", 1750 | "license": [ 1751 | "BSD-3-Clause" 1752 | ], 1753 | "authors": [ 1754 | { 1755 | "name": "Sebastian Bergmann", 1756 | "email": "sebastian@phpunit.de" 1757 | } 1758 | ], 1759 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1760 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1761 | "time": "2017-08-03T12:35:26+00:00" 1762 | }, 1763 | { 1764 | "name": "sebastian/object-reflector", 1765 | "version": "1.1.1", 1766 | "source": { 1767 | "type": "git", 1768 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1769 | "reference": "773f97c67f28de00d397be301821b06708fca0be" 1770 | }, 1771 | "dist": { 1772 | "type": "zip", 1773 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", 1774 | "reference": "773f97c67f28de00d397be301821b06708fca0be", 1775 | "shasum": "" 1776 | }, 1777 | "require": { 1778 | "php": "^7.0" 1779 | }, 1780 | "require-dev": { 1781 | "phpunit/phpunit": "^6.0" 1782 | }, 1783 | "type": "library", 1784 | "extra": { 1785 | "branch-alias": { 1786 | "dev-master": "1.1-dev" 1787 | } 1788 | }, 1789 | "autoload": { 1790 | "classmap": [ 1791 | "src/" 1792 | ] 1793 | }, 1794 | "notification-url": "https://packagist.org/downloads/", 1795 | "license": [ 1796 | "BSD-3-Clause" 1797 | ], 1798 | "authors": [ 1799 | { 1800 | "name": "Sebastian Bergmann", 1801 | "email": "sebastian@phpunit.de" 1802 | } 1803 | ], 1804 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1805 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1806 | "time": "2017-03-29T09:07:27+00:00" 1807 | }, 1808 | { 1809 | "name": "sebastian/recursion-context", 1810 | "version": "3.0.0", 1811 | "source": { 1812 | "type": "git", 1813 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1814 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" 1815 | }, 1816 | "dist": { 1817 | "type": "zip", 1818 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1819 | "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", 1820 | "shasum": "" 1821 | }, 1822 | "require": { 1823 | "php": "^7.0" 1824 | }, 1825 | "require-dev": { 1826 | "phpunit/phpunit": "^6.0" 1827 | }, 1828 | "type": "library", 1829 | "extra": { 1830 | "branch-alias": { 1831 | "dev-master": "3.0.x-dev" 1832 | } 1833 | }, 1834 | "autoload": { 1835 | "classmap": [ 1836 | "src/" 1837 | ] 1838 | }, 1839 | "notification-url": "https://packagist.org/downloads/", 1840 | "license": [ 1841 | "BSD-3-Clause" 1842 | ], 1843 | "authors": [ 1844 | { 1845 | "name": "Jeff Welch", 1846 | "email": "whatthejeff@gmail.com" 1847 | }, 1848 | { 1849 | "name": "Sebastian Bergmann", 1850 | "email": "sebastian@phpunit.de" 1851 | }, 1852 | { 1853 | "name": "Adam Harvey", 1854 | "email": "aharvey@php.net" 1855 | } 1856 | ], 1857 | "description": "Provides functionality to recursively process PHP variables", 1858 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1859 | "time": "2017-03-03T06:23:57+00:00" 1860 | }, 1861 | { 1862 | "name": "sebastian/resource-operations", 1863 | "version": "1.0.0", 1864 | "source": { 1865 | "type": "git", 1866 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1867 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" 1868 | }, 1869 | "dist": { 1870 | "type": "zip", 1871 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1872 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1873 | "shasum": "" 1874 | }, 1875 | "require": { 1876 | "php": ">=5.6.0" 1877 | }, 1878 | "type": "library", 1879 | "extra": { 1880 | "branch-alias": { 1881 | "dev-master": "1.0.x-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": "Provides a list of PHP built-in functions that operate on resources", 1900 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1901 | "time": "2015-07-28T20:34:47+00:00" 1902 | }, 1903 | { 1904 | "name": "sebastian/version", 1905 | "version": "2.0.1", 1906 | "source": { 1907 | "type": "git", 1908 | "url": "https://github.com/sebastianbergmann/version.git", 1909 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1910 | }, 1911 | "dist": { 1912 | "type": "zip", 1913 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1914 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1915 | "shasum": "" 1916 | }, 1917 | "require": { 1918 | "php": ">=5.6" 1919 | }, 1920 | "type": "library", 1921 | "extra": { 1922 | "branch-alias": { 1923 | "dev-master": "2.0.x-dev" 1924 | } 1925 | }, 1926 | "autoload": { 1927 | "classmap": [ 1928 | "src/" 1929 | ] 1930 | }, 1931 | "notification-url": "https://packagist.org/downloads/", 1932 | "license": [ 1933 | "BSD-3-Clause" 1934 | ], 1935 | "authors": [ 1936 | { 1937 | "name": "Sebastian Bergmann", 1938 | "email": "sebastian@phpunit.de", 1939 | "role": "lead" 1940 | } 1941 | ], 1942 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1943 | "homepage": "https://github.com/sebastianbergmann/version", 1944 | "time": "2016-10-03T07:35:21+00:00" 1945 | }, 1946 | { 1947 | "name": "theseer/tokenizer", 1948 | "version": "1.1.0", 1949 | "source": { 1950 | "type": "git", 1951 | "url": "https://github.com/theseer/tokenizer.git", 1952 | "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" 1953 | }, 1954 | "dist": { 1955 | "type": "zip", 1956 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", 1957 | "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", 1958 | "shasum": "" 1959 | }, 1960 | "require": { 1961 | "ext-dom": "*", 1962 | "ext-tokenizer": "*", 1963 | "ext-xmlwriter": "*", 1964 | "php": "^7.0" 1965 | }, 1966 | "type": "library", 1967 | "autoload": { 1968 | "classmap": [ 1969 | "src/" 1970 | ] 1971 | }, 1972 | "notification-url": "https://packagist.org/downloads/", 1973 | "license": [ 1974 | "BSD-3-Clause" 1975 | ], 1976 | "authors": [ 1977 | { 1978 | "name": "Arne Blankerts", 1979 | "email": "arne@blankerts.de", 1980 | "role": "Developer" 1981 | } 1982 | ], 1983 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1984 | "time": "2017-04-07T12:08:54+00:00" 1985 | }, 1986 | { 1987 | "name": "webmozart/assert", 1988 | "version": "1.3.0", 1989 | "source": { 1990 | "type": "git", 1991 | "url": "https://github.com/webmozart/assert.git", 1992 | "reference": "0df1908962e7a3071564e857d86874dad1ef204a" 1993 | }, 1994 | "dist": { 1995 | "type": "zip", 1996 | "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", 1997 | "reference": "0df1908962e7a3071564e857d86874dad1ef204a", 1998 | "shasum": "" 1999 | }, 2000 | "require": { 2001 | "php": "^5.3.3 || ^7.0" 2002 | }, 2003 | "require-dev": { 2004 | "phpunit/phpunit": "^4.6", 2005 | "sebastian/version": "^1.0.1" 2006 | }, 2007 | "type": "library", 2008 | "extra": { 2009 | "branch-alias": { 2010 | "dev-master": "1.3-dev" 2011 | } 2012 | }, 2013 | "autoload": { 2014 | "psr-4": { 2015 | "Webmozart\\Assert\\": "src/" 2016 | } 2017 | }, 2018 | "notification-url": "https://packagist.org/downloads/", 2019 | "license": [ 2020 | "MIT" 2021 | ], 2022 | "authors": [ 2023 | { 2024 | "name": "Bernhard Schussek", 2025 | "email": "bschussek@gmail.com" 2026 | } 2027 | ], 2028 | "description": "Assertions to validate method input/output with nice error messages.", 2029 | "keywords": [ 2030 | "assert", 2031 | "check", 2032 | "validate" 2033 | ], 2034 | "time": "2018-01-29T19:49:41+00:00" 2035 | } 2036 | ], 2037 | "aliases": [], 2038 | "minimum-stability": "stable", 2039 | "stability-flags": [], 2040 | "prefer-stable": false, 2041 | "prefer-lowest": false, 2042 | "platform": [], 2043 | "platform-dev": [] 2044 | } 2045 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | tests/ 9 | 10 | 11 | 12 | 13 | src/ 14 | 15 | 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Pure PHP RTSP 1.0 server 2 | This is a library that allow you to build a [RTSP/1.0](https://www.ietf.org/rfc/rfc2326.txt) server. 3 | 4 | # Examples 5 | 6 | ## Bare minimum server 7 | 8 | This is a very minimal server that will 9 | * dump requests 10 | * reply with 200 OK (which is not very useful :-)): 11 | 12 | ```php 13 | require __DIR__ . '/vendor/autoload.php'; 14 | 15 | $loop = React\EventLoop\Factory::create(); 16 | 17 | //Normal port is 554 but we use 5540 instead to avoid root required 18 | $socket = new \React\Socket\TcpServer('tcp://0.0.0.0:5540', $loop); 19 | 20 | $server = new \PhpBg\Rtsp\Server(function (\PhpBg\Rtsp\Message\Request $request, \React\Socket\ConnectionInterface $connection) { 21 | echo $request; 22 | $response = \PhpBg\Rtsp\Message\MessageFactory::response(); 23 | $response->setHeader('cseq', $request->getHeader('cseq')); 24 | return $response; 25 | }); 26 | 27 | $server->on('error', function (\Exception $e) { 28 | echo $e->getMessage() . "\r\n"; 29 | echo $e->getTraceAsString() . "\r\n"; 30 | }); 31 | 32 | $server->listen($socket); 33 | echo "Server started\r\n"; 34 | echo "Open any decent video player (e.g. vlc, mpv) and open rtsp://localhost:5540\r\n"; 35 | $loop->run(); 36 | ``` 37 | 38 | ## Server with middleware stack 39 | This is almost the same example as the previous one, but with a more flexible and trendy middleware stack. 40 | 41 | `phpbg/rtsp` comes with some handy middlewares that will help you build a rfc compliant application: 42 | * `AutoCseq`: automatically add cseq header to your responses 43 | * `AutoContentLength`: automatically add content-length to your responses 44 | * `Log`: Automatically log request and responses 45 | 46 | ```php 47 | require __DIR__ . '/../vendor/autoload.php'; 48 | 49 | $loop = React\EventLoop\Factory::create(); 50 | 51 | //Normal port is 554 but we use 5540 instead to avoid root required 52 | $socket = new \React\Socket\TcpServer('tcp://0.0.0.0:5540', $loop); 53 | 54 | $middlewares = [ 55 | new \PhpBg\Rtsp\Middleware\Log(), 56 | new \PhpBg\Rtsp\Middleware\AutoCseq(), 57 | new \PhpBg\Rtsp\Middleware\AutoContentLength(), 58 | function (\PhpBg\Rtsp\Message\Request $request, \React\Socket\ConnectionInterface $connection) { 59 | // 200 OK response middleware 60 | return \PhpBg\Rtsp\Message\MessageFactory::response(); 61 | } 62 | ]; 63 | 64 | $server = new \PhpBg\Rtsp\Server(new \PhpBg\Rtsp\Middleware\MiddlewareStack($middlewares)); 65 | 66 | $server->on('error', function (\Exception $e) { 67 | echo $e->getMessage() . "\r\n"; 68 | echo $e->getTraceAsString() . "\r\n"; 69 | }); 70 | 71 | $server->listen($socket); 72 | echo "Server started\r\n"; 73 | echo "Open any decent video player (e.g. vlc, mpv) and open rtsp://localhost:5540\r\n"; 74 | $loop->run(); 75 | ``` 76 | 77 | # Install 78 | 79 | ``` 80 | composer require phpbg/rtsp 81 | ``` 82 | 83 | # Contribute 84 | ## Tests 85 | To run unit tests launch: 86 | 87 | php vendor/phpunit/phpunit/phpunit --coverage-text -c phpunit.xml 88 | 89 | NB: launching with code coverage increase greatly the time required for tests to run, especially memory leak searching tests 90 | -------------------------------------------------------------------------------- /src/Message/AbstractMessage.php: -------------------------------------------------------------------------------- 1 | headers[strtolower($name)]); 44 | } 45 | 46 | /** 47 | * Return header $name (case insensitive) 48 | * @param string $name 49 | * @return mixed|null Null is returned if header is not set 50 | */ 51 | public function getHeader(string $name) 52 | { 53 | return $this->headers[strtolower($name)] ?? null; 54 | } 55 | 56 | public function setHeader(string $name, $value) 57 | { 58 | $this->headers[strtolower($name)] = $value; 59 | } 60 | 61 | public function __toString() 62 | { 63 | $str = "{$this->getFirstLine()}\r\n"; 64 | foreach ($this->headers as $key => $value) { 65 | $str .= "\t\t{$key}: {$value}\r\n"; 66 | } 67 | $str .= "\r\n"; 68 | if ($this->body !== null) { 69 | $str .= $this->body; 70 | $str .= "\r\n"; 71 | } 72 | 73 | return $str; 74 | } 75 | 76 | /** 77 | * Generate string message representation to be used on connection transport 78 | * @return string 79 | */ 80 | public function toTransport(): string 81 | { 82 | $str = "{$this->getFirstLine()}\r\n"; 83 | foreach ($this->headers as $key => $value) { 84 | $str .= "{$key}: {$value}\r\n"; 85 | } 86 | $str .= "\r\n"; 87 | if ($this->body !== null) { 88 | $str .= $this->body; 89 | } 90 | return $str; 91 | } 92 | } -------------------------------------------------------------------------------- /src/Message/Enum/RequestMethod.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 47 | $response->headers = $headers; 48 | $response->body = $body; 49 | $response->reasonPhrase = $reasonPhrase; 50 | $response->rtspVersion = $rtspVersion; 51 | return $response; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Message/Request.php: -------------------------------------------------------------------------------- 1 | method $this->uri $this->rtspVersion"; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Message/RequestParser.php: -------------------------------------------------------------------------------- 1 | reset(); 58 | } 59 | 60 | /** 61 | * Handle incoming data from connection 62 | * @param string $data 63 | */ 64 | public function feed(string $data) 65 | { 66 | try { 67 | if ($this->request === null) { 68 | $this->feedHeaders($data); 69 | } else { 70 | $this->feedBody($data); 71 | } 72 | } catch (MessageException $e) { 73 | $this->emit('error', [$e]); 74 | } 75 | } 76 | 77 | protected function reset() 78 | { 79 | $this->buffer = ''; 80 | $this->request = null; 81 | } 82 | 83 | /** 84 | * @param string $data 85 | * @throws MessageException 86 | */ 87 | protected function feedHeaders(string $data) 88 | { 89 | $this->buffer .= $data; 90 | $endOfHeaders = strpos($this->buffer, "\r\n\r\n"); 91 | if ($endOfHeaders !== false) { 92 | $headersString = substr($this->buffer, 0, $endOfHeaders); 93 | $this->buffer = substr($this->buffer, $endOfHeaders + 4); 94 | if ($this->buffer === false) { 95 | $this->buffer = ''; 96 | } 97 | $this->request = $this->getRequestFromHeaders($headersString); 98 | $this->feedBody(); 99 | } 100 | } 101 | 102 | /** 103 | * @param string $headersString 104 | * @return Request 105 | * @throws MessageException 106 | */ 107 | protected function getRequestFromHeaders(string $headersString): Request 108 | { 109 | $messageExploded = explode("\r\n", $headersString); 110 | if (empty($messageExploded)) { 111 | throw new MessageException('Dropping empty/invalid message'); 112 | } 113 | 114 | $messageExploded = array_map('trim', $messageExploded); 115 | $firstLine = array_shift($messageExploded); 116 | $firstLineExploded = explode(' ', $firstLine); 117 | if (count($firstLineExploded) !== 3) { 118 | throw new MessageException('Dropping invalid first line'); 119 | } 120 | $method = $firstLineExploded[0]; 121 | $version = $firstLineExploded[2]; 122 | if (!RequestMethod::isValid($method)) { 123 | throw new MessageException('Dropping invalid method'); 124 | } 125 | if (!RtspVersion::isValid($version)) { 126 | throw new MessageException('Dropping unsupported RTSP version'); 127 | } 128 | 129 | $request = new Request(); 130 | $request->method = $method; 131 | $request->uri = $firstLineExploded[1]; 132 | $request->rtspVersion = $version; 133 | 134 | foreach ($messageExploded as $headerLine) { 135 | $headerLine = trim($headerLine); 136 | if (empty($headerLine)) { 137 | // End of headers 138 | break; 139 | } 140 | $pos = strpos($headerLine, ':'); 141 | if ($pos === false || $pos === 0) { 142 | throw new MessageException('Dropping invalid message header line'); 143 | } 144 | 145 | //Keys are case insensitive 146 | $key = strtolower(trim(substr($headerLine, 0, $pos))); 147 | $value = trim(substr($headerLine, $pos + 1)); 148 | $request->headers[$key] = $value; 149 | } 150 | 151 | if (!$request->hasHeader('cseq')) { 152 | throw new MessageException('Dropping message missing cseq'); 153 | } 154 | return $request; 155 | } 156 | 157 | protected function feedBody(string $data = null) 158 | { 159 | if ($data !== null) { 160 | $this->buffer .= $data; 161 | } 162 | if (!$this->request->hasHeader('Content-Length')) { 163 | $this->emit('request', [$this->request]); 164 | $remainingData = $this->buffer; 165 | $this->reset(); 166 | if (!empty($remainingData)) { 167 | $this->feed($remainingData); 168 | } 169 | return; 170 | } 171 | $length = (int)$this->request->getHeader('Content-Length'); 172 | // TODO security check : $length should not be too big... 173 | if (strlen($this->buffer) < $length) { 174 | //Wait for buffer to fill 175 | return; 176 | } 177 | if ($length > 0) { 178 | $body = substr($this->buffer, 0, $length); 179 | $this->request->body = $body; 180 | } 181 | $this->emit('request', [$this->request]); 182 | $remainingData = substr($this->buffer, $length); 183 | $this->reset(); 184 | if (!empty($remainingData)) { 185 | $this->feed($remainingData); 186 | } 187 | return; 188 | } 189 | } -------------------------------------------------------------------------------- /src/Message/Response.php: -------------------------------------------------------------------------------- 1 | rtspVersion $this->statusCode $this->reasonPhrase"; 38 | } 39 | 40 | public function __construct() 41 | { 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /src/Middleware/AutoContentLength.php: -------------------------------------------------------------------------------- 1 | then(function (Response $resolvedResponse) use ($request) { 61 | if (isset($resolvedResponse->body) && !$resolvedResponse->hasHeader('content-length')) { 62 | $resolvedResponse->setHeader('content-length', strlen($resolvedResponse->body)); 63 | } 64 | return $resolvedResponse; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Middleware/AutoCseq.php: -------------------------------------------------------------------------------- 1 | then(function (Response $resolvedResponse) use ($request) { 56 | if (!$resolvedResponse->hasHeader('cseq') && $request->hasHeader('cseq')) { 57 | $resolvedResponse->setHeader('cseq', $request->getHeader('cseq')); 58 | } 59 | return $resolvedResponse; 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Middleware/Log.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 57 | $this->level = $level; 58 | $this->errorLevel = $errorLevel; 59 | } 60 | 61 | /** 62 | * This middleware just looks like a standard middleware: invokable, receive request and return response 63 | * 64 | * @param Request $request 65 | * @param ConnectionInterface $connection 66 | * @param callable $next 67 | * @return Response|PromiseInterface 68 | */ 69 | public function __invoke(Request $request, ConnectionInterface $connection, callable $next) 70 | { 71 | $this->logger->log($this->level, "Request:\r\n$request"); 72 | $response = $next($request, $connection); 73 | if (!($response instanceof PromiseInterface)) { 74 | $response = new FulfilledPromise($response); 75 | } 76 | return $response->then(function (Response $resolvedResponse) use ($request) { 77 | $this->logger->log($this->level, "Response:\r\n$resolvedResponse"); 78 | return $resolvedResponse; 79 | }, function($reason) { 80 | if ($reason instanceof \Exception) { 81 | $this->logger->log($this->errorLevel, "Internal server error", ['exception' => $reason]); 82 | } else { 83 | $this->logger->log($this->errorLevel, "Unexpected internal server error", ['data' => $reason]); 84 | } 85 | return reject($reason); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Middleware/MiddlewareStack.php: -------------------------------------------------------------------------------- 1 | middlewares = array_values($middlewares); 53 | } 54 | 55 | /** 56 | * This middleware just looks like a standard middleware: invokable, receive request and return response 57 | * It has no $next handler because it is meant to be the last middleware in stack 58 | * 59 | * @param Request $request 60 | * @param ConnectionInterface $connection 61 | * @return Response|PromiseInterface 62 | */ 63 | public function __invoke(Request $request, ConnectionInterface $connection) 64 | { 65 | return $this->call($request, $connection, 0); 66 | } 67 | 68 | /** 69 | * Recursively executes middlewares 70 | * 71 | * @param Request $request 72 | * @param ConnectionInterface $connection 73 | * @param int $position 74 | * @return mixed 75 | */ 76 | protected function call(Request $request, ConnectionInterface $connection, int $position) 77 | { 78 | // final request handler will be invoked without a next handler 79 | if (!isset($this->middlewares[$position + 1])) { 80 | $handler = $this->middlewares[$position]; 81 | return $handler($request, $connection); 82 | } 83 | 84 | $next = function (Request $request, ConnectionInterface $connection) use ($position) { 85 | return $this->call($request, $connection, $position + 1); 86 | }; 87 | 88 | // invoke middleware request handler with next handler 89 | $handler = $this->middlewares[$position]; 90 | return $handler($request, $connection, $next); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | on('error', function (Exception $e) { 47 | * echo 'error: ' . $e->getMessage() . PHP_EOL; 48 | * }); 49 | * ``` 50 | * 51 | * Note that this is not a fatal error event, i.e. the server keeps listening for 52 | * new connections even after this event. 53 | * 54 | */ 55 | class Server extends EventEmitter 56 | { 57 | protected $callback; 58 | 59 | /** 60 | * Server constructor 61 | * 62 | * @param callable $callback 63 | * Callback that will receive (PhpBg\Rtsp\Message\Request, React\Socket\ConnectionInterface) and return 64 | * * a PhpBg\Rtsp\Message\Response 65 | * * a React\Promise\PromiseInterface that will resolve in a Response 66 | */ 67 | public function __construct(Callable $callback) 68 | { 69 | $this->callback = $callback; 70 | } 71 | 72 | /** 73 | * Starts listening for RTSP requests on the given socket server instance 74 | * 75 | * @param ServerInterface $socket 76 | */ 77 | public function listen(ServerInterface $socket) 78 | { 79 | $socket->on('connection', [$this, 'handleConnection']); 80 | } 81 | 82 | /** 83 | * Handle incoming connections 84 | * 85 | * @param ConnectionInterface $connection 86 | */ 87 | protected function handleConnection(ConnectionInterface $connection) 88 | { 89 | $requestParser = new RequestParser(); 90 | $requestParser->on('error', function (\Exception $exception) use ($connection) { 91 | $connection->close(); 92 | $this->emit('error', [$exception]); 93 | }); 94 | $requestParser->on('request', function (Request $request) use ($connection) { 95 | $this->handleRequest($request, $connection); 96 | }); 97 | 98 | $connection->on('data', [$requestParser, 'feed']); 99 | $connection->on('end', function () use ($requestParser) { 100 | $requestParser->removeAllListeners(); 101 | }); 102 | 103 | $connection->on('error', function (\Exception $exception) use ($requestParser) { 104 | $requestParser->removeAllListeners(); 105 | $this->emit('error', [$exception]); 106 | }); 107 | 108 | $connection->on('close', function () use ($requestParser) { 109 | $requestParser->removeAllListeners(); 110 | }); 111 | } 112 | 113 | /** 114 | * Handle incoming requests 115 | * 116 | * @param Request $request 117 | * @param ConnectionInterface $connection 118 | */ 119 | protected function handleRequest(Request $request, ConnectionInterface $connection) 120 | { 121 | $callback = $this->callback; 122 | try { 123 | $response = $callback($request, $connection); 124 | } catch (\Exception $e) { 125 | $connection->close(); 126 | $this->emit('error', [$e]); 127 | return; 128 | } 129 | // Convert all non-promise response to promises 130 | // There is little overhead for this, but this allows simpler code 131 | if (!($response instanceof PromiseInterface)) { 132 | $response = new FulfilledPromise($response); 133 | } 134 | 135 | // We should probably use done() instead of then(), but done() is only part of ExtendedPromiseInterface 136 | $response->then(function ($resolvedResponse) use ($connection) { 137 | if ($resolvedResponse instanceof Response) { 138 | $connection->write($resolvedResponse->toTransport()); 139 | return; 140 | } 141 | $message = 'The response callback is expected to resolve with an object implementing PhpBg\Rtsp\Message\Response, but resolved with "%s" instead.'; 142 | $message = sprintf($message, is_object($resolvedResponse) ? get_class($resolvedResponse) : gettype($resolvedResponse)); 143 | $this->emit('error', [new ServerException($message)]); 144 | }, function ($error) use ($connection) { 145 | $message = 'The response callback is expected to resolve with an object implementing PhpBg\Rtsp\Message\Response, but rejected with "%s" instead.'; 146 | $message = sprintf($message, is_object($error) ? get_class($error) : gettype($error)); 147 | $previous = null; 148 | if ($error instanceof \Throwable || $error instanceof \Exception) { 149 | $previous = $error; 150 | } 151 | $exception = new \RuntimeException($message, null, $previous); 152 | $this->emit('error', [$exception]); 153 | 154 | $rtspResponse = MessageFactory::response(500, [], null, 'internal server error'); 155 | $connection->write($rtspResponse->toTransport()); 156 | }); 157 | } 158 | 159 | } -------------------------------------------------------------------------------- /src/ServerException.php: -------------------------------------------------------------------------------- 1 | getOptionMessage(); 38 | $rp = new RequestParser(); 39 | $request = null; 40 | $rp->on('request', function ($newRequest) use (&$request) { 41 | $request = $newRequest; 42 | }); 43 | $rp->feed($optionMessage); 44 | 45 | $this->assertRequestMatchOption($request); 46 | } 47 | 48 | public function testFeedSingleMessageProgressive() 49 | { 50 | $optionMessage = $this->getOptionMessage(); 51 | $rp = new RequestParser(); 52 | $request = null; 53 | $rp->on('request', function ($newRequest) use (&$request) { 54 | $request = $newRequest; 55 | }); 56 | 57 | $optionMessageArray = str_split($optionMessage); 58 | foreach ($optionMessageArray as $char) { 59 | $rp->feed($char); 60 | } 61 | 62 | $this->assertRequestMatchOption($request); 63 | } 64 | 65 | public function testFeedTwoMessagesOneShot() 66 | { 67 | $rp = new RequestParser(); 68 | $requests = []; 69 | $rp->on('request', function ($request) use (&$requests) { 70 | $requests[] = $request; 71 | }); 72 | $optionMessage = $this->getOptionMessage(); 73 | $describeMessage = $this->getDescribeMessage(); 74 | $rp->feed($optionMessage . $describeMessage); 75 | 76 | $this->assertCount(2, $requests); 77 | $this->assertRequestMatchOption($requests[0]); 78 | $this->assertRequestMatchDescribe($requests[1]); 79 | 80 | } 81 | 82 | public function testFeedTwoMessagesProgressive() 83 | { 84 | $rp = new RequestParser(); 85 | $request = null; 86 | $rp->on('request', function ($newRequest) use (&$request) { 87 | $request = $newRequest; 88 | }); 89 | $optionMessage = $this->getOptionMessage(); 90 | $optionMessageArray = str_split($optionMessage); 91 | foreach ($optionMessageArray as $char) { 92 | $rp->feed($char); 93 | } 94 | $this->assertRequestMatchOption($request); 95 | 96 | $describeMessage = $this->getDescribeMessage(); 97 | $describeMessageArray = str_split($describeMessage); 98 | foreach ($describeMessageArray as $char) { 99 | $rp->feed($char); 100 | } 101 | $rp->feed($describeMessage); 102 | $this->assertRequestMatchDescribe($request); 103 | 104 | } 105 | 106 | public function testNoMemoryLeak() 107 | { 108 | $optionMessage = $this->getOptionMessage(); 109 | $rp = new RequestParser(); 110 | 111 | for ($i = 0; $i < 100000; $i++) { 112 | if ($i === 100) { 113 | //Warm a bit then take initial memory usage 114 | $initialMemoryUsage = memory_get_usage(false); 115 | } 116 | $rp->feed($optionMessage); 117 | } 118 | $finalMemoryUsage = memory_get_usage(false); 119 | $this->assertTrue($finalMemoryUsage <= 1.001 * $initialMemoryUsage, "Memory grown from $initialMemoryUsage to $finalMemoryUsage"); 120 | } 121 | 122 | private function assertRequestMatchOption($request) 123 | { 124 | $this->assertNotNull($request); 125 | $this->assertTrue($request instanceof Request); 126 | $this->assertEquals('OPTIONS rtsp://192.168.1.22:5540/257 RTSP/1.0', $request->getFirstLine()); 127 | $this->assertEquals('OPTIONS', $request->method); 128 | $this->assertEquals('rtsp://192.168.1.22:5540/257', $request->uri); 129 | $this->assertSame(null, $request->body); 130 | $this->assertTrue($request->hasHeader('cseq')); 131 | $this->assertEquals(1, $request->getHeader('cseq')); 132 | $this->assertTrue($request->hasHeader('user-agent')); 133 | $this->assertEquals('mpv 0.14.0', $request->getHeader('user-agent')); 134 | $this->assertTrue($request->hasHeader('user-AGENT')); 135 | $this->assertEquals('mpv 0.14.0', $request->getHeader('user-AGENT')); 136 | $this->assertFalse($request->hasHeader('foo')); 137 | $this->assertSame(null, $request->getHeader('foo')); 138 | } 139 | 140 | private function assertRequestMatchDescribe($request) 141 | { 142 | $this->assertNotNull($request); 143 | $this->assertTrue($request instanceof Request); 144 | $this->assertEquals('DESCRIBE rtsp://192.168.1.22:5540/257 RTSP/1.0', $request->getFirstLine()); 145 | $this->assertEquals('DESCRIBE', $request->method); 146 | $this->assertEquals('rtsp://192.168.1.22:5540/257', $request->uri); 147 | $this->assertSame(null, $request->body); 148 | $this->assertTrue($request->hasHeader('cseq')); 149 | $this->assertEquals(2, $request->getHeader('cseq')); 150 | $this->assertTrue($request->hasHeader('user-agent')); 151 | $this->assertEquals('mpv 0.14.0', $request->getHeader('user-agent')); 152 | $this->assertTrue($request->hasHeader('user-AGENT')); 153 | $this->assertEquals('mpv 0.14.0', $request->getHeader('user-AGENT')); 154 | $this->assertTrue($request->hasHeader('accept')); 155 | $this->assertEquals('application/sdp', $request->getHeader('accept')); 156 | $this->assertFalse($request->hasHeader('foo')); 157 | $this->assertSame(null, $request->getHeader('foo')); 158 | } 159 | 160 | private function getOptionMessage() 161 | { 162 | return "OPTIONS rtsp://192.168.1.22:5540/257 RTSP/1.0\r\ncseq: 1\r\nuser-agent: mpv 0.14.0\r\n\r\n"; 163 | } 164 | 165 | private function getDescribeMessage() 166 | { 167 | return "DESCRIBE rtsp://192.168.1.22:5540/257 RTSP/1.0\r\naccept: application/sdp\r\ncseq: 2\r\nuser-agent: mpv 0.14.0\r\n\r\n"; 168 | } 169 | } -------------------------------------------------------------------------------- /tests/Middleware/AutoContentLengthTest.php: -------------------------------------------------------------------------------- 1 | then(function (Response $response) use (&$extractedResponse) { 53 | // We expect a FullFilledPromise that resolve instantly, 54 | // so extracting response and making assertions outside of callback secure them 55 | $extractedResponse = $response; 56 | }); 57 | 58 | $this->assertNotNull($extractedResponse); 59 | /** 60 | * @var Response $extractedResponse 61 | */ 62 | $this->assertSame(strlen($body), $extractedResponse->getHeader('content-length')); 63 | } 64 | 65 | public function testTouchHeaderIfAlreadyPresent() 66 | { 67 | $middleware = new AutoContentLength(); 68 | 69 | $request = new Request(); 70 | 71 | $body = 'foo'; 72 | 73 | /** @var PromiseInterface $response */ 74 | $response = $middleware($request, new ConnectionStub(), new ParametrizedResponseMiddleware(200, ['content-length' => strlen($body) + 1], $body)); 75 | 76 | $extractedResponse = null; 77 | $response->then(function (Response $response) use (&$extractedResponse) { 78 | // We expect a FullFilledPromise that resolve instantly, 79 | // so extracting response and making assertions outside of callback secure them 80 | $extractedResponse = $response; 81 | }); 82 | 83 | $this->assertNotNull($extractedResponse); 84 | /** 85 | * @var Response $extractedResponse 86 | */ 87 | $this->assertSame(strlen($body) + 1, $extractedResponse->getHeader('content-length')); 88 | } 89 | 90 | public function testDontAddHeaderIfNoBody() 91 | { 92 | $middleware = new AutoContentLength(); 93 | 94 | $request = new Request(); 95 | 96 | /** @var PromiseInterface $response */ 97 | $response = $middleware($request, new ConnectionStub(), new EmptyResponseMiddleware()); 98 | 99 | $extractedResponse = null; 100 | $response->then(function (Response $response) use (&$extractedResponse) { 101 | // We expect a FullFilledPromise that resolve instantly, 102 | // so extracting response and making assertions outside of callback secure them 103 | $extractedResponse = $response; 104 | }); 105 | 106 | $this->assertNotNull($extractedResponse); 107 | /** 108 | * @var Response $extractedResponse 109 | */ 110 | $this->assertFalse($extractedResponse->hasHeader('content-length')); 111 | } 112 | } -------------------------------------------------------------------------------- /tests/Middleware/AutoCseqTest.php: -------------------------------------------------------------------------------- 1 | setHeader('cseq', $cseqValue); 47 | 48 | /** @var PromiseInterface $response */ 49 | $response = $middleware($request, new ConnectionStub(), new ParametrizedResponseMiddleware()); 50 | 51 | $extractedResponse = null; 52 | $response->then(function(Response $response) use (&$extractedResponse) { 53 | // We expect a FullFilledPromise that resolve instantly, 54 | // so extracting response and making assertions outside of callback secure them 55 | $extractedResponse = $response; 56 | }); 57 | 58 | $this->assertNotNull($extractedResponse); 59 | /** 60 | * @var Response $extractedResponse 61 | */ 62 | $this->assertSame($cseqValue, $extractedResponse->getHeader('cseq')); 63 | } 64 | 65 | public function testDontTouchHeaderIfAlreadyInResponse() { 66 | $middleware = new AutoCseq(); 67 | 68 | $request = new Request(); 69 | $cseqValue = 10; 70 | $request->setHeader('cseq', $cseqValue); 71 | 72 | /** @var PromiseInterface $response */ 73 | $response = $middleware($request, new ConnectionStub(), new ParametrizedResponseMiddleware(200,['cseq'=>$cseqValue+1])); 74 | 75 | $extractedResponse = null; 76 | $response->then(function(Response $response) use (&$extractedResponse) { 77 | // We expect a FullFilledPromise that resolve instantly, 78 | // so extracting response and making assertions outside of callback secure them 79 | $extractedResponse = $response; 80 | }); 81 | 82 | $this->assertNotNull($extractedResponse); 83 | /** 84 | * @var Response $extractedResponse 85 | */ 86 | $this->assertSame($cseqValue+1, $extractedResponse->getHeader('cseq')); 87 | } 88 | 89 | public function testDontAddHeaderIfNotInRequest() { 90 | $middleware = new AutoCseq(); 91 | 92 | $request = new Request(); 93 | 94 | /** @var PromiseInterface $response */ 95 | $response = $middleware($request, new ConnectionStub(), new ParametrizedResponseMiddleware()); 96 | 97 | $extractedResponse = null; 98 | $response->then(function(Response $response) use (&$extractedResponse) { 99 | // We expect a FullFilledPromise that resolve instantly, 100 | // so extracting response and making assertions outside of callback secure them 101 | $extractedResponse = $response; 102 | }); 103 | 104 | $this->assertNotNull($extractedResponse); 105 | /** 106 | * @var Response $extractedResponse 107 | */ 108 | $this->assertFalse($extractedResponse->hasHeader('cseq')); 109 | } 110 | } -------------------------------------------------------------------------------- /tests/Middleware/LogTest.php: -------------------------------------------------------------------------------- 1 | then(function(Response $response) use (&$extractedResponse) { 53 | // We expect a FullFilledPromise that resolve instantly, 54 | // so extracting response and making assertions outside of callback secure them 55 | $extractedResponse = $response; 56 | }); 57 | $this->assertTrue($extractedResponse instanceof Response); 58 | $logger->shouldHaveReceived('log')->twice(); 59 | } 60 | 61 | public function testRejectedString() { 62 | $logger = \Mockery::spy(LoggerInterface::class); 63 | $middleware = new Log($logger); 64 | $request = new Request(); 65 | 66 | $response = $middleware($request, new ConnectionStub(), new RejectMiddleware('foo')); 67 | 68 | $this->assertTrue($response instanceof RejectedPromise); 69 | $logger->shouldHaveReceived('log')->twice(); 70 | } 71 | 72 | public function testRejectedException() { 73 | $logger = \Mockery::spy(LoggerInterface::class); 74 | $middleware = new Log($logger); 75 | $request = new Request(); 76 | 77 | $response = $middleware($request, new ConnectionStub(), new RejectMiddleware(new \Exception('foo'))); 78 | 79 | $this->assertTrue($response instanceof RejectedPromise); 80 | $logger->shouldHaveReceived('log')->twice(); 81 | } 82 | } -------------------------------------------------------------------------------- /tests/Middleware/MiddlewareStackTest.php: -------------------------------------------------------------------------------- 1 | assertSame(1, $response->statusCode); 58 | } 59 | 60 | public function testInvokeMany() 61 | { 62 | $middleware1 = new CallNextMiddleware(); 63 | $middleware2 = new CallNextMiddleware(); 64 | $middleware3 = $this->getMockBuilder(ParametrizedResponseMiddleware::class)->setMethods(['__invoke'])->getMock(); 65 | $middleware3->expects($this->once())->method('__invoke')->willReturn(new Response()); 66 | 67 | $stack = new MiddlewareStack([$middleware1, $middleware2, $middleware3]); 68 | 69 | $stack(new Request(), new ConnectionStub()); 70 | 71 | $this->assertSame(1, $middleware1->calls); 72 | $this->assertSame(1, $middleware2->calls); 73 | } 74 | } -------------------------------------------------------------------------------- /tests/Mock/CallNextMiddleware.php: -------------------------------------------------------------------------------- 1 | calls++; 39 | return $next($request, $connection); 40 | } 41 | } -------------------------------------------------------------------------------- /tests/Mock/ParametrizedResponseMiddleware.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 42 | $this->headers = $headers; 43 | $this->body = $body; 44 | } 45 | 46 | public function __invoke(Request $request, ConnectionInterface $connection) 47 | { 48 | return MessageFactory::response($this->statusCode, $this->headers, $this->body); 49 | } 50 | } -------------------------------------------------------------------------------- /tests/Mock/RejectMiddleware.php: -------------------------------------------------------------------------------- 1 | rejectValue = $rejectValue; 40 | } 41 | 42 | public function __invoke(Request $request, ConnectionInterface $connection) 43 | { 44 | return new RejectedPromise($this->rejectValue); 45 | } 46 | } -------------------------------------------------------------------------------- /tests/ServerTest.php: -------------------------------------------------------------------------------- 1 | socket = new SocketServerStub(); 18 | $this->connection = $this->getConnection(); 19 | } 20 | 21 | private function getConnection(): ConnectionInterface { 22 | return $this->getMockBuilder('React\Socket\Connection') 23 | ->disableOriginalConstructor() 24 | ->setMethods( 25 | array( 26 | 'write', 27 | 'end', 28 | 'close', 29 | 'pause', 30 | 'resume', 31 | 'isReadable', 32 | 'isWritable', 33 | 'getRemoteAddress', 34 | 'getLocalAddress', 35 | 'pipe' 36 | ) 37 | ) 38 | ->getMock(); 39 | } 40 | 41 | public function testSimpleRequest() { 42 | $called = null; 43 | $server = new Server(function ($request, $connection) use (&$called) { 44 | ++$called; 45 | $this->assertInstanceOf(Request::class, $request); 46 | $this->assertInstanceOf(ConnectionInterface::class, $connection); 47 | 48 | // Just do basic checks over $request 49 | /** @var Request $request */ 50 | $this->assertSame(1, count($request->headers)); 51 | $this->assertSame('1', $request->getHeader('cseq')); 52 | 53 | return MessageFactory::response(); 54 | }); 55 | 56 | $server->on('error', function($exception) { 57 | throw $exception; 58 | }); 59 | 60 | $server->listen($this->socket); 61 | 62 | $this->socket->emit('connection', array($this->connection)); 63 | $this->connection->emit('data', array("OPTIONS / RTSP/1.0\r\nCseq:1\r\n\r\n")); 64 | 65 | $this->assertSame(1, $called); 66 | } 67 | 68 | public function testInvalidMethodRequest() { 69 | $server = new Server(function () use (&$called) { 70 | throw new \Exception(); 71 | }); 72 | 73 | $error = null; 74 | $server->on('error', function($exception) use (&$error) { 75 | $error = $exception; 76 | }); 77 | 78 | $server->listen($this->socket); 79 | $this->socket->emit('connection', array($this->connection)); 80 | $this->connection->emit('data', array("FOO_METHOD / RTSP/1.0\r\nCseq:1\r\n\r\n")); 81 | 82 | $this->assertNotNull($error); 83 | $this->assertInstanceOf(MessageException::class, $error); 84 | } 85 | 86 | public function testCallbackDoNotReturnResponse() { 87 | $server = new Server(function () { 88 | return false; 89 | }); 90 | 91 | $error = null; 92 | $server->on('error', function($exception) use (&$error) { 93 | $error = $exception; 94 | }); 95 | 96 | $server->listen($this->socket); 97 | 98 | $this->socket->emit('connection', array($this->connection)); 99 | $this->connection->emit('data', array("OPTIONS / RTSP/1.0\r\nCseq:1\r\n\r\n")); 100 | 101 | $this->assertInstanceOf(ServerException::class, $error); 102 | $this->assertTrue(strpos($error->getMessage(), 'boolean')>0); 103 | } 104 | 105 | public function testRequestWithResponse() { 106 | $called = null; 107 | $server = new Server(function () use (&$called) { 108 | ++$called; 109 | return MessageFactory::response(200, ['foo'=>'bar'], 'baz', 'qux'); 110 | }); 111 | 112 | $server->on('error', function($exception) { 113 | throw $exception; 114 | }); 115 | 116 | $server->listen($this->socket); 117 | 118 | $this->connection->expects($this->exactly(1))->method('write')->with($this->identicalTo("RTSP/1.0 200 qux\r\nfoo: bar\r\n\r\nbaz")); 119 | 120 | $this->socket->emit('connection', array($this->connection)); 121 | $this->connection->emit('data', array("OPTIONS / RTSP/1.0\r\nCseq:1\r\n\r\n")); 122 | 123 | $this->assertSame(1, $called); 124 | } 125 | 126 | public function testNoMemoryLeak() { 127 | $called = null; 128 | $server = new Server(function () use (&$called) { 129 | ++$called; 130 | return MessageFactory::response(200, ['foo'=>'bar'], 'baz', 'qux'); 131 | }); 132 | 133 | $server->on('error', function($exception) { 134 | throw $exception; 135 | }); 136 | 137 | $server->listen($this->socket); 138 | 139 | for ($i = 0; $i < 100000; $i++) { 140 | if ($i === 100) { 141 | //Warm a bit then take initial memory usage 142 | $initialMemoryUsage = memory_get_usage(false); 143 | } 144 | $connection = new ConnectionStub(); 145 | $this->socket->emit('connection', array($connection)); 146 | $connection->emit('data', array("OPTIONS / RTSP/1.0\r\nCseq:1\r\n\r\n")); 147 | $connection->emit('close'); 148 | $connection->removeAllListeners(); 149 | } 150 | $finalMemoryUsage = memory_get_usage(false); 151 | $this->assertTrue($finalMemoryUsage <= 1.001 * $initialMemoryUsage, "Memory grown from $initialMemoryUsage to $finalMemoryUsage"); 152 | 153 | $this->assertTrue($called !== null && $called>0); 154 | } 155 | } -------------------------------------------------------------------------------- /tests/SocketServerStub.php: -------------------------------------------------------------------------------- 1 | addPsr4('PhpBg\\Rtsp\\Tests\\', __DIR__ ); -------------------------------------------------------------------------------- /tests/server.php: -------------------------------------------------------------------------------- 1 | setHeader('cseq', $request->getHeader('cseq')); 14 | return $response; 15 | }); 16 | 17 | $server->on('error', function (\Exception $e) { 18 | echo $e->getMessage() . "\r\n"; 19 | echo $e->getTraceAsString() . "\r\n"; 20 | }); 21 | 22 | $server->listen($socket); 23 | echo "Server started\r\n"; 24 | echo "Open any decent video player (e.g. vlc, mpv) and open rtsp://localhost:5540\r\n"; 25 | $loop->run(); -------------------------------------------------------------------------------- /tests/serverWithMiddlewares.php: -------------------------------------------------------------------------------- 1 | then(function($resolvedResponse) { 23 | echo $resolvedResponse."\r\n"; 24 | return $resolvedResponse; 25 | }); 26 | }, 27 | new \PhpBg\Rtsp\Middleware\AutoCseq(), 28 | new \PhpBg\Rtsp\Middleware\AutoContentLength(), 29 | function (\PhpBg\Rtsp\Message\Request $request, \React\Socket\ConnectionInterface $connection) { 30 | // 200 OK response middleware 31 | return \PhpBg\Rtsp\Message\MessageFactory::response(); 32 | } 33 | ]; 34 | 35 | $server = new \PhpBg\Rtsp\Server(new \PhpBg\Rtsp\Middleware\MiddlewareStack($middlewares)); 36 | 37 | $server->on('error', function (\Exception $e) { 38 | echo $e->getMessage() . "\r\n"; 39 | echo $e->getTraceAsString() . "\r\n"; 40 | }); 41 | 42 | $server->listen($socket); 43 | echo "Server started\r\n"; 44 | echo "Open any decent video player (e.g. vlc, mpv) and open rtsp://localhost:5540\r\n"; 45 | $loop->run(); --------------------------------------------------------------------------------