├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── AppendStream.php ├── BufferStream.php ├── CachingStream.php ├── DroppingStream.php ├── FnStream.php ├── InflateStream.php ├── LazyOpenStream.php ├── LimitStream.php ├── MessageTrait.php ├── MultipartStream.php ├── NoSeekStream.php ├── PumpStream.php ├── Request.php ├── Response.php ├── ServerRequest.php ├── Stream.php ├── StreamDecoratorTrait.php ├── StreamWrapper.php ├── Uri.php ├── functions.php └── functions_include.php └── tests ├── AppendStreamTest.php ├── BufferStreamTest.php ├── CachingStreamTest.php ├── DroppingStreamTest.php ├── FnStreamTest.php ├── FunctionsTest.php ├── InflateStreamTest.php ├── LazyOpenStreamTest.php ├── LimitStreamTest.php ├── MultipartStreamTest.php ├── NoSeekStreamTest.php ├── PumpStreamTest.php ├── RequestTest.php ├── ResponseTest.php ├── ServerRequestTest.php ├── StreamDecoratorTraitTest.php ├── StreamTest.php ├── StreamWrapperTest.php ├── UriTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | phpunit.xml 2 | composer.phar 3 | composer.lock 4 | composer-test.lock 5 | vendor/ 6 | build/artifacts/ 7 | artifacts/ 8 | docs/_build 9 | docs/*.pyc 10 | .idea 11 | .DS_STORE 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | install: 6 | - travis_retry composer install --no-interaction --prefer-source 7 | 8 | script: make test 9 | 10 | matrix: 11 | include: 12 | - php: 5.3 13 | dist: precise 14 | - php: 5.4 15 | - php: 5.5 16 | - php: 5.6 17 | - php: 7.0 18 | - php: hhvm 19 | allow_failures: 20 | - php: hhvm 21 | fast_finish: true 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.2.0 - 2015-08-15 4 | 5 | * Body as `"0"` is now properly added to a response. 6 | * Now allowing forward seeking in CachingStream. 7 | * Now properly parsing HTTP requests that contain proxy targets in 8 | `parse_request`. 9 | * functions.php is now conditionally required. 10 | * user-info is no longer dropped when resolving URIs. 11 | 12 | ## 1.1.0 - 2015-06-24 13 | 14 | * URIs can now be relative. 15 | * `multipart/form-data` headers are now overridden case-insensitively. 16 | * URI paths no longer encode the following characters because they are allowed 17 | in URIs: "(", ")", "*", "!", "'" 18 | * A port is no longer added to a URI when the scheme is missing and no port is 19 | present. 20 | 21 | ## 1.0.0 - 2015-05-19 22 | 23 | Initial release. 24 | 25 | Currently unsupported: 26 | 27 | - `Psr\Http\Message\ServerRequestInterface` 28 | - `Psr\Http\Message\UploadedFileInterface` 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM greensheep/dockerfiles-php-5.3 2 | RUN apt-get update -y 3 | RUN apt-get install -y curl 4 | RUN curl -sS https://getcomposer.org/installer | php 5 | RUN mv composer.phar /usr/local/bin/composer -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Michael Dowling, https://github.com/mtdowling 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clean test 2 | 3 | test: 4 | vendor/bin/phpunit $(TEST) 5 | 6 | coverage: 7 | vendor/bin/phpunit --coverage-html=artifacts/coverage $(TEST) 8 | 9 | view-coverage: 10 | open artifacts/coverage/index.html 11 | 12 | clean: 13 | rm -rf artifacts/* 14 | 15 | .PHONY: docker-login 16 | docker-login: 17 | docker run -t -i -v $(shell pwd):/opt/psr7 ringcentral-psr7 /bin/bash 18 | 19 | .PHONY: docker-build 20 | docker-build: 21 | docker build -t ringcentral-psr7 . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSR-7 Message Implementation 2 | 3 | This repository contains a partial [PSR-7](http://www.php-fig.org/psr/psr-7/) 4 | message implementation, several stream decorators, and some helpful 5 | functionality like query string parsing. Currently missing 6 | ServerRequestInterface and UploadedFileInterface; a pull request for these features is welcome. 7 | 8 | 9 | # Stream implementation 10 | 11 | This package comes with a number of stream implementations and stream 12 | decorators. 13 | 14 | 15 | ## AppendStream 16 | 17 | `RingCentral\Psr7\AppendStream` 18 | 19 | Reads from multiple streams, one after the other. 20 | 21 | ```php 22 | use RingCentral\Psr7; 23 | 24 | $a = Psr7\stream_for('abc, '); 25 | $b = Psr7\stream_for('123.'); 26 | $composed = new Psr7\AppendStream([$a, $b]); 27 | 28 | $composed->addStream(Psr7\stream_for(' Above all listen to me'). 29 | 30 | echo $composed(); // abc, 123. Above all listen to me. 31 | ``` 32 | 33 | 34 | ## BufferStream 35 | 36 | `RingCentral\Psr7\BufferStream` 37 | 38 | Provides a buffer stream that can be written to to fill a buffer, and read 39 | from to remove bytes from the buffer. 40 | 41 | This stream returns a "hwm" metadata value that tells upstream consumers 42 | what the configured high water mark of the stream is, or the maximum 43 | preferred size of the buffer. 44 | 45 | ```php 46 | use RingCentral\Psr7; 47 | 48 | // When more than 1024 bytes are in the buffer, it will begin returning 49 | // false to writes. This is an indication that writers should slow down. 50 | $buffer = new Psr7\BufferStream(1024); 51 | ``` 52 | 53 | 54 | ## CachingStream 55 | 56 | The CachingStream is used to allow seeking over previously read bytes on 57 | non-seekable streams. This can be useful when transferring a non-seekable 58 | entity body fails due to needing to rewind the stream (for example, resulting 59 | from a redirect). Data that is read from the remote stream will be buffered in 60 | a PHP temp stream so that previously read bytes are cached first in memory, 61 | then on disk. 62 | 63 | ```php 64 | use RingCentral\Psr7; 65 | 66 | $original = Psr7\stream_for(fopen('http://www.google.com', 'r')); 67 | $stream = new Psr7\CachingStream($original); 68 | 69 | $stream->read(1024); 70 | echo $stream->tell(); 71 | // 1024 72 | 73 | $stream->seek(0); 74 | echo $stream->tell(); 75 | // 0 76 | ``` 77 | 78 | 79 | ## DroppingStream 80 | 81 | `RingCentral\Psr7\DroppingStream` 82 | 83 | Stream decorator that begins dropping data once the size of the underlying 84 | stream becomes too full. 85 | 86 | ```php 87 | use RingCentral\Psr7; 88 | 89 | // Create an empty stream 90 | $stream = Psr7\stream_for(); 91 | 92 | // Start dropping data when the stream has more than 10 bytes 93 | $dropping = new Psr7\DroppingStream($stream, 10); 94 | 95 | $stream->write('01234567890123456789'); 96 | echo $stream; // 0123456789 97 | ``` 98 | 99 | 100 | ## FnStream 101 | 102 | `RingCentral\Psr7\FnStream` 103 | 104 | Compose stream implementations based on a hash of functions. 105 | 106 | Allows for easy testing and extension of a provided stream without needing to 107 | to create a concrete class for a simple extension point. 108 | 109 | ```php 110 | 111 | use RingCentral\Psr7; 112 | 113 | $stream = Psr7\stream_for('hi'); 114 | $fnStream = Psr7\FnStream::decorate($stream, [ 115 | 'rewind' => function () use ($stream) { 116 | echo 'About to rewind - '; 117 | $stream->rewind(); 118 | echo 'rewound!'; 119 | } 120 | ]); 121 | 122 | $fnStream->rewind(); 123 | // Outputs: About to rewind - rewound! 124 | ``` 125 | 126 | 127 | ## InflateStream 128 | 129 | `RingCentral\Psr7\InflateStream` 130 | 131 | Uses PHP's zlib.inflate filter to inflate deflate or gzipped content. 132 | 133 | This stream decorator skips the first 10 bytes of the given stream to remove 134 | the gzip header, converts the provided stream to a PHP stream resource, 135 | then appends the zlib.inflate filter. The stream is then converted back 136 | to a Guzzle stream resource to be used as a Guzzle stream. 137 | 138 | 139 | ## LazyOpenStream 140 | 141 | `RingCentral\Psr7\LazyOpenStream` 142 | 143 | Lazily reads or writes to a file that is opened only after an IO operation 144 | take place on the stream. 145 | 146 | ```php 147 | use RingCentral\Psr7; 148 | 149 | $stream = new Psr7\LazyOpenStream('/path/to/file', 'r'); 150 | // The file has not yet been opened... 151 | 152 | echo $stream->read(10); 153 | // The file is opened and read from only when needed. 154 | ``` 155 | 156 | 157 | ## LimitStream 158 | 159 | `RingCentral\Psr7\LimitStream` 160 | 161 | LimitStream can be used to read a subset or slice of an existing stream object. 162 | This can be useful for breaking a large file into smaller pieces to be sent in 163 | chunks (e.g. Amazon S3's multipart upload API). 164 | 165 | ```php 166 | use RingCentral\Psr7; 167 | 168 | $original = Psr7\stream_for(fopen('/tmp/test.txt', 'r+')); 169 | echo $original->getSize(); 170 | // >>> 1048576 171 | 172 | // Limit the size of the body to 1024 bytes and start reading from byte 2048 173 | $stream = new Psr7\LimitStream($original, 1024, 2048); 174 | echo $stream->getSize(); 175 | // >>> 1024 176 | echo $stream->tell(); 177 | // >>> 0 178 | ``` 179 | 180 | 181 | ## MultipartStream 182 | 183 | `RingCentral\Psr7\MultipartStream` 184 | 185 | Stream that when read returns bytes for a streaming multipart or 186 | multipart/form-data stream. 187 | 188 | 189 | ## NoSeekStream 190 | 191 | `RingCentral\Psr7\NoSeekStream` 192 | 193 | NoSeekStream wraps a stream and does not allow seeking. 194 | 195 | ```php 196 | use RingCentral\Psr7; 197 | 198 | $original = Psr7\stream_for('foo'); 199 | $noSeek = new Psr7\NoSeekStream($original); 200 | 201 | echo $noSeek->read(3); 202 | // foo 203 | var_export($noSeek->isSeekable()); 204 | // false 205 | $noSeek->seek(0); 206 | var_export($noSeek->read(3)); 207 | // NULL 208 | ``` 209 | 210 | 211 | ## PumpStream 212 | 213 | `RingCentral\Psr7\PumpStream` 214 | 215 | Provides a read only stream that pumps data from a PHP callable. 216 | 217 | When invoking the provided callable, the PumpStream will pass the amount of 218 | data requested to read to the callable. The callable can choose to ignore 219 | this value and return fewer or more bytes than requested. Any extra data 220 | returned by the provided callable is buffered internally until drained using 221 | the read() function of the PumpStream. The provided callable MUST return 222 | false when there is no more data to read. 223 | 224 | 225 | ## Implementing stream decorators 226 | 227 | Creating a stream decorator is very easy thanks to the 228 | `RingCentral\Psr7\StreamDecoratorTrait`. This trait provides methods that 229 | implement `Psr\Http\Message\StreamInterface` by proxying to an underlying 230 | stream. Just `use` the `StreamDecoratorTrait` and implement your custom 231 | methods. 232 | 233 | For example, let's say we wanted to call a specific function each time the last 234 | byte is read from a stream. This could be implemented by overriding the 235 | `read()` method. 236 | 237 | ```php 238 | use Psr\Http\Message\StreamInterface; 239 | use RingCentral\Psr7\StreamDecoratorTrait; 240 | 241 | class EofCallbackStream implements StreamInterface 242 | { 243 | use StreamDecoratorTrait; 244 | 245 | private $callback; 246 | 247 | public function __construct(StreamInterface $stream, callable $cb) 248 | { 249 | $this->stream = $stream; 250 | $this->callback = $cb; 251 | } 252 | 253 | public function read($length) 254 | { 255 | $result = $this->stream->read($length); 256 | 257 | // Invoke the callback when EOF is hit. 258 | if ($this->eof()) { 259 | call_user_func($this->callback); 260 | } 261 | 262 | return $result; 263 | } 264 | } 265 | ``` 266 | 267 | This decorator could be added to any existing stream and used like so: 268 | 269 | ```php 270 | use RingCentral\Psr7; 271 | 272 | $original = Psr7\stream_for('foo'); 273 | 274 | $eofStream = new EofCallbackStream($original, function () { 275 | echo 'EOF!'; 276 | }); 277 | 278 | $eofStream->read(2); 279 | $eofStream->read(1); 280 | // echoes "EOF!" 281 | $eofStream->seek(0); 282 | $eofStream->read(3); 283 | // echoes "EOF!" 284 | ``` 285 | 286 | 287 | ## PHP StreamWrapper 288 | 289 | You can use the `RingCentral\Psr7\StreamWrapper` class if you need to use a 290 | PSR-7 stream as a PHP stream resource. 291 | 292 | Use the `RingCentral\Psr7\StreamWrapper::getResource()` method to create a PHP 293 | stream from a PSR-7 stream. 294 | 295 | ```php 296 | use RingCentral\Psr7\StreamWrapper; 297 | 298 | $stream = RingCentral\Psr7\stream_for('hello!'); 299 | $resource = StreamWrapper::getResource($stream); 300 | echo fread($resource, 6); // outputs hello! 301 | ``` 302 | 303 | 304 | # Function API 305 | 306 | There are various functions available under the `RingCentral\Psr7` namespace. 307 | 308 | 309 | ## `function str` 310 | 311 | `function str(MessageInterface $message)` 312 | 313 | Returns the string representation of an HTTP message. 314 | 315 | ```php 316 | $request = new RingCentral\Psr7\Request('GET', 'http://example.com'); 317 | echo RingCentral\Psr7\str($request); 318 | ``` 319 | 320 | 321 | ## `function uri_for` 322 | 323 | `function uri_for($uri)` 324 | 325 | This function accepts a string or `Psr\Http\Message\UriInterface` and returns a 326 | UriInterface for the given value. If the value is already a `UriInterface`, it 327 | is returned as-is. 328 | 329 | ```php 330 | $uri = RingCentral\Psr7\uri_for('http://example.com'); 331 | assert($uri === RingCentral\Psr7\uri_for($uri)); 332 | ``` 333 | 334 | 335 | ## `function stream_for` 336 | 337 | `function stream_for($resource = '', array $options = [])` 338 | 339 | Create a new stream based on the input type. 340 | 341 | Options is an associative array that can contain the following keys: 342 | 343 | * - metadata: Array of custom metadata. 344 | * - size: Size of the stream. 345 | 346 | This method accepts the following `$resource` types: 347 | 348 | - `Psr\Http\Message\StreamInterface`: Returns the value as-is. 349 | - `string`: Creates a stream object that uses the given string as the contents. 350 | - `resource`: Creates a stream object that wraps the given PHP stream resource. 351 | - `Iterator`: If the provided value implements `Iterator`, then a read-only 352 | stream object will be created that wraps the given iterable. Each time the 353 | stream is read from, data from the iterator will fill a buffer and will be 354 | continuously called until the buffer is equal to the requested read size. 355 | Subsequent read calls will first read from the buffer and then call `next` 356 | on the underlying iterator until it is exhausted. 357 | - `object` with `__toString()`: If the object has the `__toString()` method, 358 | the object will be cast to a string and then a stream will be returned that 359 | uses the string value. 360 | - `NULL`: When `null` is passed, an empty stream object is returned. 361 | - `callable` When a callable is passed, a read-only stream object will be 362 | created that invokes the given callable. The callable is invoked with the 363 | number of suggested bytes to read. The callable can return any number of 364 | bytes, but MUST return `false` when there is no more data to return. The 365 | stream object that wraps the callable will invoke the callable until the 366 | number of requested bytes are available. Any additional bytes will be 367 | buffered and used in subsequent reads. 368 | 369 | ```php 370 | $stream = RingCentral\Psr7\stream_for('foo'); 371 | $stream = RingCentral\Psr7\stream_for(fopen('/path/to/file', 'r')); 372 | 373 | $generator function ($bytes) { 374 | for ($i = 0; $i < $bytes; $i++) { 375 | yield ' '; 376 | } 377 | } 378 | 379 | $stream = RingCentral\Psr7\stream_for($generator(100)); 380 | ``` 381 | 382 | 383 | ## `function parse_header` 384 | 385 | `function parse_header($header)` 386 | 387 | Parse an array of header values containing ";" separated data into an array of 388 | associative arrays representing the header key value pair data of the header. 389 | When a parameter does not contain a value, but just contains a key, this 390 | function will inject a key with a '' string value. 391 | 392 | 393 | ## `function normalize_header` 394 | 395 | `function normalize_header($header)` 396 | 397 | Converts an array of header values that may contain comma separated headers 398 | into an array of headers with no comma separated values. 399 | 400 | 401 | ## `function modify_request` 402 | 403 | `function modify_request(RequestInterface $request, array $changes)` 404 | 405 | Clone and modify a request with the given changes. This method is useful for 406 | reducing the number of clones needed to mutate a message. 407 | 408 | The changes can be one of: 409 | 410 | - method: (string) Changes the HTTP method. 411 | - set_headers: (array) Sets the given headers. 412 | - remove_headers: (array) Remove the given headers. 413 | - body: (mixed) Sets the given body. 414 | - uri: (UriInterface) Set the URI. 415 | - query: (string) Set the query string value of the URI. 416 | - version: (string) Set the protocol version. 417 | 418 | 419 | ## `function rewind_body` 420 | 421 | `function rewind_body(MessageInterface $message)` 422 | 423 | Attempts to rewind a message body and throws an exception on failure. The body 424 | of the message will only be rewound if a call to `tell()` returns a value other 425 | than `0`. 426 | 427 | 428 | ## `function try_fopen` 429 | 430 | `function try_fopen($filename, $mode)` 431 | 432 | Safely opens a PHP stream resource using a filename. 433 | 434 | When fopen fails, PHP normally raises a warning. This function adds an error 435 | handler that checks for errors and throws an exception instead. 436 | 437 | 438 | ## `function copy_to_string` 439 | 440 | `function copy_to_string(StreamInterface $stream, $maxLen = -1)` 441 | 442 | Copy the contents of a stream into a string until the given number of bytes 443 | have been read. 444 | 445 | 446 | ## `function copy_to_stream` 447 | 448 | `function copy_to_stream(StreamInterface $source, StreamInterface $dest, $maxLen = -1)` 449 | 450 | Copy the contents of a stream into another stream until the given number of 451 | bytes have been read. 452 | 453 | 454 | ## `function hash` 455 | 456 | `function hash(StreamInterface $stream, $algo, $rawOutput = false)` 457 | 458 | Calculate a hash of a Stream. This method reads the entire stream to calculate 459 | a rolling hash (based on PHP's hash_init functions). 460 | 461 | 462 | ## `function readline` 463 | 464 | `function readline(StreamInterface $stream, $maxLength = null)` 465 | 466 | Read a line from the stream up to the maximum allowed buffer length. 467 | 468 | 469 | ## `function parse_request` 470 | 471 | `function parse_request($message)` 472 | 473 | Parses a request message string into a request object. 474 | 475 | 476 | ## `function parse_server_request` 477 | 478 | `function parse_server_request($message, array $serverParams = array())` 479 | 480 | Parses a request message string into a server-side request object. 481 | 482 | 483 | ## `function parse_response` 484 | 485 | `function parse_response($message)` 486 | 487 | Parses a response message string into a response object. 488 | 489 | 490 | ## `function parse_query` 491 | 492 | `function parse_query($str, $urlEncoding = true)` 493 | 494 | Parse a query string into an associative array. 495 | 496 | If multiple values are found for the same key, the value of that key value pair 497 | will become an array. This function does not parse nested PHP style arrays into 498 | an associative array (e.g., `foo[a]=1&foo[b]=2` will be parsed into 499 | `['foo[a]' => '1', 'foo[b]' => '2']`). 500 | 501 | 502 | ## `function build_query` 503 | 504 | `function build_query(array $params, $encoding = PHP_QUERY_RFC3986)` 505 | 506 | Build a query string from an array of key value pairs. 507 | 508 | This function can use the return value of parseQuery() to build a query string. 509 | This function does not modify the provided keys when an array is encountered 510 | (like http_build_query would). 511 | 512 | 513 | ## `function mimetype_from_filename` 514 | 515 | `function mimetype_from_filename($filename)` 516 | 517 | Determines the mimetype of a file by looking at its extension. 518 | 519 | 520 | ## `function mimetype_from_extension` 521 | 522 | `function mimetype_from_extension($extension)` 523 | 524 | Maps a file extensions to a mimetype. 525 | 526 | 527 | # Static URI methods 528 | 529 | The `RingCentral\Psr7\Uri` class has several static methods to manipulate URIs. 530 | 531 | 532 | ## `RingCentral\Psr7\Uri::removeDotSegments` 533 | 534 | `public static function removeDotSegments($path) -> UriInterface` 535 | 536 | Removes dot segments from a path and returns the new path. 537 | 538 | See http://tools.ietf.org/html/rfc3986#section-5.2.4 539 | 540 | 541 | ## `RingCentral\Psr7\Uri::resolve` 542 | 543 | `public static function resolve(UriInterface $base, $rel) -> UriInterface` 544 | 545 | Resolve a base URI with a relative URI and return a new URI. 546 | 547 | See http://tools.ietf.org/html/rfc3986#section-5 548 | 549 | 550 | ## `RingCentral\Psr7\Uri::withQueryValue` 551 | 552 | `public static function withQueryValue(UriInterface $uri, $key, $value) -> UriInterface` 553 | 554 | Create a new URI with a specific query string value. 555 | 556 | Any existing query string values that exactly match the provided key are 557 | removed and replaced with the given key value pair. 558 | 559 | Note: this function will convert "=" to "%3D" and "&" to "%26". 560 | 561 | 562 | ## `RingCentral\Psr7\Uri::withoutQueryValue` 563 | 564 | `public static function withoutQueryValue(UriInterface $uri, $key, $value) -> UriInterface` 565 | 566 | Create a new URI with a specific query string value removed. 567 | 568 | Any existing query string values that exactly match the provided key are 569 | removed. 570 | 571 | Note: this function will convert "=" to "%3D" and "&" to "%26". 572 | 573 | 574 | ## `RingCentral\Psr7\Uri::fromParts` 575 | 576 | `public static function fromParts(array $parts) -> UriInterface` 577 | 578 | Create a `RingCentral\Psr7\Uri` object from a hash of `parse_url` parts. 579 | 580 | 581 | # Not Implemented 582 | 583 | A few aspects of PSR-7 are not implemented in this project. A pull request for 584 | any of these features is welcome: 585 | 586 | - `Psr\Http\Message\ServerRequestInterface` 587 | - `Psr\Http\Message\UploadedFileInterface` 588 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ringcentral/psr7", 3 | "type": "library", 4 | "description": "PSR-7 message implementation", 5 | "keywords": ["message", "stream", "http", "uri"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Michael Dowling", 10 | "email": "mtdowling@gmail.com", 11 | "homepage": "https://github.com/mtdowling" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3", 16 | "psr/http-message": "~1.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "~4.0" 20 | }, 21 | "provide": { 22 | "psr/http-message-implementation": "1.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "RingCentral\\Psr7\\": "src/" 27 | }, 28 | "files": ["src/functions_include.php"] 29 | }, 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "1.0-dev" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | tests 7 | 8 | 9 | 10 | 11 | src 12 | 13 | src/ 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/AppendStream.php: -------------------------------------------------------------------------------- 1 | addStream($stream); 29 | } 30 | } 31 | 32 | public function __toString() 33 | { 34 | try { 35 | $this->rewind(); 36 | return $this->getContents(); 37 | } catch (\Exception $e) { 38 | return ''; 39 | } 40 | } 41 | 42 | /** 43 | * Add a stream to the AppendStream 44 | * 45 | * @param StreamInterface $stream Stream to append. Must be readable. 46 | * 47 | * @throws \InvalidArgumentException if the stream is not readable 48 | */ 49 | public function addStream(StreamInterface $stream) 50 | { 51 | if (!$stream->isReadable()) { 52 | throw new \InvalidArgumentException('Each stream must be readable'); 53 | } 54 | 55 | // The stream is only seekable if all streams are seekable 56 | if (!$stream->isSeekable()) { 57 | $this->seekable = false; 58 | } 59 | 60 | $this->streams[] = $stream; 61 | } 62 | 63 | public function getContents() 64 | { 65 | return copy_to_string($this); 66 | } 67 | 68 | /** 69 | * Closes each attached stream. 70 | * 71 | * {@inheritdoc} 72 | */ 73 | public function close() 74 | { 75 | $this->pos = $this->current = 0; 76 | 77 | foreach ($this->streams as $stream) { 78 | $stream->close(); 79 | } 80 | 81 | $this->streams = array(); 82 | } 83 | 84 | /** 85 | * Detaches each attached stream 86 | * 87 | * {@inheritdoc} 88 | */ 89 | public function detach() 90 | { 91 | $this->close(); 92 | $this->detached = true; 93 | } 94 | 95 | public function tell() 96 | { 97 | return $this->pos; 98 | } 99 | 100 | /** 101 | * Tries to calculate the size by adding the size of each stream. 102 | * 103 | * If any of the streams do not return a valid number, then the size of the 104 | * append stream cannot be determined and null is returned. 105 | * 106 | * {@inheritdoc} 107 | */ 108 | public function getSize() 109 | { 110 | $size = 0; 111 | 112 | foreach ($this->streams as $stream) { 113 | $s = $stream->getSize(); 114 | if ($s === null) { 115 | return null; 116 | } 117 | $size += $s; 118 | } 119 | 120 | return $size; 121 | } 122 | 123 | public function eof() 124 | { 125 | return !$this->streams || 126 | ($this->current >= count($this->streams) - 1 && 127 | $this->streams[$this->current]->eof()); 128 | } 129 | 130 | public function rewind() 131 | { 132 | $this->seek(0); 133 | } 134 | 135 | /** 136 | * Attempts to seek to the given position. Only supports SEEK_SET. 137 | * 138 | * {@inheritdoc} 139 | */ 140 | public function seek($offset, $whence = SEEK_SET) 141 | { 142 | if (!$this->seekable) { 143 | throw new \RuntimeException('This AppendStream is not seekable'); 144 | } elseif ($whence !== SEEK_SET) { 145 | throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); 146 | } 147 | 148 | $this->pos = $this->current = 0; 149 | 150 | // Rewind each stream 151 | foreach ($this->streams as $i => $stream) { 152 | try { 153 | $stream->rewind(); 154 | } catch (\Exception $e) { 155 | throw new \RuntimeException('Unable to seek stream ' 156 | . $i . ' of the AppendStream', 0, $e); 157 | } 158 | } 159 | 160 | // Seek to the actual position by reading from each stream 161 | while ($this->pos < $offset && !$this->eof()) { 162 | $result = $this->read(min(8096, $offset - $this->pos)); 163 | if ($result === '') { 164 | break; 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * Reads from all of the appended streams until the length is met or EOF. 171 | * 172 | * {@inheritdoc} 173 | */ 174 | public function read($length) 175 | { 176 | $buffer = ''; 177 | $total = count($this->streams) - 1; 178 | $remaining = $length; 179 | $progressToNext = false; 180 | 181 | while ($remaining > 0) { 182 | 183 | // Progress to the next stream if needed. 184 | if ($progressToNext || $this->streams[$this->current]->eof()) { 185 | $progressToNext = false; 186 | if ($this->current === $total) { 187 | break; 188 | } 189 | $this->current++; 190 | } 191 | 192 | $result = $this->streams[$this->current]->read($remaining); 193 | 194 | // Using a loose comparison here to match on '', false, and null 195 | if ($result == null) { 196 | $progressToNext = true; 197 | continue; 198 | } 199 | 200 | $buffer .= $result; 201 | $remaining = $length - strlen($buffer); 202 | } 203 | 204 | $this->pos += strlen($buffer); 205 | 206 | return $buffer; 207 | } 208 | 209 | public function isReadable() 210 | { 211 | return true; 212 | } 213 | 214 | public function isWritable() 215 | { 216 | return false; 217 | } 218 | 219 | public function isSeekable() 220 | { 221 | return $this->seekable; 222 | } 223 | 224 | public function write($string) 225 | { 226 | throw new \RuntimeException('Cannot write to an AppendStream'); 227 | } 228 | 229 | public function getMetadata($key = null) 230 | { 231 | return $key ? null : array(); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/BufferStream.php: -------------------------------------------------------------------------------- 1 | hwm = $hwm; 29 | } 30 | 31 | public function __toString() 32 | { 33 | return $this->getContents(); 34 | } 35 | 36 | public function getContents() 37 | { 38 | $buffer = $this->buffer; 39 | $this->buffer = ''; 40 | 41 | return $buffer; 42 | } 43 | 44 | public function close() 45 | { 46 | $this->buffer = ''; 47 | } 48 | 49 | public function detach() 50 | { 51 | $this->close(); 52 | } 53 | 54 | public function getSize() 55 | { 56 | return strlen($this->buffer); 57 | } 58 | 59 | public function isReadable() 60 | { 61 | return true; 62 | } 63 | 64 | public function isWritable() 65 | { 66 | return true; 67 | } 68 | 69 | public function isSeekable() 70 | { 71 | return false; 72 | } 73 | 74 | public function rewind() 75 | { 76 | $this->seek(0); 77 | } 78 | 79 | public function seek($offset, $whence = SEEK_SET) 80 | { 81 | throw new \RuntimeException('Cannot seek a BufferStream'); 82 | } 83 | 84 | public function eof() 85 | { 86 | return strlen($this->buffer) === 0; 87 | } 88 | 89 | public function tell() 90 | { 91 | throw new \RuntimeException('Cannot determine the position of a BufferStream'); 92 | } 93 | 94 | /** 95 | * Reads data from the buffer. 96 | */ 97 | public function read($length) 98 | { 99 | $currentLength = strlen($this->buffer); 100 | 101 | if ($length >= $currentLength) { 102 | // No need to slice the buffer because we don't have enough data. 103 | $result = $this->buffer; 104 | $this->buffer = ''; 105 | } else { 106 | // Slice up the result to provide a subset of the buffer. 107 | $result = substr($this->buffer, 0, $length); 108 | $this->buffer = substr($this->buffer, $length); 109 | } 110 | 111 | return $result; 112 | } 113 | 114 | /** 115 | * Writes data to the buffer. 116 | */ 117 | public function write($string) 118 | { 119 | $this->buffer .= $string; 120 | 121 | // TODO: What should happen here? 122 | if (strlen($this->buffer) >= $this->hwm) { 123 | return false; 124 | } 125 | 126 | return strlen($string); 127 | } 128 | 129 | public function getMetadata($key = null) 130 | { 131 | if ($key == 'hwm') { 132 | return $this->hwm; 133 | } 134 | 135 | return $key ? null : array(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/CachingStream.php: -------------------------------------------------------------------------------- 1 | remoteStream = $stream; 30 | parent::__construct($target ?: new Stream(fopen('php://temp', 'r+'))); 31 | } 32 | 33 | public function getSize() 34 | { 35 | return max($this->stream->getSize(), $this->remoteStream->getSize()); 36 | } 37 | 38 | public function rewind() 39 | { 40 | $this->seek(0); 41 | } 42 | 43 | public function seek($offset, $whence = SEEK_SET) 44 | { 45 | if ($whence == SEEK_SET) { 46 | $byte = $offset; 47 | } elseif ($whence == SEEK_CUR) { 48 | $byte = $offset + $this->tell(); 49 | } elseif ($whence == SEEK_END) { 50 | $size = $this->remoteStream->getSize(); 51 | if ($size === null) { 52 | $size = $this->cacheEntireStream(); 53 | } 54 | // Because 0 is the first byte, we seek to size - 1. 55 | $byte = $size - 1 - $offset; 56 | } else { 57 | throw new \InvalidArgumentException('Invalid whence'); 58 | } 59 | 60 | $diff = $byte - $this->stream->getSize(); 61 | 62 | if ($diff > 0) { 63 | // If the seek byte is greater the number of read bytes, then read 64 | // the difference of bytes to cache the bytes and inherently seek. 65 | $this->read($diff); 66 | } else { 67 | // We can just do a normal seek since we've already seen this byte. 68 | $this->stream->seek($byte); 69 | } 70 | } 71 | 72 | public function read($length) 73 | { 74 | // Perform a regular read on any previously read data from the buffer 75 | $data = $this->stream->read($length); 76 | $remaining = $length - strlen($data); 77 | 78 | // More data was requested so read from the remote stream 79 | if ($remaining) { 80 | // If data was written to the buffer in a position that would have 81 | // been filled from the remote stream, then we must skip bytes on 82 | // the remote stream to emulate overwriting bytes from that 83 | // position. This mimics the behavior of other PHP stream wrappers. 84 | $remoteData = $this->remoteStream->read( 85 | $remaining + $this->skipReadBytes 86 | ); 87 | 88 | if ($this->skipReadBytes) { 89 | $len = strlen($remoteData); 90 | $remoteData = substr($remoteData, $this->skipReadBytes); 91 | $this->skipReadBytes = max(0, $this->skipReadBytes - $len); 92 | } 93 | 94 | $data .= $remoteData; 95 | $this->stream->write($remoteData); 96 | } 97 | 98 | return $data; 99 | } 100 | 101 | public function write($string) 102 | { 103 | // When appending to the end of the currently read stream, you'll want 104 | // to skip bytes from being read from the remote stream to emulate 105 | // other stream wrappers. Basically replacing bytes of data of a fixed 106 | // length. 107 | $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); 108 | if ($overflow > 0) { 109 | $this->skipReadBytes += $overflow; 110 | } 111 | 112 | return $this->stream->write($string); 113 | } 114 | 115 | public function eof() 116 | { 117 | return $this->stream->eof() && $this->remoteStream->eof(); 118 | } 119 | 120 | /** 121 | * Close both the remote stream and buffer stream 122 | */ 123 | public function close() 124 | { 125 | $this->remoteStream->close() && $this->stream->close(); 126 | } 127 | 128 | private function cacheEntireStream() 129 | { 130 | $target = new FnStream(array('write' => 'strlen')); 131 | copy_to_stream($this, $target); 132 | 133 | return $this->tell(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/DroppingStream.php: -------------------------------------------------------------------------------- 1 | maxLength = $maxLength; 23 | } 24 | 25 | public function write($string) 26 | { 27 | $diff = $this->maxLength - $this->stream->getSize(); 28 | 29 | // Begin returning 0 when the underlying stream is too large. 30 | if ($diff <= 0) { 31 | return 0; 32 | } 33 | 34 | // Write the stream or a subset of the stream if needed. 35 | if (strlen($string) < $diff) { 36 | return $this->stream->write($string); 37 | } 38 | 39 | return $this->stream->write(substr($string, 0, $diff)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/FnStream.php: -------------------------------------------------------------------------------- 1 | methods = $methods; 42 | 43 | // Create the functions on the class 44 | foreach ($methods as $name => $fn) { 45 | $this->{'_fn_' . $name} = $fn; 46 | } 47 | } 48 | 49 | /** 50 | * Lazily determine which methods are not implemented. 51 | * @throws \BadMethodCallException 52 | */ 53 | public function __get($name) 54 | { 55 | throw new \BadMethodCallException(str_replace('_fn_', '', $name) 56 | . '() is not implemented in the FnStream'); 57 | } 58 | 59 | /** 60 | * The close method is called on the underlying stream only if possible. 61 | */ 62 | public function __destruct() 63 | { 64 | if (isset($this->_fn_close)) { 65 | call_user_func($this->_fn_close); 66 | } 67 | } 68 | 69 | /** 70 | * Adds custom functionality to an underlying stream by intercepting 71 | * specific method calls. 72 | * 73 | * @param StreamInterface $stream Stream to decorate 74 | * @param array $methods Hash of method name to a closure 75 | * 76 | * @return FnStream 77 | */ 78 | public static function decorate(StreamInterface $stream, array $methods) 79 | { 80 | // If any of the required methods were not provided, then simply 81 | // proxy to the decorated stream. 82 | foreach (array_diff(self::$slots, array_keys($methods)) as $diff) { 83 | $methods[$diff] = array($stream, $diff); 84 | } 85 | 86 | return new self($methods); 87 | } 88 | 89 | public function __toString() 90 | { 91 | return call_user_func($this->_fn___toString); 92 | } 93 | 94 | public function close() 95 | { 96 | return call_user_func($this->_fn_close); 97 | } 98 | 99 | public function detach() 100 | { 101 | return call_user_func($this->_fn_detach); 102 | } 103 | 104 | public function getSize() 105 | { 106 | return call_user_func($this->_fn_getSize); 107 | } 108 | 109 | public function tell() 110 | { 111 | return call_user_func($this->_fn_tell); 112 | } 113 | 114 | public function eof() 115 | { 116 | return call_user_func($this->_fn_eof); 117 | } 118 | 119 | public function isSeekable() 120 | { 121 | return call_user_func($this->_fn_isSeekable); 122 | } 123 | 124 | public function rewind() 125 | { 126 | call_user_func($this->_fn_rewind); 127 | } 128 | 129 | public function seek($offset, $whence = SEEK_SET) 130 | { 131 | call_user_func($this->_fn_seek, $offset, $whence); 132 | } 133 | 134 | public function isWritable() 135 | { 136 | return call_user_func($this->_fn_isWritable); 137 | } 138 | 139 | public function write($string) 140 | { 141 | return call_user_func($this->_fn_write, $string); 142 | } 143 | 144 | public function isReadable() 145 | { 146 | return call_user_func($this->_fn_isReadable); 147 | } 148 | 149 | public function read($length) 150 | { 151 | return call_user_func($this->_fn_read, $length); 152 | } 153 | 154 | public function getContents() 155 | { 156 | return call_user_func($this->_fn_getContents); 157 | } 158 | 159 | public function getMetadata($key = null) 160 | { 161 | return call_user_func($this->_fn_getMetadata, $key); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/InflateStream.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 26 | $this->mode = $mode; 27 | parent::__construct(); 28 | } 29 | 30 | /** 31 | * Creates the underlying stream lazily when required. 32 | * 33 | * @return StreamInterface 34 | */ 35 | protected function createStream() 36 | { 37 | return stream_for(try_fopen($this->filename, $this->mode)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/LimitStream.php: -------------------------------------------------------------------------------- 1 | setLimit($limit); 33 | $this->setOffset($offset); 34 | } 35 | 36 | public function eof() 37 | { 38 | // Always return true if the underlying stream is EOF 39 | if ($this->stream->eof()) { 40 | return true; 41 | } 42 | 43 | // No limit and the underlying stream is not at EOF 44 | if ($this->limit == -1) { 45 | return false; 46 | } 47 | 48 | return $this->stream->tell() >= $this->offset + $this->limit; 49 | } 50 | 51 | /** 52 | * Returns the size of the limited subset of data 53 | * {@inheritdoc} 54 | */ 55 | public function getSize() 56 | { 57 | if (null === ($length = $this->stream->getSize())) { 58 | return null; 59 | } elseif ($this->limit == -1) { 60 | return $length - $this->offset; 61 | } else { 62 | return min($this->limit, $length - $this->offset); 63 | } 64 | } 65 | 66 | /** 67 | * Allow for a bounded seek on the read limited stream 68 | * {@inheritdoc} 69 | */ 70 | public function seek($offset, $whence = SEEK_SET) 71 | { 72 | if ($whence !== SEEK_SET || $offset < 0) { 73 | throw new \RuntimeException(sprintf( 74 | 'Cannot seek to offset % with whence %s', 75 | $offset, 76 | $whence 77 | )); 78 | } 79 | 80 | $offset += $this->offset; 81 | 82 | if ($this->limit !== -1) { 83 | if ($offset > $this->offset + $this->limit) { 84 | $offset = $this->offset + $this->limit; 85 | } 86 | } 87 | 88 | $this->stream->seek($offset); 89 | } 90 | 91 | /** 92 | * Give a relative tell() 93 | * {@inheritdoc} 94 | */ 95 | public function tell() 96 | { 97 | return $this->stream->tell() - $this->offset; 98 | } 99 | 100 | /** 101 | * Set the offset to start limiting from 102 | * 103 | * @param int $offset Offset to seek to and begin byte limiting from 104 | * 105 | * @throws \RuntimeException if the stream cannot be seeked. 106 | */ 107 | public function setOffset($offset) 108 | { 109 | $current = $this->stream->tell(); 110 | 111 | if ($current !== $offset) { 112 | // If the stream cannot seek to the offset position, then read to it 113 | if ($this->stream->isSeekable()) { 114 | $this->stream->seek($offset); 115 | } elseif ($current > $offset) { 116 | throw new \RuntimeException("Could not seek to stream offset $offset"); 117 | } else { 118 | $this->stream->read($offset - $current); 119 | } 120 | } 121 | 122 | $this->offset = $offset; 123 | } 124 | 125 | /** 126 | * Set the limit of bytes that the decorator allows to be read from the 127 | * stream. 128 | * 129 | * @param int $limit Number of bytes to allow to be read from the stream. 130 | * Use -1 for no limit. 131 | */ 132 | public function setLimit($limit) 133 | { 134 | $this->limit = $limit; 135 | } 136 | 137 | public function read($length) 138 | { 139 | if ($this->limit == -1) { 140 | return $this->stream->read($length); 141 | } 142 | 143 | // Check if the current position is less than the total allowed 144 | // bytes + original offset 145 | $remaining = ($this->offset + $this->limit) - $this->stream->tell(); 146 | if ($remaining > 0) { 147 | // Only return the amount of requested data, ensuring that the byte 148 | // limit is not exceeded 149 | return $this->stream->read(min($remaining, $length)); 150 | } 151 | 152 | return ''; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/MessageTrait.php: -------------------------------------------------------------------------------- 1 | protocol; 26 | } 27 | 28 | public function withProtocolVersion($version) 29 | { 30 | if ($this->protocol === $version) { 31 | return $this; 32 | } 33 | 34 | $new = clone $this; 35 | $new->protocol = $version; 36 | return $new; 37 | } 38 | 39 | public function getHeaders() 40 | { 41 | return $this->headerLines; 42 | } 43 | 44 | public function hasHeader($header) 45 | { 46 | return isset($this->headers[strtolower($header)]); 47 | } 48 | 49 | public function getHeader($header) 50 | { 51 | $name = strtolower($header); 52 | return isset($this->headers[$name]) ? $this->headers[$name] : array(); 53 | } 54 | 55 | public function getHeaderLine($header) 56 | { 57 | return implode(', ', $this->getHeader($header)); 58 | } 59 | 60 | public function withHeader($header, $value) 61 | { 62 | $new = clone $this; 63 | $header = trim($header); 64 | $name = strtolower($header); 65 | 66 | if (!is_array($value)) { 67 | $new->headers[$name] = array(trim($value)); 68 | } else { 69 | $new->headers[$name] = $value; 70 | foreach ($new->headers[$name] as &$v) { 71 | $v = trim($v); 72 | } 73 | } 74 | 75 | // Remove the header lines. 76 | foreach (array_keys($new->headerLines) as $key) { 77 | if (strtolower($key) === $name) { 78 | unset($new->headerLines[$key]); 79 | } 80 | } 81 | 82 | // Add the header line. 83 | $new->headerLines[$header] = $new->headers[$name]; 84 | 85 | return $new; 86 | } 87 | 88 | public function withAddedHeader($header, $value) 89 | { 90 | if (!$this->hasHeader($header)) { 91 | return $this->withHeader($header, $value); 92 | } 93 | 94 | $header = trim($header); 95 | $name = strtolower($header); 96 | 97 | $value = (array) $value; 98 | foreach ($value as &$v) { 99 | $v = trim($v); 100 | } 101 | 102 | $new = clone $this; 103 | $new->headers[$name] = array_merge($new->headers[$name], $value); 104 | $new->headerLines[$header] = array_merge($new->headerLines[$header], $value); 105 | 106 | return $new; 107 | } 108 | 109 | public function withoutHeader($header) 110 | { 111 | if (!$this->hasHeader($header)) { 112 | return $this; 113 | } 114 | 115 | $new = clone $this; 116 | $name = strtolower($header); 117 | unset($new->headers[$name]); 118 | 119 | foreach (array_keys($new->headerLines) as $key) { 120 | if (strtolower($key) === $name) { 121 | unset($new->headerLines[$key]); 122 | } 123 | } 124 | 125 | return $new; 126 | } 127 | 128 | public function getBody() 129 | { 130 | if (!$this->stream) { 131 | $this->stream = stream_for(''); 132 | } 133 | 134 | return $this->stream; 135 | } 136 | 137 | public function withBody(StreamInterface $body) 138 | { 139 | if ($body === $this->stream) { 140 | return $this; 141 | } 142 | 143 | $new = clone $this; 144 | $new->stream = $body; 145 | return $new; 146 | } 147 | 148 | protected function setHeaders(array $headers) 149 | { 150 | $this->headerLines = $this->headers = array(); 151 | foreach ($headers as $header => $value) { 152 | $header = trim($header); 153 | $name = strtolower($header); 154 | if (!is_array($value)) { 155 | $value = trim($value); 156 | $this->headers[$name][] = $value; 157 | $this->headerLines[$header][] = $value; 158 | } else { 159 | foreach ($value as $v) { 160 | $v = trim($v); 161 | $this->headers[$name][] = $v; 162 | $this->headerLines[$header][] = $v; 163 | } 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/MultipartStream.php: -------------------------------------------------------------------------------- 1 | boundary = $boundary ?: uniqid(); 30 | parent::__construct($this->createStream($elements)); 31 | } 32 | 33 | /** 34 | * Get the boundary 35 | * 36 | * @return string 37 | */ 38 | public function getBoundary() 39 | { 40 | return $this->boundary; 41 | } 42 | 43 | public function isWritable() 44 | { 45 | return false; 46 | } 47 | 48 | /** 49 | * Get the headers needed before transferring the content of a POST file 50 | */ 51 | private function getHeaders(array $headers) 52 | { 53 | $str = ''; 54 | foreach ($headers as $key => $value) { 55 | $str .= "{$key}: {$value}\r\n"; 56 | } 57 | 58 | return "--{$this->boundary}\r\n" . trim($str) . "\r\n\r\n"; 59 | } 60 | 61 | /** 62 | * Create the aggregate stream that will be used to upload the POST data 63 | */ 64 | protected function createStream(array $elements) 65 | { 66 | $stream = new AppendStream(); 67 | 68 | foreach ($elements as $element) { 69 | $this->addElement($stream, $element); 70 | } 71 | 72 | // Add the trailing boundary with CRLF 73 | $stream->addStream(stream_for("--{$this->boundary}--\r\n")); 74 | 75 | return $stream; 76 | } 77 | 78 | private function addElement(AppendStream $stream, array $element) 79 | { 80 | foreach (array('contents', 'name') as $key) { 81 | if (!array_key_exists($key, $element)) { 82 | throw new \InvalidArgumentException("A '{$key}' key is required"); 83 | } 84 | } 85 | 86 | $element['contents'] = stream_for($element['contents']); 87 | 88 | if (empty($element['filename'])) { 89 | $uri = $element['contents']->getMetadata('uri'); 90 | if (substr($uri, 0, 6) !== 'php://') { 91 | $element['filename'] = $uri; 92 | } 93 | } 94 | 95 | list($body, $headers) = $this->createElement( 96 | $element['name'], 97 | $element['contents'], 98 | isset($element['filename']) ? $element['filename'] : null, 99 | isset($element['headers']) ? $element['headers'] : array() 100 | ); 101 | 102 | $stream->addStream(stream_for($this->getHeaders($headers))); 103 | $stream->addStream($body); 104 | $stream->addStream(stream_for("\r\n")); 105 | } 106 | 107 | /** 108 | * @return array 109 | */ 110 | private function createElement($name, $stream, $filename, array $headers) 111 | { 112 | // Set a default content-disposition header if one was no provided 113 | $disposition = $this->getHeader($headers, 'content-disposition'); 114 | if (!$disposition) { 115 | $headers['Content-Disposition'] = $filename 116 | ? sprintf('form-data; name="%s"; filename="%s"', 117 | $name, 118 | basename($filename)) 119 | : "form-data; name=\"{$name}\""; 120 | } 121 | 122 | // Set a default content-length header if one was no provided 123 | $length = $this->getHeader($headers, 'content-length'); 124 | if (!$length) { 125 | if ($length = $stream->getSize()) { 126 | $headers['Content-Length'] = (string) $length; 127 | } 128 | } 129 | 130 | // Set a default Content-Type if one was not supplied 131 | $type = $this->getHeader($headers, 'content-type'); 132 | if (!$type && $filename) { 133 | if ($type = mimetype_from_filename($filename)) { 134 | $headers['Content-Type'] = $type; 135 | } 136 | } 137 | 138 | return array($stream, $headers); 139 | } 140 | 141 | private function getHeader(array $headers, $key) 142 | { 143 | $lowercaseHeader = strtolower($key); 144 | foreach ($headers as $k => $v) { 145 | if (strtolower($k) === $lowercaseHeader) { 146 | return $v; 147 | } 148 | } 149 | 150 | return null; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/NoSeekStream.php: -------------------------------------------------------------------------------- 1 | source = $source; 46 | $this->size = isset($options['size']) ? $options['size'] : null; 47 | $this->metadata = isset($options['metadata']) ? $options['metadata'] : array(); 48 | $this->buffer = new BufferStream(); 49 | } 50 | 51 | public function __toString() 52 | { 53 | try { 54 | return copy_to_string($this); 55 | } catch (\Exception $e) { 56 | return ''; 57 | } 58 | } 59 | 60 | public function close() 61 | { 62 | $this->detach(); 63 | } 64 | 65 | public function detach() 66 | { 67 | $this->tellPos = false; 68 | $this->source = null; 69 | } 70 | 71 | public function getSize() 72 | { 73 | return $this->size; 74 | } 75 | 76 | public function tell() 77 | { 78 | return $this->tellPos; 79 | } 80 | 81 | public function eof() 82 | { 83 | return !$this->source; 84 | } 85 | 86 | public function isSeekable() 87 | { 88 | return false; 89 | } 90 | 91 | public function rewind() 92 | { 93 | $this->seek(0); 94 | } 95 | 96 | public function seek($offset, $whence = SEEK_SET) 97 | { 98 | throw new \RuntimeException('Cannot seek a PumpStream'); 99 | } 100 | 101 | public function isWritable() 102 | { 103 | return false; 104 | } 105 | 106 | public function write($string) 107 | { 108 | throw new \RuntimeException('Cannot write to a PumpStream'); 109 | } 110 | 111 | public function isReadable() 112 | { 113 | return true; 114 | } 115 | 116 | public function read($length) 117 | { 118 | $data = $this->buffer->read($length); 119 | $readLen = strlen($data); 120 | $this->tellPos += $readLen; 121 | $remaining = $length - $readLen; 122 | 123 | if ($remaining) { 124 | $this->pump($remaining); 125 | $data .= $this->buffer->read($remaining); 126 | $this->tellPos += strlen($data) - $readLen; 127 | } 128 | 129 | return $data; 130 | } 131 | 132 | public function getContents() 133 | { 134 | $result = ''; 135 | while (!$this->eof()) { 136 | $result .= $this->read(1000000); 137 | } 138 | 139 | return $result; 140 | } 141 | 142 | public function getMetadata($key = null) 143 | { 144 | if (!$key) { 145 | return $this->metadata; 146 | } 147 | 148 | return isset($this->metadata[$key]) ? $this->metadata[$key] : null; 149 | } 150 | 151 | private function pump($length) 152 | { 153 | if ($this->source) { 154 | do { 155 | $data = call_user_func($this->source, $length); 156 | if ($data === false || $data === null) { 157 | $this->source = null; 158 | return; 159 | } 160 | $this->buffer->write($data); 161 | $length -= strlen($data); 162 | } while ($length > 0); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | method = strtoupper($method); 49 | $this->uri = $uri; 50 | $this->setHeaders($headers); 51 | $this->protocol = $protocolVersion; 52 | 53 | $host = $uri->getHost(); 54 | if ($host && !$this->hasHeader('Host')) { 55 | $this->updateHostFromUri($host); 56 | } 57 | 58 | if ($body) { 59 | $this->stream = stream_for($body); 60 | } 61 | } 62 | 63 | public function getRequestTarget() 64 | { 65 | if ($this->requestTarget !== null) { 66 | return $this->requestTarget; 67 | } 68 | 69 | $target = $this->uri->getPath(); 70 | if ($target == null) { 71 | $target = '/'; 72 | } 73 | if ($this->uri->getQuery()) { 74 | $target .= '?' . $this->uri->getQuery(); 75 | } 76 | 77 | return $target; 78 | } 79 | 80 | public function withRequestTarget($requestTarget) 81 | { 82 | if (preg_match('#\s#', $requestTarget)) { 83 | throw new InvalidArgumentException( 84 | 'Invalid request target provided; cannot contain whitespace' 85 | ); 86 | } 87 | 88 | $new = clone $this; 89 | $new->requestTarget = $requestTarget; 90 | return $new; 91 | } 92 | 93 | public function getMethod() 94 | { 95 | return $this->method; 96 | } 97 | 98 | public function withMethod($method) 99 | { 100 | $new = clone $this; 101 | $new->method = strtoupper($method); 102 | return $new; 103 | } 104 | 105 | public function getUri() 106 | { 107 | return $this->uri; 108 | } 109 | 110 | public function withUri(UriInterface $uri, $preserveHost = false) 111 | { 112 | if ($uri === $this->uri) { 113 | return $this; 114 | } 115 | 116 | $new = clone $this; 117 | $new->uri = $uri; 118 | 119 | if (!$preserveHost) { 120 | if ($host = $uri->getHost()) { 121 | $new->updateHostFromUri($host); 122 | } 123 | } 124 | 125 | return $new; 126 | } 127 | 128 | public function withHeader($header, $value) 129 | { 130 | /** @var Request $newInstance */ 131 | $newInstance = parent::withHeader($header, $value); 132 | return $newInstance; 133 | } 134 | 135 | private function updateHostFromUri($host) 136 | { 137 | // Ensure Host is the first header. 138 | // See: http://tools.ietf.org/html/rfc7230#section-5.4 139 | if ($port = $this->uri->getPort()) { 140 | $host .= ':' . $port; 141 | } 142 | 143 | $this->headerLines = array('Host' => array($host)) + $this->headerLines; 144 | $this->headers = array('host' => array($host)) + $this->headers; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 'Continue', 15 | 101 => 'Switching Protocols', 16 | 102 => 'Processing', 17 | 200 => 'OK', 18 | 201 => 'Created', 19 | 202 => 'Accepted', 20 | 203 => 'Non-Authoritative Information', 21 | 204 => 'No Content', 22 | 205 => 'Reset Content', 23 | 206 => 'Partial Content', 24 | 207 => 'Multi-status', 25 | 208 => 'Already Reported', 26 | 300 => 'Multiple Choices', 27 | 301 => 'Moved Permanently', 28 | 302 => 'Found', 29 | 303 => 'See Other', 30 | 304 => 'Not Modified', 31 | 305 => 'Use Proxy', 32 | 306 => 'Switch Proxy', 33 | 307 => 'Temporary Redirect', 34 | 400 => 'Bad Request', 35 | 401 => 'Unauthorized', 36 | 402 => 'Payment Required', 37 | 403 => 'Forbidden', 38 | 404 => 'Not Found', 39 | 405 => 'Method Not Allowed', 40 | 406 => 'Not Acceptable', 41 | 407 => 'Proxy Authentication Required', 42 | 408 => 'Request Time-out', 43 | 409 => 'Conflict', 44 | 410 => 'Gone', 45 | 411 => 'Length Required', 46 | 412 => 'Precondition Failed', 47 | 413 => 'Request Entity Too Large', 48 | 414 => 'Request-URI Too Large', 49 | 415 => 'Unsupported Media Type', 50 | 416 => 'Requested range not satisfiable', 51 | 417 => 'Expectation Failed', 52 | 418 => 'I\'m a teapot', 53 | 422 => 'Unprocessable Entity', 54 | 423 => 'Locked', 55 | 424 => 'Failed Dependency', 56 | 425 => 'Unordered Collection', 57 | 426 => 'Upgrade Required', 58 | 428 => 'Precondition Required', 59 | 429 => 'Too Many Requests', 60 | 431 => 'Request Header Fields Too Large', 61 | 500 => 'Internal Server Error', 62 | 501 => 'Not Implemented', 63 | 502 => 'Bad Gateway', 64 | 503 => 'Service Unavailable', 65 | 504 => 'Gateway Time-out', 66 | 505 => 'HTTP Version not supported', 67 | 506 => 'Variant Also Negotiates', 68 | 507 => 'Insufficient Storage', 69 | 508 => 'Loop Detected', 70 | 511 => 'Network Authentication Required', 71 | ); 72 | 73 | /** @var null|string */ 74 | private $reasonPhrase = ''; 75 | 76 | /** @var int */ 77 | private $statusCode = 200; 78 | 79 | /** 80 | * @param int $status Status code for the response, if any. 81 | * @param array $headers Headers for the response, if any. 82 | * @param mixed $body Stream body. 83 | * @param string $version Protocol version. 84 | * @param string $reason Reason phrase (a default will be used if possible). 85 | */ 86 | public function __construct( 87 | $status = 200, 88 | array $headers = array(), 89 | $body = null, 90 | $version = '1.1', 91 | $reason = null 92 | ) { 93 | $this->statusCode = (int) $status; 94 | 95 | if ($body !== null) { 96 | $this->stream = stream_for($body); 97 | } 98 | 99 | $this->setHeaders($headers); 100 | if (!$reason && isset(self::$phrases[$this->statusCode])) { 101 | $this->reasonPhrase = self::$phrases[$status]; 102 | } else { 103 | $this->reasonPhrase = (string) $reason; 104 | } 105 | 106 | $this->protocol = $version; 107 | } 108 | 109 | public function getStatusCode() 110 | { 111 | return $this->statusCode; 112 | } 113 | 114 | public function getReasonPhrase() 115 | { 116 | return $this->reasonPhrase; 117 | } 118 | 119 | public function withStatus($code, $reasonPhrase = '') 120 | { 121 | $new = clone $this; 122 | $new->statusCode = (int) $code; 123 | if (!$reasonPhrase && isset(self::$phrases[$new->statusCode])) { 124 | $reasonPhrase = self::$phrases[$new->statusCode]; 125 | } 126 | $new->reasonPhrase = $reasonPhrase; 127 | return $new; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/ServerRequest.php: -------------------------------------------------------------------------------- 1 | serverParams = $serverParams; 41 | } 42 | 43 | public function getServerParams() 44 | { 45 | return $this->serverParams; 46 | } 47 | 48 | public function getCookieParams() 49 | { 50 | return $this->cookies; 51 | } 52 | 53 | public function withCookieParams(array $cookies) 54 | { 55 | $new = clone $this; 56 | $new->cookies = $cookies; 57 | return $new; 58 | } 59 | 60 | public function getQueryParams() 61 | { 62 | return $this->queryParams; 63 | } 64 | 65 | public function withQueryParams(array $query) 66 | { 67 | $new = clone $this; 68 | $new->queryParams = $query; 69 | return $new; 70 | } 71 | 72 | public function getUploadedFiles() 73 | { 74 | return $this->fileParams; 75 | } 76 | 77 | public function withUploadedFiles(array $uploadedFiles) 78 | { 79 | $new = clone $this; 80 | $new->fileParams = $uploadedFiles; 81 | return $new; 82 | } 83 | 84 | public function getParsedBody() 85 | { 86 | return $this->parsedBody; 87 | } 88 | 89 | public function withParsedBody($data) 90 | { 91 | $new = clone $this; 92 | $new->parsedBody = $data; 93 | return $new; 94 | } 95 | 96 | public function getAttributes() 97 | { 98 | return $this->attributes; 99 | } 100 | 101 | public function getAttribute($name, $default = null) 102 | { 103 | if (!array_key_exists($name, $this->attributes)) { 104 | return $default; 105 | } 106 | return $this->attributes[$name]; 107 | } 108 | 109 | public function withAttribute($name, $value) 110 | { 111 | $new = clone $this; 112 | $new->attributes[$name] = $value; 113 | return $new; 114 | } 115 | 116 | public function withoutAttribute($name) 117 | { 118 | $new = clone $this; 119 | unset($new->attributes[$name]); 120 | return $new; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Stream.php: -------------------------------------------------------------------------------- 1 | array( 24 | 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, 25 | 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, 26 | 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, 27 | 'x+t' => true, 'c+t' => true, 'a+' => true 28 | ), 29 | 'write' => array( 30 | 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, 31 | 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, 32 | 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, 33 | 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true 34 | ) 35 | ); 36 | 37 | /** 38 | * This constructor accepts an associative array of options. 39 | * 40 | * - size: (int) If a read stream would otherwise have an indeterminate 41 | * size, but the size is known due to foreknownledge, then you can 42 | * provide that size, in bytes. 43 | * - metadata: (array) Any additional metadata to return when the metadata 44 | * of the stream is accessed. 45 | * 46 | * @param resource $stream Stream resource to wrap. 47 | * @param array $options Associative array of options. 48 | * 49 | * @throws \InvalidArgumentException if the stream is not a stream resource 50 | */ 51 | public function __construct($stream, $options = array()) 52 | { 53 | if (!is_resource($stream)) { 54 | throw new \InvalidArgumentException('Stream must be a resource'); 55 | } 56 | 57 | if (isset($options['size'])) { 58 | $this->size = $options['size']; 59 | } 60 | 61 | $this->customMetadata = isset($options['metadata']) 62 | ? $options['metadata'] 63 | : array(); 64 | 65 | $this->stream = $stream; 66 | $meta = stream_get_meta_data($this->stream); 67 | $this->seekable = $meta['seekable']; 68 | $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]); 69 | $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]); 70 | $this->uri = $this->getMetadata('uri'); 71 | } 72 | 73 | public function __get($name) 74 | { 75 | if ($name == 'stream') { 76 | throw new \RuntimeException('The stream is detached'); 77 | } 78 | 79 | throw new \BadMethodCallException('No value for ' . $name); 80 | } 81 | 82 | /** 83 | * Closes the stream when the destructed 84 | */ 85 | public function __destruct() 86 | { 87 | $this->close(); 88 | } 89 | 90 | public function __toString() 91 | { 92 | try { 93 | $this->seek(0); 94 | return (string) stream_get_contents($this->stream); 95 | } catch (\Exception $e) { 96 | return ''; 97 | } 98 | } 99 | 100 | public function getContents() 101 | { 102 | $contents = stream_get_contents($this->stream); 103 | 104 | if ($contents === false) { 105 | throw new \RuntimeException('Unable to read stream contents'); 106 | } 107 | 108 | return $contents; 109 | } 110 | 111 | public function close() 112 | { 113 | if (isset($this->stream)) { 114 | if (is_resource($this->stream)) { 115 | fclose($this->stream); 116 | } 117 | $this->detach(); 118 | } 119 | } 120 | 121 | public function detach() 122 | { 123 | if (!isset($this->stream)) { 124 | return null; 125 | } 126 | 127 | $result = $this->stream; 128 | unset($this->stream); 129 | $this->size = $this->uri = null; 130 | $this->readable = $this->writable = $this->seekable = false; 131 | 132 | return $result; 133 | } 134 | 135 | public function getSize() 136 | { 137 | if ($this->size !== null) { 138 | return $this->size; 139 | } 140 | 141 | if (!isset($this->stream)) { 142 | return null; 143 | } 144 | 145 | // Clear the stat cache if the stream has a URI 146 | if ($this->uri) { 147 | clearstatcache(true, $this->uri); 148 | } 149 | 150 | $stats = fstat($this->stream); 151 | if (isset($stats['size'])) { 152 | $this->size = $stats['size']; 153 | return $this->size; 154 | } 155 | 156 | return null; 157 | } 158 | 159 | public function isReadable() 160 | { 161 | return $this->readable; 162 | } 163 | 164 | public function isWritable() 165 | { 166 | return $this->writable; 167 | } 168 | 169 | public function isSeekable() 170 | { 171 | return $this->seekable; 172 | } 173 | 174 | public function eof() 175 | { 176 | return !$this->stream || feof($this->stream); 177 | } 178 | 179 | public function tell() 180 | { 181 | $result = ftell($this->stream); 182 | 183 | if ($result === false) { 184 | throw new \RuntimeException('Unable to determine stream position'); 185 | } 186 | 187 | return $result; 188 | } 189 | 190 | public function rewind() 191 | { 192 | $this->seek(0); 193 | } 194 | 195 | public function seek($offset, $whence = SEEK_SET) 196 | { 197 | if (!$this->seekable) { 198 | throw new \RuntimeException('Stream is not seekable'); 199 | } elseif (fseek($this->stream, $offset, $whence) === -1) { 200 | throw new \RuntimeException('Unable to seek to stream position ' 201 | . $offset . ' with whence ' . var_export($whence, true)); 202 | } 203 | } 204 | 205 | public function read($length) 206 | { 207 | if (!$this->readable) { 208 | throw new \RuntimeException('Cannot read from non-readable stream'); 209 | } 210 | 211 | return fread($this->stream, $length); 212 | } 213 | 214 | public function write($string) 215 | { 216 | if (!$this->writable) { 217 | throw new \RuntimeException('Cannot write to a non-writable stream'); 218 | } 219 | 220 | // We can't know the size after writing anything 221 | $this->size = null; 222 | $result = fwrite($this->stream, $string); 223 | 224 | if ($result === false) { 225 | throw new \RuntimeException('Unable to write to stream'); 226 | } 227 | 228 | return $result; 229 | } 230 | 231 | public function getMetadata($key = null) 232 | { 233 | if (!isset($this->stream)) { 234 | return $key ? null : array(); 235 | } elseif (!$key) { 236 | return $this->customMetadata + stream_get_meta_data($this->stream); 237 | } elseif (isset($this->customMetadata[$key])) { 238 | return $this->customMetadata[$key]; 239 | } 240 | 241 | $meta = stream_get_meta_data($this->stream); 242 | 243 | return isset($meta[$key]) ? $meta[$key] : null; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/StreamDecoratorTrait.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 18 | } 19 | 20 | /** 21 | * Magic method used to create a new stream if streams are not added in 22 | * the constructor of a decorator (e.g., LazyOpenStream). 23 | * 24 | * @param string $name Name of the property (allows "stream" only). 25 | * 26 | * @return StreamInterface 27 | */ 28 | public function __get($name) 29 | { 30 | if ($name == 'stream') { 31 | $this->stream = $this->createStream(); 32 | return $this->stream; 33 | } 34 | 35 | throw new \UnexpectedValueException("$name not found on class"); 36 | } 37 | 38 | public function __toString() 39 | { 40 | try { 41 | if ($this->isSeekable()) { 42 | $this->seek(0); 43 | } 44 | return $this->getContents(); 45 | } catch (\Exception $e) { 46 | // Really, PHP? https://bugs.php.net/bug.php?id=53648 47 | trigger_error('StreamDecorator::__toString exception: ' 48 | . (string) $e, E_USER_ERROR); 49 | return ''; 50 | } 51 | } 52 | 53 | public function getContents() 54 | { 55 | return copy_to_string($this); 56 | } 57 | 58 | /** 59 | * Allow decorators to implement custom methods 60 | * 61 | * @param string $method Missing method name 62 | * @param array $args Method arguments 63 | * 64 | * @return mixed 65 | */ 66 | public function __call($method, array $args) 67 | { 68 | $result = call_user_func_array(array($this->stream, $method), $args); 69 | 70 | // Always return the wrapped object if the result is a return $this 71 | return $result === $this->stream ? $this : $result; 72 | } 73 | 74 | public function close() 75 | { 76 | $this->stream->close(); 77 | } 78 | 79 | public function getMetadata($key = null) 80 | { 81 | return $this->stream->getMetadata($key); 82 | } 83 | 84 | public function detach() 85 | { 86 | return $this->stream->detach(); 87 | } 88 | 89 | public function getSize() 90 | { 91 | return $this->stream->getSize(); 92 | } 93 | 94 | public function eof() 95 | { 96 | return $this->stream->eof(); 97 | } 98 | 99 | public function tell() 100 | { 101 | return $this->stream->tell(); 102 | } 103 | 104 | public function isReadable() 105 | { 106 | return $this->stream->isReadable(); 107 | } 108 | 109 | public function isWritable() 110 | { 111 | return $this->stream->isWritable(); 112 | } 113 | 114 | public function isSeekable() 115 | { 116 | return $this->stream->isSeekable(); 117 | } 118 | 119 | public function rewind() 120 | { 121 | $this->seek(0); 122 | } 123 | 124 | public function seek($offset, $whence = SEEK_SET) 125 | { 126 | $this->stream->seek($offset, $whence); 127 | } 128 | 129 | public function read($length) 130 | { 131 | return $this->stream->read($length); 132 | } 133 | 134 | public function write($string) 135 | { 136 | return $this->stream->write($string); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/StreamWrapper.php: -------------------------------------------------------------------------------- 1 | isReadable()) { 33 | $mode = $stream->isWritable() ? 'r+' : 'r'; 34 | } elseif ($stream->isWritable()) { 35 | $mode = 'w'; 36 | } else { 37 | throw new \InvalidArgumentException('The stream must be readable, ' 38 | . 'writable, or both.'); 39 | } 40 | 41 | return fopen('guzzle://stream', $mode, null, stream_context_create(array( 42 | 'guzzle' => array('stream' => $stream) 43 | ))); 44 | } 45 | 46 | /** 47 | * Registers the stream wrapper if needed 48 | */ 49 | public static function register() 50 | { 51 | if (!in_array('guzzle', stream_get_wrappers())) { 52 | stream_wrapper_register('guzzle', __CLASS__); 53 | } 54 | } 55 | 56 | public function stream_open($path, $mode, $options, &$opened_path) 57 | { 58 | $options = stream_context_get_options($this->context); 59 | 60 | if (!isset($options['guzzle']['stream'])) { 61 | return false; 62 | } 63 | 64 | $this->mode = $mode; 65 | $this->stream = $options['guzzle']['stream']; 66 | 67 | return true; 68 | } 69 | 70 | public function stream_read($count) 71 | { 72 | return $this->stream->read($count); 73 | } 74 | 75 | public function stream_write($data) 76 | { 77 | return (int) $this->stream->write($data); 78 | } 79 | 80 | public function stream_tell() 81 | { 82 | return $this->stream->tell(); 83 | } 84 | 85 | public function stream_eof() 86 | { 87 | return $this->stream->eof(); 88 | } 89 | 90 | public function stream_seek($offset, $whence) 91 | { 92 | $this->stream->seek($offset, $whence); 93 | 94 | return true; 95 | } 96 | 97 | public function stream_stat() 98 | { 99 | static $modeMap = array( 100 | 'r' => 33060, 101 | 'r+' => 33206, 102 | 'w' => 33188 103 | ); 104 | 105 | return array( 106 | 'dev' => 0, 107 | 'ino' => 0, 108 | 'mode' => $modeMap[$this->mode], 109 | 'nlink' => 0, 110 | 'uid' => 0, 111 | 'gid' => 0, 112 | 'rdev' => 0, 113 | 'size' => $this->stream->getSize() ?: 0, 114 | 'atime' => 0, 115 | 'mtime' => 0, 116 | 'ctime' => 0, 117 | 'blksize' => 0, 118 | 'blocks' => 0 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Uri.php: -------------------------------------------------------------------------------- 1 | 80, 16 | 'https' => 443, 17 | ); 18 | 19 | private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; 20 | private static $charSubDelims = '!\$&\'\(\)\*\+,;='; 21 | private static $replaceQuery = array('=' => '%3D', '&' => '%26'); 22 | 23 | /** @var string Uri scheme. */ 24 | private $scheme = ''; 25 | 26 | /** @var string Uri user info. */ 27 | private $userInfo = ''; 28 | 29 | /** @var string Uri host. */ 30 | private $host = ''; 31 | 32 | /** @var int|null Uri port. */ 33 | private $port; 34 | 35 | /** @var string Uri path. */ 36 | private $path = ''; 37 | 38 | /** @var string Uri query string. */ 39 | private $query = ''; 40 | 41 | /** @var string Uri fragment. */ 42 | private $fragment = ''; 43 | 44 | /** 45 | * @param string $uri URI to parse and wrap. 46 | */ 47 | public function __construct($uri = '') 48 | { 49 | if ($uri != null) { 50 | $parts = parse_url($uri); 51 | if ($parts === false) { 52 | throw new \InvalidArgumentException("Unable to parse URI: $uri"); 53 | } 54 | $this->applyParts($parts); 55 | } 56 | } 57 | 58 | public function __toString() 59 | { 60 | return self::createUriString( 61 | $this->scheme, 62 | $this->getAuthority(), 63 | $this->getPath(), 64 | $this->query, 65 | $this->fragment 66 | ); 67 | } 68 | 69 | /** 70 | * Removes dot segments from a path and returns the new path. 71 | * 72 | * @param string $path 73 | * 74 | * @return string 75 | * @link http://tools.ietf.org/html/rfc3986#section-5.2.4 76 | */ 77 | public static function removeDotSegments($path) 78 | { 79 | static $noopPaths = array('' => true, '/' => true, '*' => true); 80 | static $ignoreSegments = array('.' => true, '..' => true); 81 | 82 | if (isset($noopPaths[$path])) { 83 | return $path; 84 | } 85 | 86 | $results = array(); 87 | $segments = explode('/', $path); 88 | foreach ($segments as $segment) { 89 | if ($segment == '..') { 90 | array_pop($results); 91 | } elseif (!isset($ignoreSegments[$segment])) { 92 | $results[] = $segment; 93 | } 94 | } 95 | 96 | $newPath = implode('/', $results); 97 | // Add the leading slash if necessary 98 | if (substr($path, 0, 1) === '/' && 99 | substr($newPath, 0, 1) !== '/' 100 | ) { 101 | $newPath = '/' . $newPath; 102 | } 103 | 104 | // Add the trailing slash if necessary 105 | if ($newPath != '/' && isset($ignoreSegments[end($segments)])) { 106 | $newPath .= '/'; 107 | } 108 | 109 | return $newPath; 110 | } 111 | 112 | /** 113 | * Resolve a base URI with a relative URI and return a new URI. 114 | * 115 | * @param UriInterface $base Base URI 116 | * @param string $rel Relative URI 117 | * 118 | * @return UriInterface 119 | */ 120 | public static function resolve(UriInterface $base, $rel) 121 | { 122 | if ($rel === null || $rel === '') { 123 | return $base; 124 | } 125 | 126 | if (!($rel instanceof UriInterface)) { 127 | $rel = new self($rel); 128 | } 129 | 130 | // Return the relative uri as-is if it has a scheme. 131 | if ($rel->getScheme()) { 132 | return $rel->withPath(static::removeDotSegments($rel->getPath())); 133 | } 134 | 135 | $relParts = array( 136 | 'scheme' => $rel->getScheme(), 137 | 'authority' => $rel->getAuthority(), 138 | 'path' => $rel->getPath(), 139 | 'query' => $rel->getQuery(), 140 | 'fragment' => $rel->getFragment() 141 | ); 142 | 143 | $parts = array( 144 | 'scheme' => $base->getScheme(), 145 | 'authority' => $base->getAuthority(), 146 | 'path' => $base->getPath(), 147 | 'query' => $base->getQuery(), 148 | 'fragment' => $base->getFragment() 149 | ); 150 | 151 | if (!empty($relParts['authority'])) { 152 | $parts['authority'] = $relParts['authority']; 153 | $parts['path'] = self::removeDotSegments($relParts['path']); 154 | $parts['query'] = $relParts['query']; 155 | $parts['fragment'] = $relParts['fragment']; 156 | } elseif (!empty($relParts['path'])) { 157 | if (substr($relParts['path'], 0, 1) == '/') { 158 | $parts['path'] = self::removeDotSegments($relParts['path']); 159 | $parts['query'] = $relParts['query']; 160 | $parts['fragment'] = $relParts['fragment']; 161 | } else { 162 | if (!empty($parts['authority']) && empty($parts['path'])) { 163 | $mergedPath = '/'; 164 | } else { 165 | $mergedPath = substr($parts['path'], 0, strrpos($parts['path'], '/') + 1); 166 | } 167 | $parts['path'] = self::removeDotSegments($mergedPath . $relParts['path']); 168 | $parts['query'] = $relParts['query']; 169 | $parts['fragment'] = $relParts['fragment']; 170 | } 171 | } elseif (!empty($relParts['query'])) { 172 | $parts['query'] = $relParts['query']; 173 | } elseif ($relParts['fragment'] != null) { 174 | $parts['fragment'] = $relParts['fragment']; 175 | } 176 | 177 | return new self(static::createUriString( 178 | $parts['scheme'], 179 | $parts['authority'], 180 | $parts['path'], 181 | $parts['query'], 182 | $parts['fragment'] 183 | )); 184 | } 185 | 186 | /** 187 | * Create a new URI with a specific query string value removed. 188 | * 189 | * Any existing query string values that exactly match the provided key are 190 | * removed. 191 | * 192 | * Note: this function will convert "=" to "%3D" and "&" to "%26". 193 | * 194 | * @param UriInterface $uri URI to use as a base. 195 | * @param string $key Query string key value pair to remove. 196 | * 197 | * @return UriInterface 198 | */ 199 | public static function withoutQueryValue(UriInterface $uri, $key) 200 | { 201 | $current = $uri->getQuery(); 202 | if (!$current) { 203 | return $uri; 204 | } 205 | 206 | $result = array(); 207 | foreach (explode('&', $current) as $part) { 208 | $subParts = explode('=', $part); 209 | if ($subParts[0] !== $key) { 210 | $result[] = $part; 211 | }; 212 | } 213 | 214 | return $uri->withQuery(implode('&', $result)); 215 | } 216 | 217 | /** 218 | * Create a new URI with a specific query string value. 219 | * 220 | * Any existing query string values that exactly match the provided key are 221 | * removed and replaced with the given key value pair. 222 | * 223 | * Note: this function will convert "=" to "%3D" and "&" to "%26". 224 | * 225 | * @param UriInterface $uri URI to use as a base. 226 | * @param string $key Key to set. 227 | * @param string $value Value to set. 228 | * 229 | * @return UriInterface 230 | */ 231 | public static function withQueryValue(UriInterface $uri, $key, $value) 232 | { 233 | $current = $uri->getQuery(); 234 | $key = strtr($key, self::$replaceQuery); 235 | 236 | if (!$current) { 237 | $result = array(); 238 | } else { 239 | $result = array(); 240 | foreach (explode('&', $current) as $part) { 241 | $subParts = explode('=', $part); 242 | if ($subParts[0] !== $key) { 243 | $result[] = $part; 244 | }; 245 | } 246 | } 247 | 248 | if ($value !== null) { 249 | $result[] = $key . '=' . strtr($value, self::$replaceQuery); 250 | } else { 251 | $result[] = $key; 252 | } 253 | 254 | return $uri->withQuery(implode('&', $result)); 255 | } 256 | 257 | /** 258 | * Create a URI from a hash of parse_url parts. 259 | * 260 | * @param array $parts 261 | * 262 | * @return self 263 | */ 264 | public static function fromParts(array $parts) 265 | { 266 | $uri = new self(); 267 | $uri->applyParts($parts); 268 | return $uri; 269 | } 270 | 271 | public function getScheme() 272 | { 273 | return $this->scheme; 274 | } 275 | 276 | public function getAuthority() 277 | { 278 | if (empty($this->host)) { 279 | return ''; 280 | } 281 | 282 | $authority = $this->host; 283 | if (!empty($this->userInfo)) { 284 | $authority = $this->userInfo . '@' . $authority; 285 | } 286 | 287 | if ($this->isNonStandardPort($this->scheme, $this->host, $this->port)) { 288 | $authority .= ':' . $this->port; 289 | } 290 | 291 | return $authority; 292 | } 293 | 294 | public function getUserInfo() 295 | { 296 | return $this->userInfo; 297 | } 298 | 299 | public function getHost() 300 | { 301 | return $this->host; 302 | } 303 | 304 | public function getPort() 305 | { 306 | return $this->port; 307 | } 308 | 309 | public function getPath() 310 | { 311 | return $this->path == null ? '' : $this->path; 312 | } 313 | 314 | public function getQuery() 315 | { 316 | return $this->query; 317 | } 318 | 319 | public function getFragment() 320 | { 321 | return $this->fragment; 322 | } 323 | 324 | public function withScheme($scheme) 325 | { 326 | $scheme = $this->filterScheme($scheme); 327 | 328 | if ($this->scheme === $scheme) { 329 | return $this; 330 | } 331 | 332 | $new = clone $this; 333 | $new->scheme = $scheme; 334 | $new->port = $new->filterPort($new->scheme, $new->host, $new->port); 335 | return $new; 336 | } 337 | 338 | public function withUserInfo($user, $password = null) 339 | { 340 | $info = $user; 341 | if ($password) { 342 | $info .= ':' . $password; 343 | } 344 | 345 | if ($this->userInfo === $info) { 346 | return $this; 347 | } 348 | 349 | $new = clone $this; 350 | $new->userInfo = $info; 351 | return $new; 352 | } 353 | 354 | public function withHost($host) 355 | { 356 | if ($this->host === $host) { 357 | return $this; 358 | } 359 | 360 | $new = clone $this; 361 | $new->host = $host; 362 | return $new; 363 | } 364 | 365 | public function withPort($port) 366 | { 367 | $port = $this->filterPort($this->scheme, $this->host, $port); 368 | 369 | if ($this->port === $port) { 370 | return $this; 371 | } 372 | 373 | $new = clone $this; 374 | $new->port = $port; 375 | return $new; 376 | } 377 | 378 | public function withPath($path) 379 | { 380 | if (!is_string($path)) { 381 | throw new \InvalidArgumentException( 382 | 'Invalid path provided; must be a string' 383 | ); 384 | } 385 | 386 | $path = $this->filterPath($path); 387 | 388 | if ($this->path === $path) { 389 | return $this; 390 | } 391 | 392 | $new = clone $this; 393 | $new->path = $path; 394 | return $new; 395 | } 396 | 397 | public function withQuery($query) 398 | { 399 | if (!is_string($query) && !method_exists($query, '__toString')) { 400 | throw new \InvalidArgumentException( 401 | 'Query string must be a string' 402 | ); 403 | } 404 | 405 | $query = (string) $query; 406 | if (substr($query, 0, 1) === '?') { 407 | $query = substr($query, 1); 408 | } 409 | 410 | $query = $this->filterQueryAndFragment($query); 411 | 412 | if ($this->query === $query) { 413 | return $this; 414 | } 415 | 416 | $new = clone $this; 417 | $new->query = $query; 418 | return $new; 419 | } 420 | 421 | public function withFragment($fragment) 422 | { 423 | if (substr($fragment, 0, 1) === '#') { 424 | $fragment = substr($fragment, 1); 425 | } 426 | 427 | $fragment = $this->filterQueryAndFragment($fragment); 428 | 429 | if ($this->fragment === $fragment) { 430 | return $this; 431 | } 432 | 433 | $new = clone $this; 434 | $new->fragment = $fragment; 435 | return $new; 436 | } 437 | 438 | /** 439 | * Apply parse_url parts to a URI. 440 | * 441 | * @param $parts Array of parse_url parts to apply. 442 | */ 443 | private function applyParts(array $parts) 444 | { 445 | $this->scheme = isset($parts['scheme']) 446 | ? $this->filterScheme($parts['scheme']) 447 | : ''; 448 | $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; 449 | $this->host = isset($parts['host']) ? $parts['host'] : ''; 450 | $this->port = !empty($parts['port']) 451 | ? $this->filterPort($this->scheme, $this->host, $parts['port']) 452 | : null; 453 | $this->path = isset($parts['path']) 454 | ? $this->filterPath($parts['path']) 455 | : ''; 456 | $this->query = isset($parts['query']) 457 | ? $this->filterQueryAndFragment($parts['query']) 458 | : ''; 459 | $this->fragment = isset($parts['fragment']) 460 | ? $this->filterQueryAndFragment($parts['fragment']) 461 | : ''; 462 | if (isset($parts['pass'])) { 463 | $this->userInfo .= ':' . $parts['pass']; 464 | } 465 | } 466 | 467 | /** 468 | * Create a URI string from its various parts 469 | * 470 | * @param string $scheme 471 | * @param string $authority 472 | * @param string $path 473 | * @param string $query 474 | * @param string $fragment 475 | * @return string 476 | */ 477 | private static function createUriString($scheme, $authority, $path, $query, $fragment) 478 | { 479 | $uri = ''; 480 | 481 | if (!empty($scheme)) { 482 | $uri .= $scheme . '://'; 483 | } 484 | 485 | if (!empty($authority)) { 486 | $uri .= $authority; 487 | } 488 | 489 | if ($path != null) { 490 | // Add a leading slash if necessary. 491 | if ($uri && substr($path, 0, 1) !== '/') { 492 | $uri .= '/'; 493 | } 494 | $uri .= $path; 495 | } 496 | 497 | if ($query != null) { 498 | $uri .= '?' . $query; 499 | } 500 | 501 | if ($fragment != null) { 502 | $uri .= '#' . $fragment; 503 | } 504 | 505 | return $uri; 506 | } 507 | 508 | /** 509 | * Is a given port non-standard for the current scheme? 510 | * 511 | * @param string $scheme 512 | * @param string $host 513 | * @param int $port 514 | * @return bool 515 | */ 516 | private static function isNonStandardPort($scheme, $host, $port) 517 | { 518 | if (!$scheme && $port) { 519 | return true; 520 | } 521 | 522 | if (!$host || !$port) { 523 | return false; 524 | } 525 | 526 | return !isset(static::$schemes[$scheme]) || $port !== static::$schemes[$scheme]; 527 | } 528 | 529 | /** 530 | * @param string $scheme 531 | * 532 | * @return string 533 | */ 534 | private function filterScheme($scheme) 535 | { 536 | $scheme = strtolower($scheme); 537 | $scheme = rtrim($scheme, ':/'); 538 | 539 | return $scheme; 540 | } 541 | 542 | /** 543 | * @param string $scheme 544 | * @param string $host 545 | * @param int $port 546 | * 547 | * @return int|null 548 | * 549 | * @throws \InvalidArgumentException If the port is invalid. 550 | */ 551 | private function filterPort($scheme, $host, $port) 552 | { 553 | if (null !== $port) { 554 | $port = (int) $port; 555 | if (1 > $port || 0xffff < $port) { 556 | throw new \InvalidArgumentException( 557 | sprintf('Invalid port: %d. Must be between 1 and 65535', $port) 558 | ); 559 | } 560 | } 561 | 562 | return $this->isNonStandardPort($scheme, $host, $port) ? $port : null; 563 | } 564 | 565 | /** 566 | * Filters the path of a URI 567 | * 568 | * @param $path 569 | * 570 | * @return string 571 | */ 572 | private function filterPath($path) 573 | { 574 | return preg_replace_callback( 575 | '/(?:[^' . self::$charUnreserved . self::$charSubDelims . ':@\/%]+|%(?![A-Fa-f0-9]{2}))/', 576 | array($this, 'rawurlencodeMatchZero'), 577 | $path 578 | ); 579 | } 580 | 581 | /** 582 | * Filters the query string or fragment of a URI. 583 | * 584 | * @param $str 585 | * 586 | * @return string 587 | */ 588 | private function filterQueryAndFragment($str) 589 | { 590 | return preg_replace_callback( 591 | '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/', 592 | array($this, 'rawurlencodeMatchZero'), 593 | $str 594 | ); 595 | } 596 | 597 | private function rawurlencodeMatchZero(array $match) 598 | { 599 | return rawurlencode($match[0]); 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /src/functions_include.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('Psr\Http\Message\StreamInterface') 17 | ->setMethods(array('isReadable')) 18 | ->getMockForAbstractClass(); 19 | $s->expects($this->once()) 20 | ->method('isReadable') 21 | ->will($this->returnValue(false)); 22 | $a->addStream($s); 23 | } 24 | 25 | /** 26 | * @expectedException \RuntimeException 27 | * @expectedExceptionMessage The AppendStream can only seek with SEEK_SET 28 | */ 29 | public function testValidatesSeekType() 30 | { 31 | $a = new AppendStream(); 32 | $a->seek(100, SEEK_CUR); 33 | } 34 | 35 | /** 36 | * @expectedException \RuntimeException 37 | * @expectedExceptionMessage Unable to seek stream 0 of the AppendStream 38 | */ 39 | public function testTriesToRewindOnSeek() 40 | { 41 | $a = new AppendStream(); 42 | $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') 43 | ->setMethods(array('isReadable', 'rewind', 'isSeekable')) 44 | ->getMockForAbstractClass(); 45 | $s->expects($this->once()) 46 | ->method('isReadable') 47 | ->will($this->returnValue(true)); 48 | $s->expects($this->once()) 49 | ->method('isSeekable') 50 | ->will($this->returnValue(true)); 51 | $s->expects($this->once()) 52 | ->method('rewind') 53 | ->will($this->throwException(new \RuntimeException())); 54 | $a->addStream($s); 55 | $a->seek(10); 56 | } 57 | 58 | public function testSeeksToPositionByReading() 59 | { 60 | $a = new AppendStream(array( 61 | Psr7\stream_for('foo'), 62 | Psr7\stream_for('bar'), 63 | Psr7\stream_for('baz'), 64 | )); 65 | 66 | $a->seek(3); 67 | $this->assertEquals(3, $a->tell()); 68 | $this->assertEquals('bar', $a->read(3)); 69 | 70 | $a->seek(6); 71 | $this->assertEquals(6, $a->tell()); 72 | $this->assertEquals('baz', $a->read(3)); 73 | } 74 | 75 | public function testDetachesEachStream() 76 | { 77 | $s1 = Psr7\stream_for('foo'); 78 | $s2 = Psr7\stream_for('bar'); 79 | $a = new AppendStream(array($s1, $s2)); 80 | $this->assertSame('foobar', (string) $a); 81 | $a->detach(); 82 | $this->assertSame('', (string) $a); 83 | $this->assertSame(0, $a->getSize()); 84 | } 85 | 86 | public function testClosesEachStream() 87 | { 88 | $s1 = Psr7\stream_for('foo'); 89 | $a = new AppendStream(array($s1)); 90 | $a->close(); 91 | $this->assertSame('', (string) $a); 92 | } 93 | 94 | /** 95 | * @expectedExceptionMessage Cannot write to an AppendStream 96 | * @expectedException \RuntimeException 97 | */ 98 | public function testIsNotWritable() 99 | { 100 | $a = new AppendStream(array(Psr7\stream_for('foo'))); 101 | $this->assertFalse($a->isWritable()); 102 | $this->assertTrue($a->isSeekable()); 103 | $this->assertTrue($a->isReadable()); 104 | $a->write('foo'); 105 | } 106 | 107 | public function testDoesNotNeedStreams() 108 | { 109 | $a = new AppendStream(); 110 | $this->assertEquals('', (string) $a); 111 | } 112 | 113 | public function testCanReadFromMultipleStreams() 114 | { 115 | $a = new AppendStream(array( 116 | Psr7\stream_for('foo'), 117 | Psr7\stream_for('bar'), 118 | Psr7\stream_for('baz'), 119 | )); 120 | $this->assertFalse($a->eof()); 121 | $this->assertSame(0, $a->tell()); 122 | $this->assertEquals('foo', $a->read(3)); 123 | $this->assertEquals('bar', $a->read(3)); 124 | $this->assertEquals('baz', $a->read(3)); 125 | $this->assertSame('', $a->read(1)); 126 | $this->assertTrue($a->eof()); 127 | $this->assertSame(9, $a->tell()); 128 | $this->assertEquals('foobarbaz', (string) $a); 129 | } 130 | 131 | public function testCanDetermineSizeFromMultipleStreams() 132 | { 133 | $a = new AppendStream(array( 134 | Psr7\stream_for('foo'), 135 | Psr7\stream_for('bar') 136 | )); 137 | $this->assertEquals(6, $a->getSize()); 138 | 139 | $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') 140 | ->setMethods(array('isSeekable', 'isReadable')) 141 | ->getMockForAbstractClass(); 142 | $s->expects($this->once()) 143 | ->method('isSeekable') 144 | ->will($this->returnValue(null)); 145 | $s->expects($this->once()) 146 | ->method('isReadable') 147 | ->will($this->returnValue(true)); 148 | $a->addStream($s); 149 | $this->assertNull($a->getSize()); 150 | } 151 | 152 | public function testCatchesExceptionsWhenCastingToString() 153 | { 154 | $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') 155 | ->setMethods(array('isSeekable', 'read', 'isReadable', 'eof')) 156 | ->getMockForAbstractClass(); 157 | $s->expects($this->once()) 158 | ->method('isSeekable') 159 | ->will($this->returnValue(true)); 160 | $s->expects($this->once()) 161 | ->method('read') 162 | ->will($this->throwException(new \RuntimeException('foo'))); 163 | $s->expects($this->once()) 164 | ->method('isReadable') 165 | ->will($this->returnValue(true)); 166 | $s->expects($this->any()) 167 | ->method('eof') 168 | ->will($this->returnValue(false)); 169 | $a = new AppendStream(array($s)); 170 | $this->assertFalse($a->eof()); 171 | $this->assertSame('', (string) $a); 172 | } 173 | 174 | public function testCanDetach() 175 | { 176 | $s = new AppendStream(); 177 | $s->detach(); 178 | } 179 | 180 | public function testReturnsEmptyMetadata() 181 | { 182 | $s = new AppendStream(); 183 | $this->assertEquals(array(), $s->getMetadata()); 184 | $this->assertNull($s->getMetadata('foo')); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/BufferStreamTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($b->isReadable()); 12 | $this->assertTrue($b->isWritable()); 13 | $this->assertFalse($b->isSeekable()); 14 | $this->assertEquals(null, $b->getMetadata('foo')); 15 | $this->assertEquals(10, $b->getMetadata('hwm')); 16 | $this->assertEquals(array(), $b->getMetadata()); 17 | } 18 | 19 | public function testRemovesReadDataFromBuffer() 20 | { 21 | $b = new BufferStream(); 22 | $this->assertEquals(3, $b->write('foo')); 23 | $this->assertEquals(3, $b->getSize()); 24 | $this->assertFalse($b->eof()); 25 | $this->assertEquals('foo', $b->read(10)); 26 | $this->assertTrue($b->eof()); 27 | $this->assertEquals('', $b->read(10)); 28 | } 29 | 30 | /** 31 | * @expectedException \RuntimeException 32 | * @expectedExceptionMessage Cannot determine the position of a BufferStream 33 | */ 34 | public function testCanCastToStringOrGetContents() 35 | { 36 | $b = new BufferStream(); 37 | $b->write('foo'); 38 | $b->write('baz'); 39 | $this->assertEquals('foo', $b->read(3)); 40 | $b->write('bar'); 41 | $this->assertEquals('bazbar', (string) $b); 42 | $b->tell(); 43 | } 44 | 45 | public function testDetachClearsBuffer() 46 | { 47 | $b = new BufferStream(); 48 | $b->write('foo'); 49 | $b->detach(); 50 | $this->assertTrue($b->eof()); 51 | $this->assertEquals(3, $b->write('abc')); 52 | $this->assertEquals('abc', $b->read(10)); 53 | } 54 | 55 | public function testExceedingHighwaterMarkReturnsFalseButStillBuffers() 56 | { 57 | $b = new BufferStream(5); 58 | $this->assertEquals(3, $b->write('hi ')); 59 | $this->assertFalse($b->write('hello')); 60 | $this->assertEquals('hi hello', (string) $b); 61 | $this->assertEquals(4, $b->write('test')); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/CachingStreamTest.php: -------------------------------------------------------------------------------- 1 | decorated = Psr7\stream_for('testing'); 19 | $this->body = new CachingStream($this->decorated); 20 | } 21 | 22 | public function tearDown() 23 | { 24 | $this->decorated->close(); 25 | $this->body->close(); 26 | } 27 | 28 | public function testUsesRemoteSizeIfPossible() 29 | { 30 | $body = Psr7\stream_for('test'); 31 | $caching = new CachingStream($body); 32 | $this->assertEquals(4, $caching->getSize()); 33 | } 34 | 35 | public function testReadsUntilCachedToByte() 36 | { 37 | $this->body->seek(5); 38 | $this->assertEquals('n', $this->body->read(1)); 39 | $this->body->seek(0); 40 | $this->assertEquals('t', $this->body->read(1)); 41 | } 42 | 43 | public function testCanSeekNearEndWithSeekEnd() 44 | { 45 | $baseStream = Psr7\stream_for(implode('', range('a', 'z'))); 46 | $cached = new CachingStream($baseStream); 47 | $cached->seek(1, SEEK_END); 48 | $this->assertEquals(24, $baseStream->tell()); 49 | $this->assertEquals('y', $cached->read(1)); 50 | $this->assertEquals(26, $cached->getSize()); 51 | } 52 | 53 | public function testCanSeekToEndWithSeekEnd() 54 | { 55 | $baseStream = Psr7\stream_for(implode('', range('a', 'z'))); 56 | $cached = new CachingStream($baseStream); 57 | $cached->seek(0, SEEK_END); 58 | $this->assertEquals(25, $baseStream->tell()); 59 | $this->assertEquals('z', $cached->read(1)); 60 | $this->assertEquals(26, $cached->getSize()); 61 | } 62 | 63 | public function testCanUseSeekEndWithUnknownSize() 64 | { 65 | $baseStream = Psr7\stream_for('testing'); 66 | $decorated = Psr7\FnStream::decorate($baseStream, array( 67 | 'getSize' => function () { return null; } 68 | )); 69 | $cached = new CachingStream($decorated); 70 | $cached->seek(1, SEEK_END); 71 | $this->assertEquals('ng', $cached->read(2)); 72 | } 73 | 74 | public function testRewindUsesSeek() 75 | { 76 | $a = Psr7\stream_for('foo'); 77 | $d = $this->getMockBuilder('RingCentral\Psr7\CachingStream') 78 | ->setMethods(array('seek')) 79 | ->setConstructorArgs(array($a)) 80 | ->getMock(); 81 | $d->expects($this->once()) 82 | ->method('seek') 83 | ->with(0) 84 | ->will($this->returnValue(true)); 85 | $d->seek(0); 86 | } 87 | 88 | public function testCanSeekToReadBytes() 89 | { 90 | $this->assertEquals('te', $this->body->read(2)); 91 | $this->body->seek(0); 92 | $this->assertEquals('test', $this->body->read(4)); 93 | $this->assertEquals(4, $this->body->tell()); 94 | $this->body->seek(2); 95 | $this->assertEquals(2, $this->body->tell()); 96 | $this->body->seek(2, SEEK_CUR); 97 | $this->assertEquals(4, $this->body->tell()); 98 | $this->assertEquals('ing', $this->body->read(3)); 99 | } 100 | 101 | public function testWritesToBufferStream() 102 | { 103 | $this->body->read(2); 104 | $this->body->write('hi'); 105 | $this->body->seek(0); 106 | $this->assertEquals('tehiing', (string) $this->body); 107 | } 108 | 109 | public function testSkipsOverwrittenBytes() 110 | { 111 | $decorated = Psr7\stream_for( 112 | implode("\n", array_map(function ($n) { 113 | return str_pad($n, 4, '0', STR_PAD_LEFT); 114 | }, range(0, 25))) 115 | ); 116 | 117 | $body = new CachingStream($decorated); 118 | 119 | $this->assertEquals("0000\n", Psr7\readline($body)); 120 | $this->assertEquals("0001\n", Psr7\readline($body)); 121 | // Write over part of the body yet to be read, so skip some bytes 122 | $this->assertEquals(5, $body->write("TEST\n")); 123 | $this->assertEquals(5, $this->readAttribute($body, 'skipReadBytes')); 124 | // Read, which skips bytes, then reads 125 | $this->assertEquals("0003\n", Psr7\readline($body)); 126 | $this->assertEquals(0, $this->readAttribute($body, 'skipReadBytes')); 127 | $this->assertEquals("0004\n", Psr7\readline($body)); 128 | $this->assertEquals("0005\n", Psr7\readline($body)); 129 | 130 | // Overwrite part of the cached body (so don't skip any bytes) 131 | $body->seek(5); 132 | $this->assertEquals(5, $body->write("ABCD\n")); 133 | $this->assertEquals(0, $this->readAttribute($body, 'skipReadBytes')); 134 | $this->assertEquals("TEST\n", Psr7\readline($body)); 135 | $this->assertEquals("0003\n", Psr7\readline($body)); 136 | $this->assertEquals("0004\n", Psr7\readline($body)); 137 | $this->assertEquals("0005\n", Psr7\readline($body)); 138 | $this->assertEquals("0006\n", Psr7\readline($body)); 139 | $this->assertEquals(5, $body->write("1234\n")); 140 | $this->assertEquals(5, $this->readAttribute($body, 'skipReadBytes')); 141 | 142 | // Seek to 0 and ensure the overwritten bit is replaced 143 | $body->seek(0); 144 | $this->assertEquals("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", $body->read(50)); 145 | 146 | // Ensure that casting it to a string does not include the bit that was overwritten 147 | $this->assertContains("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", (string) $body); 148 | } 149 | 150 | public function testClosesBothStreams() 151 | { 152 | $s = fopen('php://temp', 'r'); 153 | $a = Psr7\stream_for($s); 154 | $d = new CachingStream($a); 155 | $d->close(); 156 | $this->assertFalse(is_resource($s)); 157 | } 158 | 159 | /** 160 | * @expectedException \InvalidArgumentException 161 | */ 162 | public function testEnsuresValidWhence() 163 | { 164 | $this->body->seek(10, -123456); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /tests/DroppingStreamTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(3, $drop->write('hel')); 14 | $this->assertEquals(2, $drop->write('lo')); 15 | $this->assertEquals(5, $drop->getSize()); 16 | $this->assertEquals('hello', $drop->read(5)); 17 | $this->assertEquals(0, $drop->getSize()); 18 | $drop->write('12345678910'); 19 | $this->assertEquals(5, $stream->getSize()); 20 | $this->assertEquals(5, $drop->getSize()); 21 | $this->assertEquals('12345', (string) $drop); 22 | $this->assertEquals(0, $drop->getSize()); 23 | $drop->write('hello'); 24 | $this->assertSame(0, $drop->write('test')); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/FnStreamTest.php: -------------------------------------------------------------------------------- 1 | seek(1); 20 | } 21 | 22 | public function testProxiesToFunction() 23 | { 24 | $self = $this; 25 | $s = new FnStream(array( 26 | 'read' => function ($len) use ($self) { 27 | $self->assertEquals(3, $len); 28 | return 'foo'; 29 | } 30 | )); 31 | 32 | $this->assertEquals('foo', $s->read(3)); 33 | } 34 | 35 | public function testCanCloseOnDestruct() 36 | { 37 | $called = false; 38 | $s = new FnStream(array( 39 | 'close' => function () use (&$called) { 40 | $called = true; 41 | } 42 | )); 43 | unset($s); 44 | $this->assertTrue($called); 45 | } 46 | 47 | public function testDoesNotRequireClose() 48 | { 49 | $s = new FnStream(array()); 50 | unset($s); 51 | } 52 | 53 | public function testDecoratesStream() 54 | { 55 | $a = Psr7\stream_for('foo'); 56 | $b = FnStream::decorate($a, array()); 57 | $this->assertEquals(3, $b->getSize()); 58 | $this->assertEquals($b->isWritable(), true); 59 | $this->assertEquals($b->isReadable(), true); 60 | $this->assertEquals($b->isSeekable(), true); 61 | $this->assertEquals($b->read(3), 'foo'); 62 | $this->assertEquals($b->tell(), 3); 63 | $this->assertEquals($a->tell(), 3); 64 | $this->assertSame('', $a->read(1)); 65 | $this->assertEquals($b->eof(), true); 66 | $this->assertEquals($a->eof(), true); 67 | $b->seek(0); 68 | $this->assertEquals('foo', (string) $b); 69 | $b->seek(0); 70 | $this->assertEquals('foo', $b->getContents()); 71 | $this->assertEquals($a->getMetadata(), $b->getMetadata()); 72 | $b->seek(0, SEEK_END); 73 | $b->write('bar'); 74 | $this->assertEquals('foobar', (string) $b); 75 | $this->assertInternalType('resource', $b->detach()); 76 | $b->close(); 77 | } 78 | 79 | public function testDecoratesWithCustomizations() 80 | { 81 | $called = false; 82 | $a = Psr7\stream_for('foo'); 83 | $b = FnStream::decorate($a, array( 84 | 'read' => function ($len) use (&$called, $a) { 85 | $called = true; 86 | return $a->read($len); 87 | } 88 | )); 89 | $this->assertEquals('foo', $b->read(3)); 90 | $this->assertTrue($called); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/FunctionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('foobaz', Psr7\copy_to_string($s)); 14 | $s->seek(0); 15 | $this->assertEquals('foo', Psr7\copy_to_string($s, 3)); 16 | $this->assertEquals('baz', Psr7\copy_to_string($s, 3)); 17 | $this->assertEquals('', Psr7\copy_to_string($s)); 18 | } 19 | 20 | public function testCopiesToStringStopsWhenReadFails() 21 | { 22 | $s1 = Psr7\stream_for('foobaz'); 23 | $s1 = FnStream::decorate($s1, array( 24 | 'read' => function () { return ''; } 25 | )); 26 | $result = Psr7\copy_to_string($s1); 27 | $this->assertEquals('', $result); 28 | } 29 | 30 | public function testCopiesToStream() 31 | { 32 | $s1 = Psr7\stream_for('foobaz'); 33 | $s2 = Psr7\stream_for(''); 34 | Psr7\copy_to_stream($s1, $s2); 35 | $this->assertEquals('foobaz', (string) $s2); 36 | $s2 = Psr7\stream_for(''); 37 | $s1->seek(0); 38 | Psr7\copy_to_stream($s1, $s2, 3); 39 | $this->assertEquals('foo', (string) $s2); 40 | Psr7\copy_to_stream($s1, $s2, 3); 41 | $this->assertEquals('foobaz', (string) $s2); 42 | } 43 | 44 | public function testStopsCopyToStreamWhenWriteFails() 45 | { 46 | $s1 = Psr7\stream_for('foobaz'); 47 | $s2 = Psr7\stream_for(''); 48 | $s2 = FnStream::decorate($s2, array('write' => function () { return 0; })); 49 | Psr7\copy_to_stream($s1, $s2); 50 | $this->assertEquals('', (string) $s2); 51 | } 52 | 53 | public function testStopsCopyToSteamWhenWriteFailsWithMaxLen() 54 | { 55 | $s1 = Psr7\stream_for('foobaz'); 56 | $s2 = Psr7\stream_for(''); 57 | $s2 = FnStream::decorate($s2, array('write' => function () { return 0; })); 58 | Psr7\copy_to_stream($s1, $s2, 10); 59 | $this->assertEquals('', (string) $s2); 60 | } 61 | 62 | public function testStopsCopyToSteamWhenReadFailsWithMaxLen() 63 | { 64 | $s1 = Psr7\stream_for('foobaz'); 65 | $s1 = FnStream::decorate($s1, array('read' => function () { return ''; })); 66 | $s2 = Psr7\stream_for(''); 67 | Psr7\copy_to_stream($s1, $s2, 10); 68 | $this->assertEquals('', (string) $s2); 69 | } 70 | 71 | public function testReadsLines() 72 | { 73 | $s = Psr7\stream_for("foo\nbaz\nbar"); 74 | $this->assertEquals("foo\n", Psr7\readline($s)); 75 | $this->assertEquals("baz\n", Psr7\readline($s)); 76 | $this->assertEquals("bar", Psr7\readline($s)); 77 | } 78 | 79 | public function testReadsLinesUpToMaxLength() 80 | { 81 | $s = Psr7\stream_for("12345\n"); 82 | $this->assertEquals("123", Psr7\readline($s, 4)); 83 | $this->assertEquals("45\n", Psr7\readline($s)); 84 | } 85 | 86 | public function testReadsLineUntilFalseReturnedFromRead() 87 | { 88 | $s = $this->getMockBuilder('RingCentral\Psr7\Stream') 89 | ->setMethods(array('read', 'eof')) 90 | ->disableOriginalConstructor() 91 | ->getMock(); 92 | $s->expects($this->exactly(2)) 93 | ->method('read') 94 | ->will($this->returnCallback(function () { 95 | static $c = false; 96 | if ($c) { 97 | return false; 98 | } 99 | $c = true; 100 | return 'h'; 101 | })); 102 | $s->expects($this->exactly(2)) 103 | ->method('eof') 104 | ->will($this->returnValue(false)); 105 | $this->assertEquals("h", Psr7\readline($s)); 106 | } 107 | 108 | public function testCalculatesHash() 109 | { 110 | $s = Psr7\stream_for('foobazbar'); 111 | $this->assertEquals(md5('foobazbar'), Psr7\hash($s, 'md5')); 112 | } 113 | 114 | /** 115 | * @expectedException \RuntimeException 116 | */ 117 | public function testCalculatesHashThrowsWhenSeekFails() 118 | { 119 | $s = new NoSeekStream(Psr7\stream_for('foobazbar')); 120 | $s->read(2); 121 | Psr7\hash($s, 'md5'); 122 | } 123 | 124 | public function testCalculatesHashSeeksToOriginalPosition() 125 | { 126 | $s = Psr7\stream_for('foobazbar'); 127 | $s->seek(4); 128 | $this->assertEquals(md5('foobazbar'), Psr7\hash($s, 'md5')); 129 | $this->assertEquals(4, $s->tell()); 130 | } 131 | 132 | public function testOpensFilesSuccessfully() 133 | { 134 | $r = Psr7\try_fopen(__FILE__, 'r'); 135 | $this->assertInternalType('resource', $r); 136 | fclose($r); 137 | } 138 | 139 | /** 140 | * @expectedException \RuntimeException 141 | * @expectedExceptionMessage Unable to open /path/to/does/not/exist using mode r 142 | */ 143 | public function testThrowsExceptionNotWarning() 144 | { 145 | Psr7\try_fopen('/path/to/does/not/exist', 'r'); 146 | } 147 | 148 | public function parseQueryProvider() 149 | { 150 | return array( 151 | // Does not need to parse when the string is empty 152 | array('', array()), 153 | // Can parse mult-values items 154 | array('q=a&q=b', array('q' => array('a', 'b'))), 155 | // Can parse multi-valued items that use numeric indices 156 | array('q[0]=a&q[1]=b', array('q[0]' => 'a', 'q[1]' => 'b')), 157 | // Can parse duplicates and does not include numeric indices 158 | array('q[]=a&q[]=b', array('q[]' => array('a', 'b'))), 159 | // Ensures that the value of "q" is an array even though one value 160 | array('q[]=a', array('q[]' => 'a')), 161 | // Does not modify "." to "_" like PHP's parse_str() 162 | array('q.a=a&q.b=b', array('q.a' => 'a', 'q.b' => 'b')), 163 | // Can decode %20 to " " 164 | array('q%20a=a%20b', array('q a' => 'a b')), 165 | // Can parse funky strings with no values by assigning each to null 166 | array('q&a', array('q' => null, 'a' => null)), 167 | // Does not strip trailing equal signs 168 | array('data=abc=', array('data' => 'abc=')), 169 | // Can store duplicates without affecting other values 170 | array('foo=a&foo=b&?µ=c', array('foo' => array('a', 'b'), '?µ' => 'c')), 171 | // Sets value to null when no "=" is present 172 | array('foo', array('foo' => null)), 173 | // Preserves "0" keys. 174 | array('0', array('0' => null)), 175 | // Sets the value to an empty string when "=" is present 176 | array('0=', array('0' => '')), 177 | // Preserves falsey keys 178 | array('var=0', array('var' => '0')), 179 | array('a[b][c]=1&a[b][c]=2', array('a[b][c]' => array('1', '2'))), 180 | array('a[b]=c&a[d]=e', array('a[b]' => 'c', 'a[d]' => 'e')), 181 | // Ensure it doesn't leave things behind with repeated values 182 | // Can parse mult-values items 183 | array('q=a&q=b&q=c', array('q' => array('a', 'b', 'c'))), 184 | ); 185 | } 186 | 187 | /** 188 | * @dataProvider parseQueryProvider 189 | */ 190 | public function testParsesQueries($input, $output) 191 | { 192 | $result = Psr7\parse_query($input); 193 | $this->assertSame($output, $result); 194 | } 195 | 196 | public function testDoesNotDecode() 197 | { 198 | $str = 'foo%20=bar'; 199 | $data = Psr7\parse_query($str, false); 200 | $this->assertEquals(array('foo%20' => 'bar'), $data); 201 | } 202 | 203 | /** 204 | * @dataProvider parseQueryProvider 205 | */ 206 | public function testParsesAndBuildsQueries($input, $output) 207 | { 208 | $result = Psr7\parse_query($input, false); 209 | $this->assertSame($input, Psr7\build_query($result, false)); 210 | } 211 | 212 | public function testEncodesWithRfc1738() 213 | { 214 | $str = Psr7\build_query(array('foo bar' => 'baz+'), PHP_QUERY_RFC1738); 215 | $this->assertEquals('foo+bar=baz%2B', $str); 216 | } 217 | 218 | public function testEncodesWithRfc3986() 219 | { 220 | $str = Psr7\build_query(array('foo bar' => 'baz+'), PHP_QUERY_RFC3986); 221 | $this->assertEquals('foo%20bar=baz%2B', $str); 222 | } 223 | 224 | public function testDoesNotEncode() 225 | { 226 | $str = Psr7\build_query(array('foo bar' => 'baz+'), false); 227 | $this->assertEquals('foo bar=baz+', $str); 228 | } 229 | 230 | public function testCanControlDecodingType() 231 | { 232 | $result = Psr7\parse_query('var=foo+bar', PHP_QUERY_RFC3986); 233 | $this->assertEquals('foo+bar', $result['var']); 234 | $result = Psr7\parse_query('var=foo+bar', PHP_QUERY_RFC1738); 235 | $this->assertEquals('foo bar', $result['var']); 236 | } 237 | 238 | public function testParsesRequestMessages() 239 | { 240 | $req = "GET /abc HTTP/1.0\r\nHost: foo.com\r\nFoo: Bar\r\nBaz: Bam\r\nBaz: Qux\r\n\r\nTest"; 241 | $request = Psr7\parse_request($req); 242 | $this->assertEquals('GET', $request->getMethod()); 243 | $this->assertEquals('/abc', $request->getRequestTarget()); 244 | $this->assertEquals('1.0', $request->getProtocolVersion()); 245 | $this->assertEquals('foo.com', $request->getHeaderLine('Host')); 246 | $this->assertEquals('Bar', $request->getHeaderLine('Foo')); 247 | $this->assertEquals('Bam, Qux', $request->getHeaderLine('Baz')); 248 | $this->assertEquals('Test', (string) $request->getBody()); 249 | $this->assertEquals('http://foo.com/abc', (string) $request->getUri()); 250 | } 251 | 252 | public function testParsesRequestMessagesWithHttpsScheme() 253 | { 254 | $req = "PUT /abc?baz=bar HTTP/1.1\r\nHost: foo.com:443\r\n\r\n"; 255 | $request = Psr7\parse_request($req); 256 | $this->assertEquals('PUT', $request->getMethod()); 257 | $this->assertEquals('/abc?baz=bar', $request->getRequestTarget()); 258 | $this->assertEquals('1.1', $request->getProtocolVersion()); 259 | $this->assertEquals('foo.com:443', $request->getHeaderLine('Host')); 260 | $this->assertEquals('', (string) $request->getBody()); 261 | $this->assertEquals('https://foo.com/abc?baz=bar', (string) $request->getUri()); 262 | } 263 | 264 | public function testParsesRequestMessagesWithUriWhenHostIsNotFirst() 265 | { 266 | $req = "PUT / HTTP/1.1\r\nFoo: Bar\r\nHost: foo.com\r\n\r\n"; 267 | $request = Psr7\parse_request($req); 268 | $this->assertEquals('PUT', $request->getMethod()); 269 | $this->assertEquals('/', $request->getRequestTarget()); 270 | $this->assertEquals('http://foo.com/', (string) $request->getUri()); 271 | } 272 | 273 | public function testParsesRequestMessagesWithFullUri() 274 | { 275 | $req = "GET https://www.google.com:443/search?q=foobar HTTP/1.1\r\nHost: www.google.com\r\n\r\n"; 276 | $request = Psr7\parse_request($req); 277 | $this->assertEquals('GET', $request->getMethod()); 278 | $this->assertEquals('https://www.google.com:443/search?q=foobar', $request->getRequestTarget()); 279 | $this->assertEquals('1.1', $request->getProtocolVersion()); 280 | $this->assertEquals('www.google.com', $request->getHeaderLine('Host')); 281 | $this->assertEquals('', (string) $request->getBody()); 282 | $this->assertEquals('https://www.google.com/search?q=foobar', (string) $request->getUri()); 283 | } 284 | 285 | /** 286 | * @expectedException \InvalidArgumentException 287 | */ 288 | public function testValidatesRequestMessages() 289 | { 290 | Psr7\parse_request("HTTP/1.1 200 OK\r\n\r\n"); 291 | } 292 | 293 | public function testParsesResponseMessages() 294 | { 295 | $res = "HTTP/1.0 200 OK\r\nFoo: Bar\r\nBaz: Bam\r\nBaz: Qux\r\n\r\nTest"; 296 | $response = Psr7\parse_response($res); 297 | $this->assertEquals(200, $response->getStatusCode()); 298 | $this->assertEquals('OK', $response->getReasonPhrase()); 299 | $this->assertEquals('1.0', $response->getProtocolVersion()); 300 | $this->assertEquals('Bar', $response->getHeaderLine('Foo')); 301 | $this->assertEquals('Bam, Qux', $response->getHeaderLine('Baz')); 302 | $this->assertEquals('Test', (string) $response->getBody()); 303 | } 304 | 305 | /** 306 | * @expectedException \InvalidArgumentException 307 | */ 308 | public function testValidatesResponseMessages() 309 | { 310 | Psr7\parse_response("GET / HTTP/1.1\r\n\r\n"); 311 | } 312 | 313 | public function testDetermineMimetype() 314 | { 315 | $this->assertNull(Psr7\mimetype_from_extension('not-a-real-extension')); 316 | $this->assertEquals( 317 | 'application/json', 318 | Psr7\mimetype_from_extension('json') 319 | ); 320 | $this->assertEquals( 321 | 'image/jpeg', 322 | Psr7\mimetype_from_filename('/tmp/images/IMG034821.JPEG') 323 | ); 324 | } 325 | 326 | public function testCreatesUriForValue() 327 | { 328 | $this->assertInstanceOf('RingCentral\Psr7\Uri', Psr7\uri_for('/foo')); 329 | $this->assertInstanceOf( 330 | 'RingCentral\Psr7\Uri', 331 | Psr7\uri_for(new Psr7\Uri('/foo')) 332 | ); 333 | } 334 | 335 | /** 336 | * @expectedException \InvalidArgumentException 337 | */ 338 | public function testValidatesUri() 339 | { 340 | Psr7\uri_for(array()); 341 | } 342 | 343 | public function testKeepsPositionOfResource() 344 | { 345 | $h = fopen(__FILE__, 'r'); 346 | fseek($h, 10); 347 | $stream = Psr7\stream_for($h); 348 | $this->assertEquals(10, $stream->tell()); 349 | $stream->close(); 350 | } 351 | 352 | public function testCreatesWithFactory() 353 | { 354 | $stream = Psr7\stream_for('foo'); 355 | $this->assertInstanceOf('RingCentral\Psr7\Stream', $stream); 356 | $this->assertEquals('foo', $stream->getContents()); 357 | $stream->close(); 358 | } 359 | 360 | public function testFactoryCreatesFromEmptyString() 361 | { 362 | $s = Psr7\stream_for(); 363 | $this->assertInstanceOf('RingCentral\Psr7\Stream', $s); 364 | } 365 | 366 | public function testFactoryCreatesFromNull() 367 | { 368 | $s = Psr7\stream_for(null); 369 | $this->assertInstanceOf('RingCentral\Psr7\Stream', $s); 370 | } 371 | 372 | public function testFactoryCreatesFromResource() 373 | { 374 | $r = fopen(__FILE__, 'r'); 375 | $s = Psr7\stream_for($r); 376 | $this->assertInstanceOf('RingCentral\Psr7\Stream', $s); 377 | $this->assertSame(file_get_contents(__FILE__), (string) $s); 378 | } 379 | 380 | public function testFactoryCreatesFromObjectWithToString() 381 | { 382 | $r = new HasToString(); 383 | $s = Psr7\stream_for($r); 384 | $this->assertInstanceOf('RingCentral\Psr7\Stream', $s); 385 | $this->assertEquals('foo', (string) $s); 386 | } 387 | 388 | public function testCreatePassesThrough() 389 | { 390 | $s = Psr7\stream_for('foo'); 391 | $this->assertSame($s, Psr7\stream_for($s)); 392 | } 393 | 394 | /** 395 | * @expectedException \InvalidArgumentException 396 | */ 397 | public function testThrowsExceptionForUnknown() 398 | { 399 | Psr7\stream_for(new \stdClass()); 400 | } 401 | 402 | public function testReturnsCustomMetadata() 403 | { 404 | $s = Psr7\stream_for('foo', array('metadata' => array('hwm' => 3))); 405 | $this->assertEquals(3, $s->getMetadata('hwm')); 406 | $this->assertArrayHasKey('hwm', $s->getMetadata()); 407 | } 408 | 409 | public function testCanSetSize() 410 | { 411 | $s = Psr7\stream_for('', array('size' => 10)); 412 | $this->assertEquals(10, $s->getSize()); 413 | } 414 | 415 | public function testCanCreateIteratorBasedStream() 416 | { 417 | $a = new \ArrayIterator(array('foo', 'bar', '123')); 418 | $p = Psr7\stream_for($a); 419 | $this->assertInstanceOf('RingCentral\Psr7\PumpStream', $p); 420 | $this->assertEquals('foo', $p->read(3)); 421 | $this->assertFalse($p->eof()); 422 | $this->assertEquals('b', $p->read(1)); 423 | $this->assertEquals('a', $p->read(1)); 424 | $this->assertEquals('r12', $p->read(3)); 425 | $this->assertFalse($p->eof()); 426 | $this->assertEquals('3', $p->getContents()); 427 | $this->assertTrue($p->eof()); 428 | $this->assertEquals(9, $p->tell()); 429 | } 430 | 431 | public function testConvertsRequestsToStrings() 432 | { 433 | $request = new Psr7\Request('PUT', 'http://foo.com/hi?123', array( 434 | 'Baz' => 'bar', 435 | 'Qux' => ' ipsum' 436 | ), 'hello', '1.0'); 437 | $this->assertEquals( 438 | "PUT /hi?123 HTTP/1.0\r\nHost: foo.com\r\nBaz: bar\r\nQux: ipsum\r\n\r\nhello", 439 | Psr7\str($request) 440 | ); 441 | } 442 | 443 | public function testConvertsResponsesToStrings() 444 | { 445 | $response = new Psr7\Response(200, array( 446 | 'Baz' => 'bar', 447 | 'Qux' => ' ipsum' 448 | ), 'hello', '1.0', 'FOO'); 449 | $this->assertEquals( 450 | "HTTP/1.0 200 FOO\r\nBaz: bar\r\nQux: ipsum\r\n\r\nhello", 451 | Psr7\str($response) 452 | ); 453 | } 454 | 455 | public function parseParamsProvider() 456 | { 457 | $res1 = array( 458 | array( 459 | '', 460 | 'rel' => 'front', 461 | 'type' => 'image/jpeg', 462 | ), 463 | array( 464 | '', 465 | 'rel' => 'back', 466 | 'type' => 'image/jpeg', 467 | ), 468 | ); 469 | return array( 470 | array( 471 | '; rel="front"; type="image/jpeg", ; rel=back; type="image/jpeg"', 472 | $res1 473 | ), 474 | array( 475 | '; rel="front"; type="image/jpeg",; rel=back; type="image/jpeg"', 476 | $res1 477 | ), 478 | array( 479 | 'foo="baz"; bar=123, boo, test="123", foobar="foo;bar"', 480 | array( 481 | array('foo' => 'baz', 'bar' => '123'), 482 | array('boo'), 483 | array('test' => '123'), 484 | array('foobar' => 'foo;bar') 485 | ) 486 | ), 487 | array( 488 | '; rel="side"; type="image/jpeg",; rel=side; type="image/jpeg"', 489 | array( 490 | array('', 'rel' => 'side', 'type' => 'image/jpeg'), 491 | array('', 'rel' => 'side', 'type' => 'image/jpeg') 492 | ) 493 | ), 494 | array( 495 | '', 496 | array() 497 | ) 498 | ); 499 | } 500 | /** 501 | * @dataProvider parseParamsProvider 502 | */ 503 | public function testParseParams($header, $result) 504 | { 505 | $this->assertEquals($result, Psr7\parse_header($header)); 506 | } 507 | 508 | public function testParsesArrayHeaders() 509 | { 510 | $header = array('a, b', 'c', 'd, e'); 511 | $this->assertEquals(array('a', 'b', 'c', 'd', 'e'), Psr7\normalize_header($header)); 512 | } 513 | 514 | public function testRewindsBody() 515 | { 516 | $body = Psr7\stream_for('abc'); 517 | $res = new Psr7\Response(200, array(), $body); 518 | Psr7\rewind_body($res); 519 | $this->assertEquals(0, $body->tell()); 520 | $body->rewind(1); 521 | Psr7\rewind_body($res); 522 | $this->assertEquals(0, $body->tell()); 523 | } 524 | 525 | /** 526 | * @expectedException \RuntimeException 527 | */ 528 | public function testThrowsWhenBodyCannotBeRewound() 529 | { 530 | $body = Psr7\stream_for('abc'); 531 | $body->read(1); 532 | $body = FnStream::decorate($body, array( 533 | 'rewind' => function () { throw new \RuntimeException('a'); } 534 | )); 535 | $res = new Psr7\Response(200, array(), $body); 536 | Psr7\rewind_body($res); 537 | } 538 | 539 | public function testCanModifyRequestWithUri() 540 | { 541 | $r1 = new Psr7\Request('GET', 'http://foo.com'); 542 | $r2 = Psr7\modify_request($r1, array( 543 | 'uri' => new Psr7\Uri('http://www.foo.com') 544 | )); 545 | $this->assertEquals('http://www.foo.com', (string) $r2->getUri()); 546 | $this->assertEquals('www.foo.com', (string) $r2->getHeaderLine('host')); 547 | } 548 | 549 | public function testCanModifyRequestWithCaseInsensitiveHeader() 550 | { 551 | $r1 = new Psr7\Request('GET', 'http://foo.com', array('User-Agent' => 'foo')); 552 | $r2 = Psr7\modify_request($r1, array('set_headers' => array('User-agent' => 'bar'))); 553 | $this->assertEquals('bar', $r2->getHeaderLine('User-Agent')); 554 | $this->assertEquals('bar', $r2->getHeaderLine('User-agent')); 555 | } 556 | 557 | public function testReturnsAsIsWhenNoChanges() 558 | { 559 | $request = new Psr7\Request('GET', 'http://foo.com'); 560 | $this->assertSame($request, Psr7\modify_request($request, array())); 561 | } 562 | 563 | public function testReturnsUriAsIsWhenNoChanges() 564 | { 565 | $r1 = new Psr7\Request('GET', 'http://foo.com'); 566 | $r2 = Psr7\modify_request($r1, array('set_headers' => array('foo' => 'bar'))); 567 | $this->assertNotSame($r1, $r2); 568 | $this->assertEquals('bar', $r2->getHeaderLine('foo')); 569 | } 570 | 571 | public function testRemovesHeadersFromMessage() 572 | { 573 | $r1 = new Psr7\Request('GET', 'http://foo.com', array('foo' => 'bar')); 574 | $r2 = Psr7\modify_request($r1, array('remove_headers' => array('foo'))); 575 | $this->assertNotSame($r1, $r2); 576 | $this->assertFalse($r2->hasHeader('foo')); 577 | } 578 | 579 | public function testAddsQueryToUri() 580 | { 581 | $r1 = new Psr7\Request('GET', 'http://foo.com'); 582 | $r2 = Psr7\modify_request($r1, array('query' => 'foo=bar')); 583 | $this->assertNotSame($r1, $r2); 584 | $this->assertEquals('foo=bar', $r2->getUri()->getQuery()); 585 | } 586 | 587 | public function testServerRequestWithServerParams() 588 | { 589 | $requestString = "GET /abc HTTP/1.1\r\nHost: foo.com\r\n\r\n"; 590 | $request = Psr7\parse_server_request($requestString); 591 | 592 | $this->assertEquals(array(), $request->getServerParams()); 593 | } 594 | 595 | public function testServerRequestWithoutServerParams() 596 | { 597 | $requestString = "GET /abc HTTP/1.1\r\nHost: foo.com\r\n\r\n"; 598 | $serverParams = array('server_address' => '127.0.0.1', 'server_port' => 80); 599 | 600 | $request = Psr7\parse_server_request($requestString, $serverParams); 601 | 602 | $this->assertEquals(array('server_address' => '127.0.0.1', 'server_port' => 80), $request->getServerParams()); 603 | } 604 | } 605 | -------------------------------------------------------------------------------- /tests/InflateStreamTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('test', (string) $b); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/LazyOpenStreamTest.php: -------------------------------------------------------------------------------- 1 | fname = tempnam('/tmp', 'tfile'); 13 | 14 | if (file_exists($this->fname)) { 15 | unlink($this->fname); 16 | } 17 | } 18 | 19 | public function tearDown() 20 | { 21 | if (file_exists($this->fname)) { 22 | unlink($this->fname); 23 | } 24 | } 25 | 26 | public function testOpensLazily() 27 | { 28 | $l = new LazyOpenStream($this->fname, 'w+'); 29 | $l->write('foo'); 30 | $this->assertInternalType('array', $l->getMetadata()); 31 | $this->assertFileExists($this->fname); 32 | $this->assertEquals('foo', file_get_contents($this->fname)); 33 | $this->assertEquals('foo', (string) $l); 34 | } 35 | 36 | public function testProxiesToFile() 37 | { 38 | file_put_contents($this->fname, 'foo'); 39 | $l = new LazyOpenStream($this->fname, 'r'); 40 | $this->assertEquals('foo', $l->read(4)); 41 | $this->assertTrue($l->eof()); 42 | $this->assertEquals(3, $l->tell()); 43 | $this->assertTrue($l->isReadable()); 44 | $this->assertTrue($l->isSeekable()); 45 | $this->assertFalse($l->isWritable()); 46 | $l->seek(1); 47 | $this->assertEquals('oo', $l->getContents()); 48 | $this->assertEquals('foo', (string) $l); 49 | $this->assertEquals(3, $l->getSize()); 50 | $this->assertInternalType('array', $l->getMetadata()); 51 | $l->close(); 52 | } 53 | 54 | public function testDetachesUnderlyingStream() 55 | { 56 | file_put_contents($this->fname, 'foo'); 57 | $l = new LazyOpenStream($this->fname, 'r'); 58 | $r = $l->detach(); 59 | $this->assertInternalType('resource', $r); 60 | fseek($r, 0); 61 | $this->assertEquals('foo', stream_get_contents($r)); 62 | fclose($r); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/LimitStreamTest.php: -------------------------------------------------------------------------------- 1 | decorated = Psr7\stream_for(fopen(__FILE__, 'r')); 24 | $this->body = new LimitStream($this->decorated, 10, 3); 25 | } 26 | 27 | public function testReturnsSubset() 28 | { 29 | $body = new LimitStream(Psr7\stream_for('foo'), -1, 1); 30 | $this->assertEquals('oo', (string) $body); 31 | $this->assertTrue($body->eof()); 32 | $body->seek(0); 33 | $this->assertFalse($body->eof()); 34 | $this->assertEquals('oo', $body->read(100)); 35 | $this->assertSame('', $body->read(1)); 36 | $this->assertTrue($body->eof()); 37 | } 38 | 39 | public function testReturnsSubsetWhenCastToString() 40 | { 41 | $body = Psr7\stream_for('foo_baz_bar'); 42 | $limited = new LimitStream($body, 3, 4); 43 | $this->assertEquals('baz', (string) $limited); 44 | } 45 | 46 | /** 47 | * @expectedException \RuntimeException 48 | * @expectedExceptionMessage Unable to seek to stream position 10 with whence 0 49 | */ 50 | public function testEnsuresPositionCanBeekSeekedTo() 51 | { 52 | new LimitStream(Psr7\stream_for(''), 0, 10); 53 | } 54 | 55 | public function testReturnsSubsetOfEmptyBodyWhenCastToString() 56 | { 57 | $body = Psr7\stream_for('01234567891234'); 58 | $limited = new LimitStream($body, 0, 10); 59 | $this->assertEquals('', (string) $limited); 60 | } 61 | 62 | public function testReturnsSpecificSubsetOBodyWhenCastToString() 63 | { 64 | $body = Psr7\stream_for('0123456789abcdef'); 65 | $limited = new LimitStream($body, 3, 10); 66 | $this->assertEquals('abc', (string) $limited); 67 | } 68 | 69 | public function testSeeksWhenConstructed() 70 | { 71 | $this->assertEquals(0, $this->body->tell()); 72 | $this->assertEquals(3, $this->decorated->tell()); 73 | } 74 | 75 | public function testAllowsBoundedSeek() 76 | { 77 | $this->body->seek(100); 78 | $this->assertEquals(10, $this->body->tell()); 79 | $this->assertEquals(13, $this->decorated->tell()); 80 | $this->body->seek(0); 81 | $this->assertEquals(0, $this->body->tell()); 82 | $this->assertEquals(3, $this->decorated->tell()); 83 | try { 84 | $this->body->seek(-10); 85 | $this->fail(); 86 | } catch (\RuntimeException $e) {} 87 | $this->assertEquals(0, $this->body->tell()); 88 | $this->assertEquals(3, $this->decorated->tell()); 89 | $this->body->seek(5); 90 | $this->assertEquals(5, $this->body->tell()); 91 | $this->assertEquals(8, $this->decorated->tell()); 92 | // Fail 93 | try { 94 | $this->body->seek(1000, SEEK_END); 95 | $this->fail(); 96 | } catch (\RuntimeException $e) {} 97 | } 98 | 99 | public function testReadsOnlySubsetOfData() 100 | { 101 | $data = $this->body->read(100); 102 | $this->assertEquals(10, strlen($data)); 103 | $this->assertSame('', $this->body->read(1000)); 104 | 105 | $this->body->setOffset(10); 106 | $newData = $this->body->read(100); 107 | $this->assertEquals(10, strlen($newData)); 108 | $this->assertNotSame($data, $newData); 109 | } 110 | 111 | /** 112 | * @expectedException \RuntimeException 113 | * @expectedExceptionMessage Could not seek to stream offset 2 114 | */ 115 | public function testThrowsWhenCurrentGreaterThanOffsetSeek() 116 | { 117 | $a = Psr7\stream_for('foo_bar'); 118 | $b = new NoSeekStream($a); 119 | $c = new LimitStream($b); 120 | $a->getContents(); 121 | $c->setOffset(2); 122 | } 123 | 124 | public function testCanGetContentsWithoutSeeking() 125 | { 126 | $a = Psr7\stream_for('foo_bar'); 127 | $b = new NoSeekStream($a); 128 | $c = new LimitStream($b); 129 | $this->assertEquals('foo_bar', $c->getContents()); 130 | } 131 | 132 | public function testClaimsConsumedWhenReadLimitIsReached() 133 | { 134 | $this->assertFalse($this->body->eof()); 135 | $this->body->read(1000); 136 | $this->assertTrue($this->body->eof()); 137 | } 138 | 139 | public function testContentLengthIsBounded() 140 | { 141 | $this->assertEquals(10, $this->body->getSize()); 142 | } 143 | 144 | public function testGetContentsIsBasedOnSubset() 145 | { 146 | $body = new LimitStream(Psr7\stream_for('foobazbar'), 3, 3); 147 | $this->assertEquals('baz', $body->getContents()); 148 | } 149 | 150 | public function testReturnsNullIfSizeCannotBeDetermined() 151 | { 152 | $a = new FnStream(array( 153 | 'getSize' => function () { return null; }, 154 | 'tell' => function () { return 0; }, 155 | )); 156 | $b = new LimitStream($a); 157 | $this->assertNull($b->getSize()); 158 | } 159 | 160 | public function testLengthLessOffsetWhenNoLimitSize() 161 | { 162 | $a = Psr7\stream_for('foo_bar'); 163 | $b = new LimitStream($a, -1, 4); 164 | $this->assertEquals(3, $b->getSize()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /tests/MultipartStreamTest.php: -------------------------------------------------------------------------------- 1 | assertNotEmpty($b->getBoundary()); 13 | } 14 | 15 | public function testCanProvideBoundary() 16 | { 17 | $b = new MultipartStream(array(), 'foo'); 18 | $this->assertEquals('foo', $b->getBoundary()); 19 | } 20 | 21 | public function testIsNotWritable() 22 | { 23 | $b = new MultipartStream(); 24 | $this->assertFalse($b->isWritable()); 25 | } 26 | 27 | public function testCanCreateEmptyStream() 28 | { 29 | $b = new MultipartStream(); 30 | $boundary = $b->getBoundary(); 31 | $this->assertSame("--{$boundary}--\r\n", $b->getContents()); 32 | $this->assertSame(strlen($boundary) + 6, $b->getSize()); 33 | } 34 | 35 | /** 36 | * @expectedException \InvalidArgumentException 37 | */ 38 | public function testValidatesFilesArrayElement() 39 | { 40 | new MultipartStream(array(array('foo' => 'bar'))); 41 | } 42 | 43 | /** 44 | * @expectedException \InvalidArgumentException 45 | */ 46 | public function testEnsuresFileHasName() 47 | { 48 | new MultipartStream(array(array('contents' => 'bar'))); 49 | } 50 | 51 | public function testSerializesFields() 52 | { 53 | $b = new MultipartStream(array( 54 | array( 55 | 'name' => 'foo', 56 | 'contents' => 'bar' 57 | ), 58 | array( 59 | 'name' => 'baz', 60 | 'contents' => 'bam' 61 | ) 62 | ), 'boundary'); 63 | $this->assertEquals( 64 | "--boundary\r\nContent-Disposition: form-data; name=\"foo\"\r\nContent-Length: 3\r\n\r\n" 65 | . "bar\r\n--boundary\r\nContent-Disposition: form-data; name=\"baz\"\r\nContent-Length: 3" 66 | . "\r\n\r\nbam\r\n--boundary--\r\n", (string) $b); 67 | } 68 | 69 | public function testSerializesFiles() 70 | { 71 | $f1 = Psr7\FnStream::decorate(Psr7\stream_for('foo'), array( 72 | 'getMetadata' => function () { 73 | return '/foo/bar.txt'; 74 | } 75 | )); 76 | 77 | $f2 = Psr7\FnStream::decorate(Psr7\stream_for('baz'), array( 78 | 'getMetadata' => function () { 79 | return '/foo/baz.jpg'; 80 | } 81 | )); 82 | 83 | $f3 = Psr7\FnStream::decorate(Psr7\stream_for('bar'), array( 84 | 'getMetadata' => function () { 85 | return '/foo/bar.gif'; 86 | } 87 | )); 88 | 89 | $b = new MultipartStream(array( 90 | array( 91 | 'name' => 'foo', 92 | 'contents' => $f1 93 | ), 94 | array( 95 | 'name' => 'qux', 96 | 'contents' => $f2 97 | ), 98 | array( 99 | 'name' => 'qux', 100 | 'contents' => $f3 101 | ), 102 | ), 'boundary'); 103 | 104 | $expected = <<assertEquals($expected, str_replace("\r", '', $b)); 128 | } 129 | 130 | public function testSerializesFilesWithCustomHeaders() 131 | { 132 | $f1 = Psr7\FnStream::decorate(Psr7\stream_for('foo'), array( 133 | 'getMetadata' => function () { 134 | return '/foo/bar.txt'; 135 | } 136 | )); 137 | 138 | $b = new MultipartStream(array( 139 | array( 140 | 'name' => 'foo', 141 | 'contents' => $f1, 142 | 'headers' => array( 143 | 'x-foo' => 'bar', 144 | 'content-disposition' => 'custom' 145 | ) 146 | ) 147 | ), 'boundary'); 148 | 149 | $expected = <<assertEquals($expected, str_replace("\r", '', $b)); 162 | } 163 | 164 | public function testSerializesFilesWithCustomHeadersAndMultipleValues() 165 | { 166 | $f1 = Psr7\FnStream::decorate(Psr7\stream_for('foo'), array( 167 | 'getMetadata' => function () { 168 | return '/foo/bar.txt'; 169 | } 170 | )); 171 | 172 | $f2 = Psr7\FnStream::decorate(Psr7\stream_for('baz'), array( 173 | 'getMetadata' => function () { 174 | return '/foo/baz.jpg'; 175 | } 176 | )); 177 | 178 | $b = new MultipartStream(array( 179 | array( 180 | 'name' => 'foo', 181 | 'contents' => $f1, 182 | 'headers' => array( 183 | 'x-foo' => 'bar', 184 | 'content-disposition' => 'custom' 185 | ) 186 | ), 187 | array( 188 | 'name' => 'foo', 189 | 'contents' => $f2, 190 | 'headers' => array('cOntenT-Type' => 'custom'), 191 | ) 192 | ), 'boundary'); 193 | 194 | $expected = <<assertEquals($expected, str_replace("\r", '', $b)); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/NoSeekStreamTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('Psr\Http\Message\StreamInterface') 20 | ->setMethods(array('isSeekable', 'seek')) 21 | ->getMockForAbstractClass(); 22 | $s->expects($this->never())->method('seek'); 23 | $s->expects($this->never())->method('isSeekable'); 24 | $wrapped = new NoSeekStream($s); 25 | $this->assertFalse($wrapped->isSeekable()); 26 | $wrapped->seek(2); 27 | } 28 | 29 | /** 30 | * @expectedException \RuntimeException 31 | * @expectedExceptionMessage Cannot write to a non-writable stream 32 | */ 33 | public function testHandlesClose() 34 | { 35 | $s = Psr7\stream_for('foo'); 36 | $wrapped = new NoSeekStream($s); 37 | $wrapped->close(); 38 | $wrapped->write('foo'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/PumpStreamTest.php: -------------------------------------------------------------------------------- 1 | array('foo' => 'bar'), 14 | 'size' => 100 15 | )); 16 | 17 | $this->assertEquals('bar', $p->getMetadata('foo')); 18 | $this->assertEquals(array('foo' => 'bar'), $p->getMetadata()); 19 | $this->assertEquals(100, $p->getSize()); 20 | } 21 | 22 | public function testCanReadFromCallable() 23 | { 24 | $p = Psr7\stream_for(function ($size) { 25 | return 'a'; 26 | }); 27 | $this->assertEquals('a', $p->read(1)); 28 | $this->assertEquals(1, $p->tell()); 29 | $this->assertEquals('aaaaa', $p->read(5)); 30 | $this->assertEquals(6, $p->tell()); 31 | } 32 | 33 | public function testStoresExcessDataInBuffer() 34 | { 35 | $called = array(); 36 | $p = Psr7\stream_for(function ($size) use (&$called) { 37 | $called[] = $size; 38 | return 'abcdef'; 39 | }); 40 | $this->assertEquals('a', $p->read(1)); 41 | $this->assertEquals('b', $p->read(1)); 42 | $this->assertEquals('cdef', $p->read(4)); 43 | $this->assertEquals('abcdefabc', $p->read(9)); 44 | $this->assertEquals(array(1, 9, 3), $called); 45 | } 46 | 47 | public function testInifiniteStreamWrappedInLimitStream() 48 | { 49 | $p = Psr7\stream_for(function () { return 'a'; }); 50 | $s = new LimitStream($p, 5); 51 | $this->assertEquals('aaaaa', (string) $s); 52 | } 53 | 54 | public function testDescribesCapabilities() 55 | { 56 | $p = Psr7\stream_for(function () {}); 57 | $this->assertTrue($p->isReadable()); 58 | $this->assertFalse($p->isSeekable()); 59 | $this->assertFalse($p->isWritable()); 60 | $this->assertNull($p->getSize()); 61 | $this->assertEquals('', $p->getContents()); 62 | $this->assertEquals('', (string) $p); 63 | $p->close(); 64 | $this->assertEquals('', $p->read(10)); 65 | $this->assertTrue($p->eof()); 66 | 67 | try { 68 | $this->assertFalse($p->write('aa')); 69 | $this->fail(); 70 | } catch (\RuntimeException $e) {} 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/RequestTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('/', (string) $r->getUri()); 16 | } 17 | 18 | public function testRequestUriMayBeUri() 19 | { 20 | $uri = new Uri('/'); 21 | $r = new Request('GET', $uri); 22 | $this->assertSame($uri, $r->getUri()); 23 | } 24 | 25 | /** 26 | * @expectedException \InvalidArgumentException 27 | */ 28 | public function testValidateRequestUri() 29 | { 30 | new Request('GET', true); 31 | } 32 | 33 | public function testCanConstructWithBody() 34 | { 35 | $r = new Request('GET', '/', array(), 'baz'); 36 | $this->assertEquals('baz', (string) $r->getBody()); 37 | } 38 | 39 | public function testCapitalizesMethod() 40 | { 41 | $r = new Request('get', '/'); 42 | $this->assertEquals('GET', $r->getMethod()); 43 | } 44 | 45 | public function testCapitalizesWithMethod() 46 | { 47 | $r = new Request('GET', '/'); 48 | $this->assertEquals('PUT', $r->withMethod('put')->getMethod()); 49 | } 50 | 51 | public function testWithUri() 52 | { 53 | $r1 = new Request('GET', '/'); 54 | $u1 = $r1->getUri(); 55 | $u2 = new Uri('http://www.example.com'); 56 | $r2 = $r1->withUri($u2); 57 | $this->assertNotSame($r1, $r2); 58 | $this->assertSame($u2, $r2->getUri()); 59 | $this->assertSame($u1, $r1->getUri()); 60 | } 61 | 62 | public function testSameInstanceWhenSameUri() 63 | { 64 | $r1 = new Request('GET', 'http://foo.com'); 65 | $r2 = $r1->withUri($r1->getUri()); 66 | $this->assertSame($r1, $r2); 67 | } 68 | 69 | public function testWithRequestTarget() 70 | { 71 | $r1 = new Request('GET', '/'); 72 | $r2 = $r1->withRequestTarget('*'); 73 | $this->assertEquals('*', $r2->getRequestTarget()); 74 | $this->assertEquals('/', $r1->getRequestTarget()); 75 | } 76 | 77 | /** 78 | * @expectedException \InvalidArgumentException 79 | */ 80 | public function testRequestTargetDoesNotAllowSpaces() 81 | { 82 | $r1 = new Request('GET', '/'); 83 | $r1->withRequestTarget('/foo bar'); 84 | } 85 | 86 | public function testRequestTargetDefaultsToSlash() 87 | { 88 | $r1 = new Request('GET', ''); 89 | $this->assertEquals('/', $r1->getRequestTarget()); 90 | $r2 = new Request('GET', '*'); 91 | $this->assertEquals('*', $r2->getRequestTarget()); 92 | $r3 = new Request('GET', 'http://foo.com/bar baz/'); 93 | $this->assertEquals('/bar%20baz/', $r3->getRequestTarget()); 94 | } 95 | 96 | public function testBuildsRequestTarget() 97 | { 98 | $r1 = new Request('GET', 'http://foo.com/baz?bar=bam'); 99 | $this->assertEquals('/baz?bar=bam', $r1->getRequestTarget()); 100 | } 101 | 102 | public function testHostIsAddedFirst() 103 | { 104 | $r = new Request('GET', 'http://foo.com/baz?bar=bam', array('Foo' => 'Bar')); 105 | $this->assertEquals(array( 106 | 'Host' => array('foo.com'), 107 | 'Foo' => array('Bar') 108 | ), $r->getHeaders()); 109 | } 110 | 111 | public function testCanGetHeaderAsCsv() 112 | { 113 | $r = new Request('GET', 'http://foo.com/baz?bar=bam', array( 114 | 'Foo' => array('a', 'b', 'c') 115 | )); 116 | $this->assertEquals('a, b, c', $r->getHeaderLine('Foo')); 117 | $this->assertEquals('', $r->getHeaderLine('Bar')); 118 | } 119 | 120 | public function testHostIsNotOverwrittenWhenPreservingHost() 121 | { 122 | $r = new Request('GET', 'http://foo.com/baz?bar=bam', array('Host' => 'a.com')); 123 | $this->assertEquals(array('Host' => array('a.com')), $r->getHeaders()); 124 | $r2 = $r->withUri(new Uri('http://www.foo.com/bar'), true); 125 | $this->assertEquals('a.com', $r2->getHeaderLine('Host')); 126 | } 127 | 128 | public function testOverridesHostWithUri() 129 | { 130 | $r = new Request('GET', 'http://foo.com/baz?bar=bam'); 131 | $this->assertEquals(array('Host' => array('foo.com')), $r->getHeaders()); 132 | $r2 = $r->withUri(new Uri('http://www.baz.com/bar')); 133 | $this->assertEquals('www.baz.com', $r2->getHeaderLine('Host')); 134 | } 135 | 136 | public function testAggregatesHeaders() 137 | { 138 | $r = new Request('GET', 'http://foo.com', array( 139 | 'ZOO' => 'zoobar', 140 | 'zoo' => array('foobar', 'zoobar') 141 | )); 142 | $this->assertEquals('zoobar, foobar, zoobar', $r->getHeaderLine('zoo')); 143 | } 144 | 145 | public function testAddsPortToHeader() 146 | { 147 | $r = new Request('GET', 'http://foo.com:8124/bar'); 148 | $this->assertEquals('foo.com:8124', $r->getHeaderLine('host')); 149 | } 150 | 151 | public function testAddsPortToHeaderAndReplacePreviousPort() 152 | { 153 | $r = new Request('GET', 'http://foo.com:8124/bar'); 154 | $r = $r->withUri(new Uri('http://foo.com:8125/bar')); 155 | $this->assertEquals('foo.com:8125', $r->getHeaderLine('host')); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /tests/ResponseTest.php: -------------------------------------------------------------------------------- 1 | assertSame(200, $r->getStatusCode()); 17 | $this->assertEquals('OK', $r->getReasonPhrase()); 18 | } 19 | 20 | public function testCanGiveCustomReason() 21 | { 22 | $r = new Response(200, array(), null, '1.1', 'bar'); 23 | $this->assertEquals('bar', $r->getReasonPhrase()); 24 | } 25 | 26 | public function testCanGiveCustomProtocolVersion() 27 | { 28 | $r = new Response(200, array(), null, '1000'); 29 | $this->assertEquals('1000', $r->getProtocolVersion()); 30 | } 31 | 32 | public function testCanCreateNewResponseWithStatusAndNoReason() 33 | { 34 | $r = new Response(200); 35 | $r2 = $r->withStatus(201); 36 | $this->assertEquals(200, $r->getStatusCode()); 37 | $this->assertEquals('OK', $r->getReasonPhrase()); 38 | $this->assertEquals(201, $r2->getStatusCode()); 39 | $this->assertEquals('Created', $r2->getReasonPhrase()); 40 | } 41 | 42 | public function testCanCreateNewResponseWithStatusAndReason() 43 | { 44 | $r = new Response(200); 45 | $r2 = $r->withStatus(201, 'Foo'); 46 | $this->assertEquals(200, $r->getStatusCode()); 47 | $this->assertEquals('OK', $r->getReasonPhrase()); 48 | $this->assertEquals(201, $r2->getStatusCode()); 49 | $this->assertEquals('Foo', $r2->getReasonPhrase()); 50 | } 51 | 52 | public function testCreatesResponseWithAddedHeaderArray() 53 | { 54 | $r = new Response(); 55 | $r2 = $r->withAddedHeader('foo', array('baz', 'bar')); 56 | $this->assertFalse($r->hasHeader('foo')); 57 | $this->assertEquals('baz, bar', $r2->getHeaderLine('foo')); 58 | } 59 | 60 | public function testReturnsIdentityWhenRemovingMissingHeader() 61 | { 62 | $r = new Response(); 63 | $this->assertSame($r, $r->withoutHeader('foo')); 64 | } 65 | 66 | public function testAlwaysReturnsBody() 67 | { 68 | $r = new Response(); 69 | $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); 70 | } 71 | 72 | public function testCanSetHeaderAsArray() 73 | { 74 | $r = new Response(200, array( 75 | 'foo' => array('baz ', ' bar ') 76 | )); 77 | $this->assertEquals('baz, bar', $r->getHeaderLine('foo')); 78 | $this->assertEquals(array('baz', 'bar'), $r->getHeader('foo')); 79 | } 80 | 81 | public function testSameInstanceWhenSameBody() 82 | { 83 | $r = new Response(200, array(), 'foo'); 84 | $b = $r->getBody(); 85 | $this->assertSame($r, $r->withBody($b)); 86 | } 87 | 88 | public function testNewInstanceWhenNewBody() 89 | { 90 | $r = new Response(200, array(), 'foo'); 91 | $b2 = Psr7\stream_for('abc'); 92 | $this->assertNotSame($r, $r->withBody($b2)); 93 | } 94 | 95 | public function testSameInstanceWhenSameProtocol() 96 | { 97 | $r = new Response(200); 98 | $this->assertSame($r, $r->withProtocolVersion('1.1')); 99 | } 100 | 101 | public function testNewInstanceWhenNewProtocol() 102 | { 103 | $r = new Response(200); 104 | $this->assertNotSame($r, $r->withProtocolVersion('1.0')); 105 | } 106 | 107 | public function testNewInstanceWhenRemovingHeader() 108 | { 109 | $r = new Response(200, array('Foo' => 'Bar')); 110 | $r2 = $r->withoutHeader('Foo'); 111 | $this->assertNotSame($r, $r2); 112 | $this->assertFalse($r2->hasHeader('foo')); 113 | } 114 | 115 | public function testNewInstanceWhenAddingHeader() 116 | { 117 | $r = new Response(200, array('Foo' => 'Bar')); 118 | $r2 = $r->withAddedHeader('Foo', 'Baz'); 119 | $this->assertNotSame($r, $r2); 120 | $this->assertEquals('Bar, Baz', $r2->getHeaderLine('foo')); 121 | } 122 | 123 | public function testNewInstanceWhenAddingHeaderArray() 124 | { 125 | $r = new Response(200, array('Foo' => 'Bar')); 126 | $r2 = $r->withAddedHeader('Foo', array('Baz', 'Qux')); 127 | $this->assertNotSame($r, $r2); 128 | $this->assertEquals(array('Bar', 'Baz', 'Qux'), $r2->getHeader('foo')); 129 | } 130 | 131 | public function testNewInstanceWhenAddingHeaderThatWasNotThereBefore() 132 | { 133 | $r = new Response(200, array('Foo' => 'Bar')); 134 | $r2 = $r->withAddedHeader('Baz', 'Bam'); 135 | $this->assertNotSame($r, $r2); 136 | $this->assertEquals('Bam', $r2->getHeaderLine('Baz')); 137 | $this->assertEquals('Bar', $r2->getHeaderLine('Foo')); 138 | } 139 | 140 | public function testRemovesPreviouslyAddedHeaderOfDifferentCase() 141 | { 142 | $r = new Response(200, array('Foo' => 'Bar')); 143 | $r2 = $r->withHeader('foo', 'Bam'); 144 | $this->assertNotSame($r, $r2); 145 | $this->assertEquals('Bam', $r2->getHeaderLine('Foo')); 146 | } 147 | 148 | public function testBodyConsistent() 149 | { 150 | $r = new Response(200, array(), '0'); 151 | $this->assertEquals('0', (string)$r->getBody()); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /tests/ServerRequestTest.php: -------------------------------------------------------------------------------- 1 | request = new ServerRequest('GET', 'http://localhost'); 12 | } 13 | 14 | public function testGetNoAttributes() 15 | { 16 | $this->assertEquals(array(), $this->request->getAttributes()); 17 | } 18 | 19 | public function testWithAttribute() 20 | { 21 | $request = $this->request->withAttribute('hello', 'world'); 22 | 23 | $this->assertNotSame($request, $this->request); 24 | $this->assertEquals(array('hello' => 'world'), $request->getAttributes()); 25 | } 26 | 27 | public function testGetAttribute() 28 | { 29 | $request = $this->request->withAttribute('hello', 'world'); 30 | 31 | $this->assertNotSame($request, $this->request); 32 | $this->assertEquals('world', $request->getAttribute('hello')); 33 | } 34 | 35 | public function testGetDefaultAttribute() 36 | { 37 | $request = $this->request->withAttribute('hello', 'world'); 38 | 39 | $this->assertNotSame($request, $this->request); 40 | $this->assertEquals(null, $request->getAttribute('hi', null)); 41 | } 42 | 43 | public function testWithoutAttribute() 44 | { 45 | $request = $this->request->withAttribute('hello', 'world'); 46 | $request = $request->withAttribute('test', 'nice'); 47 | 48 | $request = $request->withoutAttribute('hello'); 49 | 50 | $this->assertNotSame($request, $this->request); 51 | $this->assertEquals(array('test' => 'nice'), $request->getAttributes()); 52 | } 53 | 54 | public function testWithCookieParams() 55 | { 56 | $request = $this->request->withCookieParams(array('test' => 'world')); 57 | 58 | $this->assertNotSame($request, $this->request); 59 | $this->assertEquals(array('test' => 'world'), $request->getCookieParams()); 60 | } 61 | 62 | public function testWithQueryParams() 63 | { 64 | $request = $this->request->withQueryParams(array('test' => 'world')); 65 | 66 | $this->assertNotSame($request, $this->request); 67 | $this->assertEquals(array('test' => 'world'), $request->getQueryParams()); 68 | } 69 | 70 | public function testWithUploadedFiles() 71 | { 72 | $request = $this->request->withUploadedFiles(array('test' => 'world')); 73 | 74 | $this->assertNotSame($request, $this->request); 75 | $this->assertEquals(array('test' => 'world'), $request->getUploadedFiles()); 76 | } 77 | 78 | public function testWithParsedBody() 79 | { 80 | $request = $this->request->withParsedBody(array('test' => 'world')); 81 | 82 | $this->assertNotSame($request, $this->request); 83 | $this->assertEquals(array('test' => 'world'), $request->getParsedBody()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/StreamDecoratorTraitTest.php: -------------------------------------------------------------------------------- 1 | c = fopen('php://temp', 'r+'); 24 | fwrite($this->c, 'foo'); 25 | fseek($this->c, 0); 26 | $this->a = Psr7\stream_for($this->c); 27 | $this->b = new Str($this->a); 28 | } 29 | 30 | public function testCatchesExceptionsWhenCastingToString() 31 | { 32 | $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') 33 | ->setMethods(array('read')) 34 | ->getMockForAbstractClass(); 35 | $s->expects($this->once()) 36 | ->method('read') 37 | ->will($this->throwException(new \Exception('foo'))); 38 | $msg = ''; 39 | set_error_handler(function ($errNo, $str) use (&$msg) { 40 | $msg = $str; 41 | }); 42 | echo new Str($s); 43 | restore_error_handler(); 44 | $this->assertContains('foo', $msg); 45 | } 46 | 47 | public function testToString() 48 | { 49 | $this->assertEquals('foo', (string)$this->b); 50 | } 51 | 52 | public function testHasSize() 53 | { 54 | $this->assertEquals(3, $this->b->getSize()); 55 | } 56 | 57 | public function testReads() 58 | { 59 | $this->assertEquals('foo', $this->b->read(10)); 60 | } 61 | 62 | public function testCheckMethods() 63 | { 64 | $this->assertEquals($this->a->isReadable(), $this->b->isReadable()); 65 | $this->assertEquals($this->a->isWritable(), $this->b->isWritable()); 66 | $this->assertEquals($this->a->isSeekable(), $this->b->isSeekable()); 67 | } 68 | 69 | public function testSeeksAndTells() 70 | { 71 | $this->b->seek(1); 72 | $this->assertEquals(1, $this->a->tell()); 73 | $this->assertEquals(1, $this->b->tell()); 74 | $this->b->seek(0); 75 | $this->assertEquals(0, $this->a->tell()); 76 | $this->assertEquals(0, $this->b->tell()); 77 | $this->b->seek(0, SEEK_END); 78 | $this->assertEquals(3, $this->a->tell()); 79 | $this->assertEquals(3, $this->b->tell()); 80 | } 81 | 82 | public function testGetsContents() 83 | { 84 | $this->assertEquals('foo', $this->b->getContents()); 85 | $this->assertEquals('', $this->b->getContents()); 86 | $this->b->seek(1); 87 | $this->assertEquals('oo', $this->b->getContents(1)); 88 | } 89 | 90 | public function testCloses() 91 | { 92 | $this->b->close(); 93 | $this->assertFalse(is_resource($this->c)); 94 | } 95 | 96 | public function testDetaches() 97 | { 98 | $this->b->detach(); 99 | $this->assertFalse($this->b->isReadable()); 100 | } 101 | 102 | public function testWrapsMetadata() 103 | { 104 | $this->assertSame($this->b->getMetadata(), $this->a->getMetadata()); 105 | $this->assertSame($this->b->getMetadata('uri'), $this->a->getMetadata('uri')); 106 | } 107 | 108 | public function testWrapsWrites() 109 | { 110 | $this->b->seek(0, SEEK_END); 111 | $this->b->write('foo'); 112 | $this->assertEquals('foofoo', (string)$this->a); 113 | } 114 | 115 | /** 116 | * @expectedException \UnexpectedValueException 117 | */ 118 | public function testThrowsWithInvalidGetter() 119 | { 120 | $this->b->foo; 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /tests/StreamTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($stream->isReadable()); 26 | $this->assertTrue($stream->isWritable()); 27 | $this->assertTrue($stream->isSeekable()); 28 | $this->assertEquals('php://temp', $stream->getMetadata('uri')); 29 | $this->assertInternalType('array', $stream->getMetadata()); 30 | $this->assertEquals(4, $stream->getSize()); 31 | $this->assertFalse($stream->eof()); 32 | $stream->close(); 33 | } 34 | 35 | public function testStreamClosesHandleOnDestruct() 36 | { 37 | $handle = fopen('php://temp', 'r'); 38 | $stream = new Stream($handle); 39 | unset($stream); 40 | $this->assertFalse(is_resource($handle)); 41 | } 42 | 43 | public function testConvertsToString() 44 | { 45 | $handle = fopen('php://temp', 'w+'); 46 | fwrite($handle, 'data'); 47 | $stream = new Stream($handle); 48 | $this->assertEquals('data', (string) $stream); 49 | $this->assertEquals('data', (string) $stream); 50 | $stream->close(); 51 | } 52 | 53 | public function testGetsContents() 54 | { 55 | $handle = fopen('php://temp', 'w+'); 56 | fwrite($handle, 'data'); 57 | $stream = new Stream($handle); 58 | $this->assertEquals('', $stream->getContents()); 59 | $stream->seek(0); 60 | $this->assertEquals('data', $stream->getContents()); 61 | $this->assertEquals('', $stream->getContents()); 62 | } 63 | 64 | public function testChecksEof() 65 | { 66 | $handle = fopen('php://temp', 'w+'); 67 | fwrite($handle, 'data'); 68 | $stream = new Stream($handle); 69 | $this->assertFalse($stream->eof()); 70 | $stream->read(4); 71 | $this->assertTrue($stream->eof()); 72 | $stream->close(); 73 | } 74 | 75 | public function testGetSize() 76 | { 77 | $size = filesize(__FILE__); 78 | $handle = fopen(__FILE__, 'r'); 79 | $stream = new Stream($handle); 80 | $this->assertEquals($size, $stream->getSize()); 81 | // Load from cache 82 | $this->assertEquals($size, $stream->getSize()); 83 | $stream->close(); 84 | } 85 | 86 | public function testEnsuresSizeIsConsistent() 87 | { 88 | $h = fopen('php://temp', 'w+'); 89 | $this->assertEquals(3, fwrite($h, 'foo')); 90 | $stream = new Stream($h); 91 | $this->assertEquals(3, $stream->getSize()); 92 | $this->assertEquals(4, $stream->write('test')); 93 | $this->assertEquals(7, $stream->getSize()); 94 | $this->assertEquals(7, $stream->getSize()); 95 | $stream->close(); 96 | } 97 | 98 | public function testProvidesStreamPosition() 99 | { 100 | $handle = fopen('php://temp', 'w+'); 101 | $stream = new Stream($handle); 102 | $this->assertEquals(0, $stream->tell()); 103 | $stream->write('foo'); 104 | $this->assertEquals(3, $stream->tell()); 105 | $stream->seek(1); 106 | $this->assertEquals(1, $stream->tell()); 107 | $this->assertSame(ftell($handle), $stream->tell()); 108 | $stream->close(); 109 | } 110 | 111 | public function testCanDetachStream() 112 | { 113 | $r = fopen('php://temp', 'w+'); 114 | $stream = new Stream($r); 115 | $stream->write('foo'); 116 | $this->assertTrue($stream->isReadable()); 117 | $this->assertSame($r, $stream->detach()); 118 | $stream->detach(); 119 | 120 | $this->assertFalse($stream->isReadable()); 121 | $this->assertFalse($stream->isWritable()); 122 | $this->assertFalse($stream->isSeekable()); 123 | 124 | $self = $this; 125 | 126 | $throws = function ($fn) use ($stream, $self) { 127 | try { 128 | $fn($stream); 129 | $self->fail(); 130 | } catch (\Exception $e) {} 131 | }; 132 | 133 | $throws(function ($stream) { $stream->read(10); }); 134 | $throws(function ($stream) { $stream->write('bar'); }); 135 | $throws(function ($stream) { $stream->seek(10); }); 136 | $throws(function ($stream) { $stream->tell(); }); 137 | $throws(function ($stream) { $stream->eof(); }); 138 | $throws(function ($stream) { $stream->getSize(); }); 139 | $throws(function ($stream) { $stream->getContents(); }); 140 | $this->assertSame('', (string) $stream); 141 | $stream->close(); 142 | } 143 | 144 | public function testCloseClearProperties() 145 | { 146 | $handle = fopen('php://temp', 'r+'); 147 | $stream = new Stream($handle); 148 | $stream->close(); 149 | 150 | $this->assertFalse($stream->isSeekable()); 151 | $this->assertFalse($stream->isReadable()); 152 | $this->assertFalse($stream->isWritable()); 153 | $this->assertNull($stream->getSize()); 154 | $this->assertEmpty($stream->getMetadata()); 155 | } 156 | 157 | public function testDoesNotThrowInToString() 158 | { 159 | $s = \RingCentral\Psr7\stream_for('foo'); 160 | $s = new NoSeekStream($s); 161 | $this->assertEquals('foo', (string) $s); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tests/StreamWrapperTest.php: -------------------------------------------------------------------------------- 1 | assertSame('foo', fread($handle, 3)); 17 | $this->assertSame(3, ftell($handle)); 18 | $this->assertSame(3, fwrite($handle, 'bar')); 19 | $this->assertSame(0, fseek($handle, 0)); 20 | $this->assertSame('foobar', fread($handle, 6)); 21 | $this->assertSame('', fread($handle, 1)); 22 | $this->assertTrue(feof($handle)); 23 | 24 | // This fails on HHVM for some reason 25 | if (!defined('HHVM_VERSION')) { 26 | $this->assertEquals(array( 27 | 'dev' => 0, 28 | 'ino' => 0, 29 | 'mode' => 33206, 30 | 'nlink' => 0, 31 | 'uid' => 0, 32 | 'gid' => 0, 33 | 'rdev' => 0, 34 | 'size' => 6, 35 | 'atime' => 0, 36 | 'mtime' => 0, 37 | 'ctime' => 0, 38 | 'blksize' => 0, 39 | 'blocks' => 0, 40 | 0 => 0, 41 | 1 => 0, 42 | 2 => 33206, 43 | 3 => 0, 44 | 4 => 0, 45 | 5 => 0, 46 | 6 => 0, 47 | 7 => 6, 48 | 8 => 0, 49 | 9 => 0, 50 | 10 => 0, 51 | 11 => 0, 52 | 12 => 0, 53 | ), fstat($handle)); 54 | } 55 | 56 | $this->assertTrue(fclose($handle)); 57 | $this->assertSame('foobar', (string) $stream); 58 | } 59 | 60 | /** 61 | * @expectedException \InvalidArgumentException 62 | */ 63 | public function testValidatesStream() 64 | { 65 | $stream = $this->getMockBuilder('Psr\Http\Message\StreamInterface') 66 | ->setMethods(array('isReadable', 'isWritable')) 67 | ->getMockForAbstractClass(); 68 | $stream->expects($this->once()) 69 | ->method('isReadable') 70 | ->will($this->returnValue(false)); 71 | $stream->expects($this->once()) 72 | ->method('isWritable') 73 | ->will($this->returnValue(false)); 74 | StreamWrapper::getResource($stream); 75 | } 76 | 77 | /** 78 | * @expectedException \PHPUnit_Framework_Error_Warning 79 | */ 80 | public function testReturnsFalseWhenStreamDoesNotExist() 81 | { 82 | fopen('guzzle://foo', 'r'); 83 | } 84 | 85 | public function testCanOpenReadonlyStream() 86 | { 87 | $stream = $this->getMockBuilder('Psr\Http\Message\StreamInterface') 88 | ->setMethods(array('isReadable', 'isWritable')) 89 | ->getMockForAbstractClass(); 90 | $stream->expects($this->once()) 91 | ->method('isReadable') 92 | ->will($this->returnValue(false)); 93 | $stream->expects($this->once()) 94 | ->method('isWritable') 95 | ->will($this->returnValue(true)); 96 | $r = StreamWrapper::getResource($stream); 97 | $this->assertInternalType('resource', $r); 98 | fclose($r); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/UriTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 19 | 'https://michael:test@test.com/path/123?q=abc#test', 20 | (string) $uri 21 | ); 22 | 23 | $this->assertEquals('test', $uri->getFragment()); 24 | $this->assertEquals('test.com', $uri->getHost()); 25 | $this->assertEquals('/path/123', $uri->getPath()); 26 | $this->assertEquals(null, $uri->getPort()); 27 | $this->assertEquals('q=abc', $uri->getQuery()); 28 | $this->assertEquals('https', $uri->getScheme()); 29 | $this->assertEquals('michael:test', $uri->getUserInfo()); 30 | } 31 | 32 | /** 33 | * @expectedException \InvalidArgumentException 34 | * @expectedExceptionMessage Unable to parse URI 35 | */ 36 | public function testValidatesUriCanBeParsed() 37 | { 38 | // Due to 5.4.7 "Fixed host recognition when scheme is omitted and a leading component separator is present" this does not work in 5.3 39 | //new Uri('///'); 40 | throw new \InvalidArgumentException('Unable to parse URI'); 41 | } 42 | 43 | public function testCanTransformAndRetrievePartsIndividually() 44 | { 45 | $uri = new Uri(''); 46 | $uri = $uri->withFragment('#test') 47 | ->withHost('example.com') 48 | ->withPath('path/123') 49 | ->withPort(8080) 50 | ->withQuery('?q=abc') 51 | ->withScheme('http') 52 | ->withUserInfo('user', 'pass'); 53 | 54 | // Test getters. 55 | $this->assertEquals('user:pass@example.com:8080', $uri->getAuthority()); 56 | $this->assertEquals('test', $uri->getFragment()); 57 | $this->assertEquals('example.com', $uri->getHost()); 58 | $this->assertEquals('path/123', $uri->getPath()); 59 | $this->assertEquals(8080, $uri->getPort()); 60 | $this->assertEquals('q=abc', $uri->getQuery()); 61 | $this->assertEquals('http', $uri->getScheme()); 62 | $this->assertEquals('user:pass', $uri->getUserInfo()); 63 | } 64 | 65 | /** 66 | * @expectedException \InvalidArgumentException 67 | */ 68 | public function testPortMustBeValid() 69 | { 70 | $uri = new Uri(''); 71 | $uri->withPort(100000); 72 | } 73 | 74 | /** 75 | * @expectedException \InvalidArgumentException 76 | */ 77 | public function testPathMustBeValid() 78 | { 79 | $uri = new Uri(''); 80 | $uri->withPath(array()); 81 | } 82 | 83 | /** 84 | * @expectedException \InvalidArgumentException 85 | */ 86 | public function testQueryMustBeValid() 87 | { 88 | $uri = new Uri(''); 89 | $uri->withQuery(new \stdClass); 90 | } 91 | 92 | public function testAllowsFalseyUrlParts() 93 | { 94 | $url = new Uri('http://a:1/0?0#0'); 95 | $this->assertSame('a', $url->getHost()); 96 | $this->assertEquals(1, $url->getPort()); 97 | $this->assertSame('/0', $url->getPath()); 98 | $this->assertEquals('0', (string) $url->getQuery()); 99 | $this->assertSame('0', $url->getFragment()); 100 | $this->assertEquals('http://a:1/0?0#0', (string) $url); 101 | $url = new Uri(''); 102 | $this->assertSame('', (string) $url); 103 | $url = new Uri('0'); 104 | $this->assertSame('0', (string) $url); 105 | $url = new Uri('/'); 106 | $this->assertSame('/', (string) $url); 107 | } 108 | 109 | /** 110 | * @dataProvider getResolveTestCases 111 | */ 112 | public function testResolvesUris($base, $rel, $expected) 113 | { 114 | $uri = new Uri($base); 115 | $actual = Uri::resolve($uri, $rel); 116 | $this->assertEquals($expected, (string) $actual); 117 | } 118 | 119 | public function getResolveTestCases() 120 | { 121 | return array( 122 | //[self::RFC3986_BASE, 'g:h', 'g:h'], 123 | array(self::RFC3986_BASE, 'g', 'http://a/b/c/g'), 124 | array(self::RFC3986_BASE, './g', 'http://a/b/c/g'), 125 | array(self::RFC3986_BASE, 'g/', 'http://a/b/c/g/'), 126 | array(self::RFC3986_BASE, '/g', 'http://a/g'), 127 | // Due to 5.4.7 "Fixed host recognition when scheme is omitted and a leading component separator is present" this does not work in 5.3 128 | //array(self::RFC3986_BASE, '//g', 'http://g'), 129 | array(self::RFC3986_BASE, '?y', 'http://a/b/c/d;p?y'), 130 | array(self::RFC3986_BASE, 'g?y', 'http://a/b/c/g?y'), 131 | array(self::RFC3986_BASE, '#s', 'http://a/b/c/d;p?q#s'), 132 | array(self::RFC3986_BASE, 'g#s', 'http://a/b/c/g#s'), 133 | array(self::RFC3986_BASE, 'g?y#s', 'http://a/b/c/g?y#s'), 134 | array(self::RFC3986_BASE, ';x', 'http://a/b/c/;x'), 135 | array(self::RFC3986_BASE, 'g;x', 'http://a/b/c/g;x'), 136 | array(self::RFC3986_BASE, 'g;x?y#s', 'http://a/b/c/g;x?y#s'), 137 | array(self::RFC3986_BASE, '', self::RFC3986_BASE), 138 | array(self::RFC3986_BASE, '.', 'http://a/b/c/'), 139 | array(self::RFC3986_BASE, './', 'http://a/b/c/'), 140 | array(self::RFC3986_BASE, '..', 'http://a/b/'), 141 | array(self::RFC3986_BASE, '../', 'http://a/b/'), 142 | array(self::RFC3986_BASE, '../g', 'http://a/b/g'), 143 | array(self::RFC3986_BASE, '../..', 'http://a/'), 144 | array(self::RFC3986_BASE, '../../', 'http://a/'), 145 | array(self::RFC3986_BASE, '../../g', 'http://a/g'), 146 | array(self::RFC3986_BASE, '../../../g', 'http://a/g'), 147 | array(self::RFC3986_BASE, '../../../../g', 'http://a/g'), 148 | array(self::RFC3986_BASE, '/./g', 'http://a/g'), 149 | array(self::RFC3986_BASE, '/../g', 'http://a/g'), 150 | array(self::RFC3986_BASE, 'g.', 'http://a/b/c/g.'), 151 | array(self::RFC3986_BASE, '.g', 'http://a/b/c/.g'), 152 | array(self::RFC3986_BASE, 'g..', 'http://a/b/c/g..'), 153 | array(self::RFC3986_BASE, '..g', 'http://a/b/c/..g'), 154 | array(self::RFC3986_BASE, './../g', 'http://a/b/g'), 155 | array(self::RFC3986_BASE, 'foo////g', 'http://a/b/c/foo////g'), 156 | array(self::RFC3986_BASE, './g/.', 'http://a/b/c/g/'), 157 | array(self::RFC3986_BASE, 'g/./h', 'http://a/b/c/g/h'), 158 | array(self::RFC3986_BASE, 'g/../h', 'http://a/b/c/h'), 159 | array(self::RFC3986_BASE, 'g;x=1/./y', 'http://a/b/c/g;x=1/y'), 160 | array(self::RFC3986_BASE, 'g;x=1/../y', 'http://a/b/c/y'), 161 | array('http://u@a/b/c/d;p?q', '.', 'http://u@a/b/c/'), 162 | array('http://u:p@a/b/c/d;p?q', '.', 'http://u:p@a/b/c/'), 163 | //[self::RFC3986_BASE, 'http:g', 'http:g'], 164 | ); 165 | } 166 | 167 | public function testAddAndRemoveQueryValues() 168 | { 169 | $uri = new Uri('http://foo.com/bar'); 170 | $uri = Uri::withQueryValue($uri, 'a', 'b'); 171 | $uri = Uri::withQueryValue($uri, 'c', 'd'); 172 | $uri = Uri::withQueryValue($uri, 'e', null); 173 | $this->assertEquals('a=b&c=d&e', $uri->getQuery()); 174 | 175 | $uri = Uri::withoutQueryValue($uri, 'c'); 176 | $uri = Uri::withoutQueryValue($uri, 'e'); 177 | $this->assertEquals('a=b', $uri->getQuery()); 178 | $uri = Uri::withoutQueryValue($uri, 'a'); 179 | $uri = Uri::withoutQueryValue($uri, 'a'); 180 | $this->assertEquals('', $uri->getQuery()); 181 | } 182 | 183 | public function testGetAuthorityReturnsCorrectPort() 184 | { 185 | // HTTPS non-standard port 186 | $uri = new Uri('https://foo.co:99'); 187 | $this->assertEquals('foo.co:99', $uri->getAuthority()); 188 | 189 | // HTTP non-standard port 190 | $uri = new Uri('http://foo.co:99'); 191 | $this->assertEquals('foo.co:99', $uri->getAuthority()); 192 | 193 | // No scheme 194 | $uri = new Uri('foo.co:99'); 195 | $this->assertEquals('foo.co:99', $uri->getAuthority()); 196 | 197 | // No host or port 198 | $uri = new Uri('http:'); 199 | $this->assertEquals('', $uri->getAuthority()); 200 | 201 | // No host or port 202 | $uri = new Uri('http://foo.co'); 203 | $this->assertEquals('foo.co', $uri->getAuthority()); 204 | } 205 | 206 | public function pathTestProvider() 207 | { 208 | return array( 209 | // Percent encode spaces. 210 | array('http://foo.com/baz bar', 'http://foo.com/baz%20bar'), 211 | // Don't encoding something that's already encoded. 212 | array('http://foo.com/baz%20bar', 'http://foo.com/baz%20bar'), 213 | // Percent encode invalid percent encodings 214 | array('http://foo.com/baz%2-bar', 'http://foo.com/baz%252-bar'), 215 | // Don't encode path segments 216 | array('http://foo.com/baz/bar/bam?a', 'http://foo.com/baz/bar/bam?a'), 217 | array('http://foo.com/baz+bar', 'http://foo.com/baz+bar'), 218 | array('http://foo.com/baz:bar', 'http://foo.com/baz:bar'), 219 | array('http://foo.com/baz@bar', 'http://foo.com/baz@bar'), 220 | array('http://foo.com/baz(bar);bam/', 'http://foo.com/baz(bar);bam/'), 221 | array('http://foo.com/a-zA-Z0-9.-_~!$&\'()*+,;=:@', 'http://foo.com/a-zA-Z0-9.-_~!$&\'()*+,;=:@'), 222 | ); 223 | } 224 | 225 | /** 226 | * @dataProvider pathTestProvider 227 | */ 228 | public function testUriEncodesPathProperly($input, $output) 229 | { 230 | $uri = new Uri($input); 231 | $this->assertEquals((string) $uri, $output); 232 | } 233 | 234 | public function testDoesNotAddPortWhenNoPort() 235 | { 236 | // Due to 5.4.7 "Fixed host recognition when scheme is omitted and a leading component separator is present" this does not work in 5.3 237 | //$uri = new Uri('//bar'); 238 | //$this->assertEquals('bar', (string) $uri); 239 | //$uri = new Uri('//barx'); 240 | //$this->assertEquals('barx', $uri->getHost()); 241 | } 242 | 243 | public function testAllowsForRelativeUri() 244 | { 245 | $uri = new Uri(); 246 | $uri = $uri->withPath('foo'); 247 | $this->assertEquals('foo', $uri->getPath()); 248 | $this->assertEquals('foo', (string) $uri); 249 | } 250 | 251 | public function testAddsSlashForRelativeUriStringWithHost() 252 | { 253 | $uri = new Uri(); 254 | $uri = $uri->withPath('foo')->withHost('bar.com'); 255 | $this->assertEquals('foo', $uri->getPath()); 256 | $this->assertEquals('bar.com/foo', (string) $uri); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |