Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
/.vscode/
/.vs/
/vendor/
/composer.lock
/composer.lock
/.phpunit.cache/
/.phpunit.result.cache
13 changes: 13 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
"src/Helpers.php"
]
},
"autoload-dev": {
"psr-4": {
"InitPHP\\HTTP\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit"
},
"authors": [
{
"name": "Muhammet ŞAFAK",
Expand All @@ -26,5 +34,10 @@
"psr/http-message": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-client": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"php-http/psr7-integration-tests": "^1.3",
"http-interop/http-factory-tests": "^2.2"
}
}
28 changes: 28 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
failOnRisky="false"
failOnWarning="false"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="PSR-7 Integration">
<directory>tests/Psr7</directory>
</testsuite>
<testsuite name="PSR-17 Integration">
<directory>tests/Psr17</directory>
</testsuite>
<testsuite name="PSR-18 Integration">
<directory>tests/Psr18</directory>
</testsuite>
<testsuite name="Immutability">
<directory>tests/Immutability</directory>
</testsuite>
</testsuites>
<php>
<const name="URI_FACTORY" value="InitPHP\HTTP\Factory\Factory"/>
<const name="STREAM_FACTORY" value="InitPHP\HTTP\Factory\Factory"/>
<const name="UPLOADED_FILE_FACTORY" value="InitPHP\HTTP\Factory\Factory"/>
</php>
</phpunit>
2 changes: 1 addition & 1 deletion src/Client/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ private function prepareCurlOptions(RequestInterface $request, array &$response)
$options[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
}

if ($method === 'HEAD') {
if (\strtoupper($method) === 'HEAD') {
$options[\CURLOPT_NOBODY] = true;
} elseif ($body !== '') {
$options[\CURLOPT_POSTFIELDS] = $body;
Expand Down
18 changes: 18 additions & 0 deletions src/Message/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ class Request implements \InitPHP\HTTP\Message\Interfaces\RequestInterface

private static Request $requestImmutable;

/**
* PSR-7 immutability: clone'da body ve URI'yi de derinleştir; aksi halde
* `$cloned->getBody()->write(...)` veya `$cloned->getUri()->setHost(...)`
* orijinali mutasyona uğratır.
*/
public function __clone()
{
if (isset($this->stream)) {
$this->stream = clone $this->stream;
}
if (isset($this->uri)) {
$this->uri = clone $this->uri;
}
if (isset($this->_objParameters)) {
$this->_objParameters = clone $this->_objParameters;
}
}

public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
{
if(!($uri instanceof UriInterface)) {
Expand Down
11 changes: 11 additions & 0 deletions src/Message/Response.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ class Response implements ResponseInterface
* @param string $version
* @param string|null $reason
*/
/**
* PSR-7 immutability: clone'da body'i de derinleştir ki
* `$cloned->getBody()->write(...)` orijinali bozmasΔ±n.
*/
public function __clone()
{
if (isset($this->stream)) {
$this->stream = clone $this->stream;
}
}

public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null)
{
if(!in_array($version, ['1.0', '1.1', '2.0'], true)){
Expand Down
14 changes: 14 additions & 0 deletions src/Message/ServerRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ class ServerRequest implements ServerRequestInterface
protected array $uploadedFiles = [];


/**
* PSR-7 immutability: clone'da body + URI'yi derinleştir.
* uploadedFiles ve attributes ham PHP array'i; copy-on-write zaten kopyalar.
*/
public function __clone()
{
if (isset($this->stream)) {
$this->stream = clone $this->stream;
}
if (isset($this->uri)) {
$this->uri = clone $this->uri;
}
}

public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
{
$this->serverParams = $serverParams;
Expand Down
63 changes: 59 additions & 4 deletions src/Message/Stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,55 @@ public function __destruct()
$this->close();
}

/**
* PSR-7 immutability: clone yapıldığında resource'u derinleştir; aksi halde
* iki Stream aynı resource handle'ını paylaşır, withBody dışı tüm withX
* çağrıları orijinal mesajın body'sini de değiştirir.
*/
public function __clone()
{
if (!isset($this->stream)) {
return;
}
$this->uri = null;
if (is_string($this->stream)) {
// String backend zaten PHP copy-on-write; ek iş gerekmiyor.
return;
}
if (!is_resource($this->stream)) {
return;
}
$originalPosition = @ftell($this->stream);
if ($this->seekable) {
@rewind($this->stream);
}
$contents = @stream_get_contents($this->stream);
if ($contents === false) {
$contents = '';
}
if ($this->seekable && is_int($originalPosition)) {
@fseek($this->stream, $originalPosition);
}
$copy = @fopen('php://temp', 'w+b');
if ($copy === false) {
return;
}
if ($contents !== '') {
fwrite($copy, $contents);
}
// Orijinal stream'in pozisyonunu koru
if (is_int($originalPosition)) {
fseek($copy, $originalPosition);
} else {
rewind($copy);
}
$this->stream = $copy;
$this->size = strlen($contents);
$this->seekable = true;
$this->readable = true;
$this->writable = true;
}

/**
* @param null|string|resource|StreamInterface $body
* @param string|null $target <p>["php://temp"|"php://memory"|NULL]</p>
Expand All @@ -155,6 +204,15 @@ public function init($body = null, ?string $target = 'php://temp'): StreamInterf
}
$body = $body->getContents();
}
// Resource'lar target'tan bağımsız her zaman kabul edilir
if(is_resource($body)){
$this->stream = $body;
$meta = stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'] && fseek($this->stream, 0, SEEK_CUR) === 0;
$this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
$this->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
return $this;
}
if($this->target === null){
if(!is_scalar($body)){
throw new InvalidArgumentException("The parameter \$body must be a string.");
Expand All @@ -176,10 +234,7 @@ public function init($body = null, ?string $target = 'php://temp'): StreamInterf
fwrite($resource, $body);
fseek($resource, 0);
}
$body = $resource;
}
if(is_resource($body)){
$this->stream = $body;
$this->stream = $resource;
$meta = stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'] && fseek($this->stream, 0, SEEK_CUR) === 0;
$this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
Expand Down
4 changes: 2 additions & 2 deletions src/Message/Traits/MessageTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ public function outHeader($name): self
return $this;
}

$name = $this->headerNames[$lowercase];
unset($this->headerNames[$name], $this->headerNames[$lowercase]);
$original = $this->headerNames[$lowercase];
unset($this->headers[$original], $this->headerNames[$lowercase]);

return $this;
}
Expand Down
17 changes: 9 additions & 8 deletions src/Message/Traits/RequestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use function is_string;
use function preg_match;
use function strtoupper;
use function array_map;

trait RequestTrait
{
Expand All @@ -37,55 +38,55 @@ trait RequestTrait
*/
public function isGet(): bool
{
return $this->getMethod() === 'GET';
return strtoupper($this->getMethod()) === 'GET';
}

/**
* @inheritDoc
*/
public function isPost(): bool
{
return $this->getMethod() === 'POST';
return strtoupper($this->getMethod()) === 'POST';
}

/**
* @inheritDoc
*/
public function isPut(): bool
{
return $this->getMethod() === 'PUT';
return strtoupper($this->getMethod()) === 'PUT';
}

/**
* @inheritDoc
*/
public function isDelete(): bool
{
return $this->getMethod() === 'DELETE';
return strtoupper($this->getMethod()) === 'DELETE';
}

/**
* @inheritDoc
*/
public function isHead(): bool
{
return $this->getMethod() === 'HEAD';
return strtoupper($this->getMethod()) === 'HEAD';
}

/**
* @inheritDoc
*/
public function isPatch(): bool
{
return $this->getMethod() === 'PATCH';
return strtoupper($this->getMethod()) === 'PATCH';
}

/**
* @inheritDoc
*/
public function isMethod(string ...$method): bool
{
return in_array($this->getMethod(), $method);
return in_array(strtoupper($this->getMethod()), array_map('strtoupper', $method), true);
}

/**
Expand Down Expand Up @@ -143,7 +144,7 @@ public function setMethod($method): self
if(!is_string($method)){
throw new \InvalidArgumentException('Method must be a string.');
}
$this->method = strtoupper($method);
$this->method = $method;
return $this;
}

Expand Down
22 changes: 17 additions & 5 deletions src/Message/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
use function rawurlencode;
use function ltrim;
use function sprintf;
use function strpos;
use function preg_replace;

class Uri implements UriInterface
{
Expand Down Expand Up @@ -56,14 +58,16 @@ public function __construct(string $uri = '')
throw new InvalidArgumentException(sprintf('Unable to parse URI: "%s"', $uri));
}
$this->scheme = isset($parts['scheme']) ? strtolower($parts['scheme']) : '';
$this->userInfo = $parts['user'] ?? '';
$this->host = isset($parts['host']) ? strtolower($parts['host']) : '';
$this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
$this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
$this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
$this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $parts['pass'];
if (isset($parts['user'])) {
$this->userInfo = $this->filterUserInfoComponent($parts['user']);
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $this->filterUserInfoComponent($parts['pass']);
}
}
}
}
Expand Down Expand Up @@ -131,6 +135,9 @@ public function getPort(): ?int
*/
public function getPath(): string
{
if(isset($this->path[0], $this->path[1]) && $this->path[0] === '/' && $this->path[1] === '/'){
return '/' . ltrim($this->path, '/');
}
return $this->path;
}

Expand Down Expand Up @@ -180,9 +187,9 @@ public function withScheme($scheme): self
*/
public function setUserInfo(string $user, ?string $password = null): self
{
$info = $user;
$info = $this->filterUserInfoComponent($user);
if ($password !== null && $password !== '') {
$info .= ':' . $password;
$info .= ':' . $this->filterUserInfoComponent($password);
}
if ($this->userInfo === $info) {
return $this;
Expand Down Expand Up @@ -375,6 +382,11 @@ protected function filterQueryAndFragment($str): string
return preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
}

protected function filterUserInfoComponent(string $component): string
{
return preg_replace_callback('/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $component);
}

protected static function rawurlencodeMatchZero(array $match): string
{
return rawurlencode($match[0]);
Expand Down
Loading