diff --git a/.appveyor.yml b/.appveyor.yml
index 52639fc..9ac6bd8 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -1,43 +1,147 @@
-version: '{branch}.{build}'
+# See https://github.com/sergeyklay/php-appveyor
 
 branches:
   only:
-    - master
-
-image:
-  - Visual Studio 2015
+  - master
+  - appveyor
+  - w32
+  - /^v\d+\.\d+\.\d+$/
 
 environment:
-  PHP_SDK_BINARY_TOOLS_URL: https://windows.php.net/downloads/php-sdk
-  PHP_SDK_BINARY_TOOLS_PACKAGE: php-sdk-binary-tools-20110915.zip
-  PHP_DEPS_URL: https://windows.php.net/downloads/php-sdk
+  EXTNAME: http_message
 
   matrix:
-    - PHP_REL: 7.0
-      ARCHITECTURE: x64
-      ARCH: amd64
-      ZTS_STATE: disable
+    - PHP_VERSION: 7.2
+      BUILD_TYPE: Win32
+      VC_VERSION: vc15
+      APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+
+    - PHP_VERSION: 7.2
+      BUILD_TYPE: nts-Win32
+      VC_VERSION: vc15
+      APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+
+    - PHP_VERSION: 7.3
+      BUILD_TYPE: Win32
+      VC_VERSION: vc15
+      APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+
+    - PHP_VERSION: 7.3
+      BUILD_TYPE: nts-Win32
+      VC_VERSION: vc15
+      APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+
+    - PHP_VERSION: 7.4
+      BUILD_TYPE: Win32
+      VC_VERSION: vc15
       APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
-      PHP_BUILD_CRT: vc14
-      PHP_DEPS_PACKAGE: deps-7.0-vc14-x64.7z
 
-# Uncomment the following two lines to use RDP to debug "stuck" builds
-#init:
-#  - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
-    
+    - PHP_VERSION: 7.4
+      BUILD_TYPE: nts-Win32
+      VC_VERSION: vc15
+      APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
+
+  PHP_SDK_VERSION: 2.2.0
+  PHP_AVM: https://raw.githubusercontent.com/sergeyklay/php-appveyor/master/php-appveyor.psm1
+
+  TEST_PHP_EXECUTABLE: C:\php\php.exe
+  NO_INTERACTION: 1
+  REPORT_EXIT_STATUS: 1
+
+matrix:
+  fast_finish: true
+
+cache:
+  - 'C:\Downloads -> .appveyor.yml'
+
+platform:
+  - x86
+  - x64
+
+init:
+  - ps: $DebugPreference = 'SilentlyContinue' # Continue
+  - ps: >-
+      if ($env:APPVEYOR_REPO_TAG -eq "true") {
+        Update-AppveyorBuild -Version "$($Env:APPVEYOR_REPO_TAG_NAME.TrimStart("v"))"
+      } else {
+        Update-AppveyorBuild -Version "${Env:APPVEYOR_REPO_BRANCH}-$($Env:APPVEYOR_REPO_COMMIT.Substring(0, 7))"
+      }
+
 install:
-  - cmd: cinst wget
-  - cmd: >-
-         "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %ARCH%
+  - ps: Import-Module .\.ci\php-appveyor.psm1
+
+  - ps: InstallPhpSdk     $Env:PHP_SDK_VERSION $Env:VC_VERSION $Env:PLATFORM
+  - ps: InstallPhp        $Env:PHP_VERSION $Env:BUILD_TYPE $Env:VC_VERSION $Env:PLATFORM
+  - ps: InstallPhpDevPack $Env:PHP_VERSION $Env:BUILD_TYPE $Env:VC_VERSION $Env:PLATFORM
+
+  - ps: >-
+      InstallPeclExtension `
+        -Name       psr `
+        -Version    1.0.1 `
+        -PhpVersion $Env:PHP_VERSION `
+        -BuildType  $Env:BUILD_TYPE `
+        -VC         $Env:VC_VERSION `
+        -Platform   $Env:PLATFORM `
+        -Headers    $true `
+        -Enable     $true
 
 build_script:
-  - .appveyor\build.cmd
+  - ps: Import-Module .\.ci\appveyor.psm1
+  - ps: InitializeBuildVars
+  - cmd: '"%VSDEVCMD%" -arch=%PLATFORM%'
+  - cmd: '"%VCVARSALL%" %ARCH%'
+  - cmd: C:\php-sdk\bin\phpsdk_setvars
+  - cmd: C:\php-devpack\phpize
+  - cmd: configure.bat --with-prefix=C:\php --with-php-build=C:\php-devpack --disable-all --enable-http-message
+  - cmd: nmake 2> compile-errors.log 1> compile.log
+  - ps: InitializeReleaseVars
 
 test_script:
-  - .appveyor\test.cmd
+  - cmd: nmake test
+
+after_build:
+  - ps: Set-Location "${Env:APPVEYOR_BUILD_FOLDER}"
+  - ps: >-
+      PrepareReleasePackage `
+        -PhpVersion     $Env:PHP_VERSION `
+        -BuildType      $Env:BUILD_TYPE `
+        -Platform       $Env:PLATFORM `
+        -ConverMdToHtml $true `
+        -ReleaseFiles   "${Env:RELEASE_FOLDER}\php_${Env:EXTNAME}.dll",`
+                        "${Env:APPVEYOR_BUILD_FOLDER}\CREDITS",`
+                        "${Env:APPVEYOR_BUILD_FOLDER}\LICENSE"
+
+on_failure :
+  - ps: >-
+      If (Test-Path -Path "${Env:APPVEYOR_BUILD_FOLDER}\compile-errors.log") {
+        Get-Content -Path "${Env:APPVEYOR_BUILD_FOLDER}\compile-errors.log"
+      }
+
+      If (Test-Path -Path "${Env:APPVEYOR_BUILD_FOLDER}\compile.log") {
+        Get-Content -Path "${Env:APPVEYOR_BUILD_FOLDER}\compile.log"
+      }
+
+      Get-ChildItem "${Env:APPVEYOR_BUILD_FOLDER}\tests" -Recurse -Filter *.diff | Foreach-Object {
+        [Environment]::NewLine
+        Write-Output $_.FullName
+        Get-Content -Path $_.FullName
+      }
 
-# Uncomment the following two lines to use RDP on finish, blocking
-# until the "lock" file on the VM desktop is deleted.
-#on_finish:
-#  - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
+artifacts:
+  - path: '.\$(RELEASE_ZIPBALL).zip'
+    name: '$(EXTNAME)'
+    type: zip
 
+deploy:
+  release: v$(appveyor_build_version)
+  description: 'v$(appveyor_build_version)'
+  provider: GitHub
+  auth_token:
+    secure: 9kSFTreufYy0WOpd+eYAFa+2IjSxe1idbVAzehcTS/womadyGxVG6qTKpJnXDmTh
+  artifact: '$(RELEASE_ZIPBALL).zip'
+  draft: false
+  prerelease: false
+  force_update: true
+  on:
+    branch: master
+    APPVEYOR_REPO_TAG: true
diff --git a/.appveyor/build.cmd b/.appveyor/build.cmd
deleted file mode 100644
index 5292e5d..0000000
--- a/.appveyor/build.cmd
+++ /dev/null
@@ -1,51 +0,0 @@
-@echo off
-
-setlocal enableextensions enabledelayedexpansion
-
-	REM TODO: build external libraries
-
-	REM set up PHP
-	mkdir C:\projects\php-sdk >NUL 2>NUL
-	cd C:\projects\php-sdk
-    wget %PHP_SDK_BINARY_TOOLS_URL%/%PHP_SDK_BINARY_TOOLS_PACKAGE% --no-check-certificate -q -O %PHP_SDK_BINARY_TOOLS_PACKAGE%
-    7z x -y %PHP_SDK_BINARY_TOOLS_PACKAGE%
-	cmd /c bin\phpsdk_buildtree.bat phpdev
-	pushd phpdev
-	ren vc9 vc14
-	pushd vc14\x64
-    git clone https://git.php.net/repository/php-src.git
-	cd php-src
-	git checkout PHP-%PHP_REL%
-	cd ..
-	wget %PHP_DEPS_URL%/%PHP_DEPS_PACKAGE% --no-check-certificate -q -O %PHP_DEPS_PACKAGE%
-	7z x -y %PHP_DEPS_PACKAGE%
-	popd
-	popd
-
-	REM copy the extension into the PHP tree
-	mkdir c:\projects\php-sdk\phpdev\vc14\x64\php-src\ext\skeleton
-	xcopy c:\projects\skeleton-php-ext\*.* c:\projects\php-sdk\phpdev\vc14\x64\php-src\ext\skeleton /s/e/v
-	pushd c:\projects\php-sdk\phpdev\vc14\x64\php-src\ext\skeleton
-	del /q CREDITS
-	popd
-	
-	REM The bison utility is needed for the PHP build, so add MSYS to the path.
-	REM Note: Add to the end to ensure MSVC tools are found firts.
-	set PATH=%PATH%;c:\MinGW\msys\1.0\bin
-
-	REM perform the build
-	cmd /c bin\phpsdk_setvars.bat
-	pushd phpdev\vc14\x64\php-src
-	cmd /c buildconf --force
-	cmd /c configure --disable-all --enable-cli --with-skeleton=shared
-	nmake
-	popd
-
-	REM TODO: debugging
-
-	dir php_skeleton.dll /s
-	dir php.exe /s
-	dir php*.dll /s
-
-endlocal
-
diff --git a/.appveyor/test.cmd b/.appveyor/test.cmd
deleted file mode 100644
index 255d475..0000000
--- a/.appveyor/test.cmd
+++ /dev/null
@@ -1,16 +0,0 @@
-@echo off
-
-setlocal enableextensions enabledelayedexpansion
-
-    cd C:\projects\php-sdk\phpdev\vc14\x64\php-src
-	
-	pushd ext\skeleton
-    echo [PHP] > php.ini
-    echo extension_dir = "ext" >> php.ini
-    echo extension=php_skeleton.dll >> php.ini
-	popd
-
-	set TEST_PHP_EXECUTABLE=C:\projects\php-sdk\phpdev\vc14\x64\php-src\x64\Release_TS\php.exe
-	%TEST_PHP_EXECUTABLE% run-tests.php ext\skeleton -q --show-diff
-
-endlocal
diff --git a/.ci/appveyor.psm1 b/.ci/appveyor.psm1
new file mode 100644
index 0000000..e22786b
--- /dev/null
+++ b/.ci/appveyor.psm1
@@ -0,0 +1,57 @@
+Function InitializeBuildVars {
+	switch ($Env:VC_VERSION) {
+		'vc14' {
+			If (-not (Test-Path $Env:VS120COMNTOOLS)) {
+				Throw'The VS120COMNTOOLS environment variable is not set. Check your VS installation'
+			}
+
+			$Env:VSDEVCMD = ($Env:VS120COMNTOOLS -replace '\\$', '') + '\VsDevCmd.bat'
+			Break
+		}
+		'vc15' {
+			If (-not (Test-Path $Env:VS140COMNTOOLS)) {
+				Throw'The VS140COMNTOOLS environment variable is not set. Check your VS installation'
+			}
+
+			$Env:VSDEVCMD = ($Env:VS140COMNTOOLS -replace '\\$', '') + '\VsDevCmd.bat'
+			Break
+		}
+		default {
+			$Env:VSDEVCMD = Get-ChildItem -Path "${Env:ProgramFiles(x86)}" -Filter "VsDevCmd.bat" -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
+
+			If ("$Env:VSDEVCMD" -eq "") {
+				Throw 'Unable to find VsDevCmd. Check your VS installation'
+			}
+		}
+	}
+
+	If ($Env:PLATFORM -eq 'x64') {
+		$Env:ARCH = 'x86_amd64'
+	} Else {
+		$Env:ARCH = 'x86'
+	}
+
+	$Env:ENABLE_EXT = "--enable-{0}" -f ("${Env:EXTNAME}" -replace "_","-")
+
+	$SearchInFolder = (Get-Item $Env:VSDEVCMD).Directory.Parent.Parent.FullName
+	$Env:VCVARSALL = Get-ChildItem -Path "$SearchInFolder" -Filter "vcvarsall.bat" -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName }
+}
+
+Function InitializeReleaseVars {
+	If ($Env:PLATFORM -eq 'x86') {
+		If ($Env:BUILD_TYPE -Match "nts-Win32") {
+			$Env:RELEASE_SUBFOLDER = "Release"
+		} Else {
+			$Env:RELEASE_SUBFOLDER = "Release_TS"
+		}
+	} Else {
+		If ($Env:BUILD_TYPE -Match "nts-Win32") {
+			$Env:RELEASE_SUBFOLDER = "${Env:PLATFORM}\Release"
+		} Else {
+			$Env:RELEASE_SUBFOLDER = "${Env:PLATFORM}\Release_TS"
+		}
+	}
+
+	$Env:RELEASE_FOLDER = "${Env:APPVEYOR_BUILD_FOLDER}\${Env:RELEASE_SUBFOLDER}"
+	$Env:RELEASE_ZIPBALL = "${Env:EXTNAME}_${Env:PLATFORM}_${Env:VC_VERSION}_php${Env:PHP_VERSION}_${Env:APPVEYOR_BUILD_VERSION}"
+}
diff --git a/.ci/php-appveyor.psm1 b/.ci/php-appveyor.psm1
new file mode 100644
index 0000000..de0c921
--- /dev/null
+++ b/.ci/php-appveyor.psm1
@@ -0,0 +1,567 @@
+# This file is part of the php-appveyor.psm1 project.
+#
+# (c) Serghei Iakovlev <sadhooklay@gmail.com>
+#
+# For the full copyright and license information, please view
+# the LICENSE file that was distributed with this source code.
+
+# $ErrorActionPreference = "Stop"
+
+function InstallPhpSdk {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $Version,
+		[Parameter(Mandatory=$true)]  [System.String] $VC,
+		[Parameter(Mandatory=$true)]  [System.String] $Platform,
+		[Parameter(Mandatory=$false)] [System.String] $InstallPath = "C:\php-sdk"
+	)
+
+	Write-Debug "Install PHP SDK binary tools: ${Version}"
+	SetupPrerequisites
+
+	$FileName  = "php-sdk-${Version}"
+	$RemoteUrl = "https://github.com/Microsoft/php-sdk-binary-tools/archive/${FileName}.zip"
+	$Archive   = "C:\Downloads\${FileName}.zip"
+
+	if (-not (Test-Path "${InstallPath}\bin\php\php.exe")) {
+		if (-not (Test-Path $Archive)) {
+			DownloadFile -RemoteUrl $RemoteUrl -Destination $Archive
+		}
+
+		$UnzipPath = "${Env:Temp}\php-sdk-binary-tools-${FileName}"
+		if (-not (Test-Path "${UnzipPath}")) {
+			Expand-Item7zip -Archive $Archive -Destination $Env:Temp
+		}
+
+		Move-Item -Path $UnzipPath -Destination $InstallPath
+	}
+
+	EnsureRequiredDirectoriesPresent `
+		-Directories bin,lib,include `
+		-Prefix "${InstallPath}\phpdev\${VC}\${Platform}"
+}
+
+function InstallPhp {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $Version,
+		[Parameter(Mandatory=$true)]  [System.String] $BuildType,
+		[Parameter(Mandatory=$true)]  [System.String] $VC,
+		[Parameter(Mandatory=$true)]  [System.String] $Platform,
+		[Parameter(Mandatory=$false)] [System.String] $InstallPath = "C:\php"
+	)
+
+	SetupPrerequisites
+	$FullVersion = SetupPhpVersionString -Pattern $Version
+
+	Write-Debug "Install PHP v${FullVersion}"
+
+	$ReleasesPart = "releases"
+	if ([System.Convert]::ToDecimal($Version) -lt 7.1) {
+		$ReleasesPart = "releases/archives"
+	}
+
+	$RemoteUrl = "http://windows.php.net/downloads/{0}/php-{1}-{2}-{3}-{4}.zip" -f
+	$ReleasesPart, $FullVersion, $BuildType, $VC, $Platform
+
+	$Archive   = "C:\Downloads\php-${FullVersion}-${BuildType}-${VC}-${Platform}.zip"
+
+	if (-not (Test-Path "${InstallPath}\php.exe")) {
+		if (-not (Test-Path $Archive)) {
+			DownloadFile $RemoteUrl $Archive
+		}
+
+		Expand-Item7zip $Archive $InstallPath
+	}
+
+	if (-not (Test-Path "${InstallPath}\php.ini")) {
+		Copy-Item "${InstallPath}\php.ini-development" "${InstallPath}\php.ini"
+	}
+}
+
+function InstallPhpDevPack {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $PhpVersion,
+		[Parameter(Mandatory=$true)]  [System.String] $BuildType,
+		[Parameter(Mandatory=$true)]  [System.String] $VC,
+		[Parameter(Mandatory=$true)]  [System.String] $Platform,
+		[Parameter(Mandatory=$false)] [System.String] $InstallPath = "C:\php-devpack"
+	)
+
+	SetupPrerequisites
+	$Version = SetupPhpVersionString -Pattern $PhpVersion
+
+	Write-Debug "Install PHP Dev for PHP v${Version}"
+
+	$ReleasesPart = "releases"
+	if ([System.Convert]::ToDecimal($PhpVersion) -lt 7.1) {
+		$ReleasesPart = "releases/archives"
+	}
+
+	$RemoteUrl = "http://windows.php.net/downloads/{0}/php-devel-pack-{1}-{2}-{3}-{4}.zip" -f
+	$ReleasesPart, $Version, $BuildType, $VC, $Platform
+
+	$Archive   = "C:\Downloads\php-devel-pack-${Version}-${BuildType}-${VC}-${Platform}.zip"
+
+	if (-not (Test-Path "${InstallPath}\phpize.bat")) {
+		if (-not (Test-Path $Archive)) {
+			DownloadFile $RemoteUrl $Archive
+		}
+
+		$UnzipPath = "${Env:Temp}\php-${Version}-devel-${VC}-${Platform}"
+		if (-not (Test-Path "${UnzipPath}\phpize.bat")) {
+			Expand-Item7zip $Archive $Env:Temp
+		}
+
+		if (Test-Path "${InstallPath}") {
+			Move-Item -Path "${UnzipPath}\*" -Destination $InstallPath
+		} else {
+			Move-Item -Path $UnzipPath -Destination $InstallPath
+		}
+	}
+}
+
+function InstallPeclExtension {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $Name,
+		[Parameter(Mandatory=$true)]  [System.String] $Version,
+		[Parameter(Mandatory=$true)]  [System.String] $PhpVersion,
+		[Parameter(Mandatory=$true)]  [System.String] $BuildType,
+		[Parameter(Mandatory=$true)]  [System.String] $VC,
+		[Parameter(Mandatory=$true)]  [System.String] $Platform,
+		[Parameter(Mandatory=$false)] [System.String] $InstallPath = "C:\php\ext",
+		[Parameter(Mandatory=$false)] [System.Boolean] $Headers = $false,
+		[Parameter(Mandatory=$false)] [System.Boolean] $Enable = $false
+	)
+
+	SetupPrerequisites
+
+	$BaseUri = "https://windows.php.net/downloads/pecl/releases/${Name}/${Version}"
+	$LocalPart = "php_${Name}-${Version}-${PhpVersion}"
+
+	if ($BuildType -Match "nts-Win32") {
+		$TS = "nts"
+	} else {
+		$TS = "ts"
+	}
+
+	$RemoteUrl = "${BaseUri}/${LocalPart}-${TS}-${VC}-${Platform}.zip"
+	$DestinationPath = "C:\Downloads\${LocalPart}-${TS}-${VC}-${Platform}.zip"
+
+	if (-not (Test-Path "${InstallPath}\php_${Name}.dll")) {
+		if (-not (Test-Path $DestinationPath)) {
+			DownloadFile $RemoteUrl $DestinationPath
+		}
+
+		Expand-Item7zip $DestinationPath $InstallPath
+	}
+
+	if ($Headers) {
+		InstallPeclHeaders -Name $Name -Version $Version
+	}
+
+	if ($Enable) {
+		EnablePhpExtension -Name $Name
+	}
+}
+
+Function InstallPeclHeaders {
+	Param(
+		[Parameter(Mandatory=$true)]  [System.String] $Name,
+		[Parameter(Mandatory=$true)]  [System.String] $Version,
+		[Parameter(Mandatory=$false)] [System.String] $InstallPath = 'C:\php-devpack\include\ext\'
+	)
+
+	$RemoteUrl = "https://pecl.php.net/get/${Name}-${Version}.tgz"
+	$DownloadFile = "C:\Downloads\${Name}-${Version}.tgz"
+
+	If (-not [System.IO.File]::Exists($DownloadFile)) {
+		DownloadFile $RemoteUrl $DownloadFile
+	}
+
+	Ensure7ZipIsInstalled
+
+	Expand-Item7zip $DownloadFile "${Env:Temp}"
+
+	Write-Debug "Copy header files to ${InstallPath}\${Name}"
+
+	New-Item -Path "${InstallPath}" -Name "${Name}" -ItemType "directory" | Out-Null
+	Copy-Item "${Env:Temp}\${Name}-${Version}\*.h" -Destination "${InstallPath}\${Name}" -Recurse
+}
+
+function InstallComposer {
+	param (
+		[Parameter(Mandatory=$false)] [System.String] $PhpInstallPath = 'C:\php',
+		[Parameter(Mandatory=$false)] [System.String] $InstallPath = '.'
+	)
+
+	$InstallPath = Resolve-Path $InstallPath
+
+	$ComposerBatch = "${InstallPath}\composer.bat"
+	$ComposerPhar  = "${InstallPath}\composer.phar"
+
+	if (-not (Test-Path -Path $ComposerPhar)) {
+		DownloadFile "https://getcomposer.org/composer.phar" "${ComposerPhar}"
+
+		Write-Output '@echo off' | Out-File -Encoding "ASCII" $ComposerBatch
+		Write-Output "${PhpInstallPath}\php.exe `"${ComposerPhar}`" %*" | Out-File -Encoding "ASCII" -Append $ComposerBatch
+	}
+}
+
+function EnablePhpExtension {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $Name,
+		[Parameter(Mandatory=$false)] [System.String] $PhpInstallPath = 'C:\php',
+		[Parameter(Mandatory=$false)] [System.String] $ExtPath = 'C:\php\ext',
+		[Parameter(Mandatory=$false)] [System.String] $PrintableName = ''
+	)
+
+	$FullyQualifiedExtensionPath = "${ExtPath}\php_${Name}.dll"
+
+	$IniFile = "${PhpInstallPath}\php.ini"
+	$PhpExe  = "${PhpInstallPath}\php.exe"
+
+	if (-not (Test-Path $IniFile)) {
+		throw "Unable to locate ${IniFile}"
+	}
+
+	if (-not (Test-Path "${ExtPath}")) {
+		throw "Unable to locate ${ExtPath} direcory"
+	}
+
+	Write-Debug "Add `"extension = ${FullyQualifiedExtensionPath}`" to the ${IniFile}"
+	Write-Output "extension = ${FullyQualifiedExtensionPath}"  | Out-File -Encoding "ASCII" -Append $IniFile
+
+	if (Test-Path -Path "${PhpExe}") {
+		if ($PrintableName) {
+			Write-Debug "Minimal load test using command: ${PhpExe} --ri `"${PrintableName}`""
+			$Result = (& "${PhpExe}" --ri "${PrintableName}")
+		} else {
+			Write-Debug "Minimal load test using command: ${PhpExe} --ri ${Name}"
+			$Result = (& "${PhpExe}" --ri $Name)
+		}
+
+		$ExitCode = $LASTEXITCODE
+		if ($ExitCode -ne 0) {
+			throw "An error occurred while enabling ${Name} at ${IniFile}. ${Result}"
+		}
+	}
+}
+
+function TuneUpPhp {
+	param (
+		[Parameter(Mandatory=$false)] [System.String]   $MemoryLimit = '256M',
+		[Parameter(Mandatory=$false)] [System.String[]] $DefaultExtensions = @(),
+		[Parameter(Mandatory=$false)] [System.String]   $IniFile = 'C:\php\php.ini',
+		[Parameter(Mandatory=$false)] [System.String]   $ExtPath = 'C:\php\ext'
+	)
+
+	Write-Debug "Tune up PHP using file `"${IniFile}`""
+
+	if (-not (Test-Path $IniFile)) {
+		throw "Unable to locate ${IniFile} file"
+	}
+
+	if (-not (Test-Path $ExtPath)) {
+		throw "Unable to locate ${ExtPath} direcory"
+	}
+
+	Write-Output "" | Out-File -Encoding "ASCII" -Append $IniFile
+
+	Write-Output "extension_dir = ${ExtPath}"    | Out-File -Encoding "ASCII" -Append $IniFile
+	Write-Output "memory_limit = ${MemoryLimit}" | Out-File -Encoding "ASCII" -Append $IniFile
+
+	if ($DefaultExtensions.count -gt 0) {
+		Write-Output "" | Out-File -Encoding "ASCII" -Append $IniFile
+
+		foreach ($Ext in $DefaultExtensions) {
+			Write-Output "extension = php_${Ext}.dll" | Out-File -Encoding "ASCII" -Append $IniFile
+		}
+	}
+}
+
+function PrepareReleaseNote {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $PhpVersion,
+		[Parameter(Mandatory=$true)]  [System.String] $BuildType,
+		[Parameter(Mandatory=$true)]  [System.String] $Platform,
+		[Parameter(Mandatory=$false)] [System.String] $ReleaseFile,
+		[Parameter(Mandatory=$false)] [System.String] $ReleaseDirectory,
+		[Parameter(Mandatory=$false)] [System.String] $BasePath
+	)
+
+	$Destination = "${BasePath}\${ReleaseDirectory}"
+
+	if (-not (Test-Path $Destination)) {
+		New-Item -ItemType Directory -Force -Path "${Destination}" | Out-Null
+	}
+
+	$ReleaseFile = "${Destination}\${ReleaseFile}"
+	$ReleaseDate = Get-Date -Format o
+
+	$Image = $Env:APPVEYOR_BUILD_WORKER_IMAGE
+	$Version = $Env:APPVEYOR_BUILD_VERSION
+	$Commit = $Env:APPVEYOR_REPO_COMMIT
+	$CommitDate = $Env:APPVEYOR_REPO_COMMIT_TIMESTAMP
+
+	Write-Output "Release date: ${ReleaseDate}"      | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+	Write-Output "Release version: ${Version}"       | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+	Write-Output "Git commit: ${Commit}"             | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+	Write-Output "Commit date: ${CommitDate}"        | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+	Write-Output "Build type: ${BuildType}"          | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+	Write-Output "Platform: ${Platform}"             | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+	Write-Output "Target PHP version: ${PhpVersion}" | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+	Write-Output "Build worker image: ${Image}"      | Out-File -Encoding "ASCII" -Append "${ReleaseFile}"
+}
+
+function PrepareReleasePackage {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $PhpVersion,
+		[Parameter(Mandatory=$true)]  [System.String] $BuildType,
+		[Parameter(Mandatory=$true)]  [System.String] $Platform,
+		[Parameter(Mandatory=$false)] [System.String] $ZipballName = '',
+		[Parameter(Mandatory=$false)] [System.String[]] $ReleaseFiles = @(),
+		[Parameter(Mandatory=$false)] [System.String] $ReleaseFile = 'RELEASE.txt',
+		[Parameter(Mandatory=$false)] [System.Boolean] $ConverMdToHtml = $false,
+		[Parameter(Mandatory=$false)] [System.String] $BasePath = '.'
+	)
+
+	$BasePath = Resolve-Path $BasePath
+	$ReleaseDirectory = "${Env:APPVEYOR_PROJECT_NAME}-${Env:APPVEYOR_BUILD_ID}-${Env:APPVEYOR_JOB_ID}-${Env:APPVEYOR_JOB_NUMBER}"
+
+	PrepareReleaseNote `
+		-PhpVersion       $PhpVersion `
+		-BuildType        $BuildType `
+		-Platform         $Platform `
+		-ReleaseFile      $ReleaseFile `
+		-ReleaseDirectory $ReleaseDirectory `
+		-BasePath         $BasePath
+
+	$ReleaseDestination = "${BasePath}\${ReleaseDirectory}"
+
+	$CurrentPath = Resolve-Path '.'
+
+	if ($ConverMdToHtml) {
+		InstallReleaseDependencies
+		FormatReleaseFiles -ReleaseDirectory $ReleaseDirectory
+	}
+
+	if ($ReleaseFiles.count -gt 0) {
+		foreach ($File in $ReleaseFiles) {
+			Copy-Item "${File}" "${ReleaseDestination}"
+			Write-Debug "Copy ${File} to ${ReleaseDestination}"
+		}
+	}
+
+	if (!$ZipballName) {
+		if (!$Env:RELEASE_ZIPBALL) {
+			throw "Required parameter `"ZipballName`" is missing"
+		} else {
+			$ZipballName = $Env:RELEASE_ZIPBALL;
+		}
+	}
+
+	Ensure7ZipIsInstalled
+
+	Set-Location "${ReleaseDestination}"
+	$Output = (& 7z a "${ZipballName}.zip" *)
+	$ExitCode = $LASTEXITCODE
+
+	$DirectoryContents = Get-ChildItem -Path "${ReleaseDestination}"
+	Write-Debug ($DirectoryContents | Out-String)
+
+	if ($ExitCode -ne 0) {
+		Set-Location "${CurrentPath}"
+		throw "An error occurred while creating release zippbal: `"${ZipballName}`". ${Output}"
+	}
+
+	Move-Item "${ZipballName}.zip" -Destination "${BasePath}"
+	Set-Location "${CurrentPath}"
+}
+
+function FormatReleaseFiles {
+	param (
+		[Parameter(Mandatory=$true)]  [System.String] $ReleaseDirectory,
+		[Parameter(Mandatory=$false)] [System.String] $BasePath = '.'
+	)
+
+	EnsurePandocIsInstalled
+
+	$CurrentPath = (Get-Item -Path ".\" -Verbose).FullName
+
+	$BasePath = Resolve-Path $BasePath
+	Set-Location "${BasePath}"
+
+	Get-ChildItem (Get-Item -Path ".\" -Verbose).FullName *.md |
+	ForEach-Object{
+		$BaseName = $_.BaseName
+		pandoc -f markdown -t html5 "${BaseName}.md" > "${BasePath}\${ReleaseDirectory}\${BaseName}.html"
+	}
+
+	Set-Location "${CurrentPath}"
+}
+
+function SetupPhpVersionString {
+	param (
+		[Parameter(Mandatory=$true)] [String] $Pattern
+	)
+
+	$RemoteUrl   = 'http://windows.php.net/downloads/releases/sha256sum.txt'
+	$Destination = "${Env:Temp}\php-sha256sum.txt"
+
+	if (-not (Test-Path $Destination)) {
+		DownloadFile $RemoteUrl $Destination
+	}
+
+	$VersionString = Get-Content $Destination | Where-Object {
+		$_ -match "php-($Pattern\.\d+)-src"
+	} | ForEach-Object { $matches[1] }
+
+	if ($VersionString -NotMatch '\d+\.\d+\.\d+' -or $null -eq $VersionString) {
+		throw "Unable to obtain PHP version string using pattern 'php-($Pattern\.\d+)-src'"
+	}
+
+	Write-Output $VersionString.Split(' ')[-1]
+}
+
+function SetupPrerequisites {
+	Ensure7ZipIsInstalled
+	EnsureRequiredDirectoriesPresent -Directories C:\Downloads
+}
+
+function Ensure7ZipIsInstalled  {
+	if (-not (Get-Command "7z" -ErrorAction SilentlyContinue)) {
+		$7zipInstallationDirectory = "${Env:ProgramFiles}\7-Zip"
+
+		if (-not (Test-Path "${7zipInstallationDirectory}")) {
+			throw "The 7-zip file archiver is needed to use this module"
+		}
+
+		$Env:Path += ";$7zipInstallationDirectory"
+	}
+}
+
+function InstallReleaseDependencies {
+	EnsureChocolateyIsInstalled
+	$Output = (choco install -y --no-progress pandoc)
+	$ExitCode = $LASTEXITCODE
+
+	if ($ExitCode -ne 0) {
+		throw "An error occurred while installing pandoc. ${Output}"
+	}
+}
+
+function EnsureChocolateyIsInstalled {
+	if (-not (Get-Command "choco" -ErrorAction SilentlyContinue)) {
+		$ChocolateyInstallationDirectory = "${Env:ChocolateyInstall}\bin"
+
+		if (-not (Test-Path "$ChocolateyInstallationDirectory")) {
+			throw "The choco is needed to use this module"
+		}
+
+		$Env:Path += ";$ChocolateyInstallationDirectory"
+	}
+}
+
+function EnsurePandocIsInstalled {
+	if (-not (Get-Command "pandoc" -ErrorAction SilentlyContinue)) {
+		$PandocInstallationDirectory = "${Env:ChocolateyInstall}\bin"
+
+		if (-not (Test-Path "$PandocInstallationDirectory")) {
+			throw "The pandoc is needed to use this module"
+		}
+
+		$Env:Path += ";$PandocInstallationDirectory"
+	}
+
+	$Output = (& "pandoc" -v)
+	$ExitCode = $LASTEXITCODE
+
+	if ($ExitCode -ne 0) {
+		throw "An error occurred while self testing pandoc. ${Output}"
+	}
+}
+
+function EnsureRequiredDirectoriesPresent {
+	param (
+		[Parameter(Mandatory=$true)] [String[]] $Directories,
+		[Parameter(Mandatory=$false)] [String] $Prefix = ""
+	)
+
+	foreach ($Dir in $Directories) {
+		if (-not (Test-Path $Dir)) {
+			if ($Prefix) {
+				New-Item -ItemType Directory -Force -Path "${Prefix}\${Dir}" | Out-Null
+			} else {
+				New-Item -ItemType Directory -Force -Path "${Dir}" | Out-Null
+			}
+
+		}
+	}
+}
+
+function DownloadFile {
+	param (
+		[Parameter(Mandatory=$true)] [System.String] $RemoteUrl,
+		[Parameter(Mandatory=$true)] [System.String] $Destination
+	)
+
+	$RetryMax   = 5
+	$RetryCount = 0
+	$Completed  = $false
+
+	$WebClient = New-Object System.Net.WebClient
+	$WebClient.Headers.Add('User-Agent', 'AppVeyor PowerShell Script')
+
+	Write-Debug "Downloading: '${RemoteUrl}' => '${Destination}' ..."
+
+	while (-not $Completed) {
+		try {
+			$WebClient.DownloadFile($RemoteUrl, $Destination)
+			$Completed = $true
+		} catch  {
+			if ($RetryCount -ge $RetryMax) {
+				$ErrorMessage = $_.Exception.Message
+				Write-Error -Message "${ErrorMessage}"
+				$Completed = $true
+			} else {
+				$RetryCount++
+			}
+		}
+	}
+}
+
+function Expand-Item7zip {
+	param(
+		[Parameter(Mandatory=$true)] [System.String] $Archive,
+		[Parameter(Mandatory=$true)] [System.String] $Destination
+	)
+
+	if (-not (Test-Path -Path $Archive -PathType Leaf)) {
+		throw "Specified archive file does not exist: ${Archive}"
+	}
+
+	Write-Debug "Unzipping ${Archive} to ${Destination} ..."
+
+	if (-not (Test-Path -Path $Destination -PathType Container)) {
+		New-Item $Destination -ItemType Directory | Out-Null
+	}
+
+	$ExitCode = 0
+
+	if ("${Archive}" -like "*.tgz") {
+		$Output = (& 7z x -tgzip "$Archive" -bd -y "-o${Env:Temp}")
+		$ExitCode = $LASTEXITCODE
+
+		if ($ExitCode -eq 0) {
+			$Archive = "${Env:Temp}/{0}.tar" -f [System.IO.Path]::GetFileNameWithoutExtension($Archive)
+		}
+	}
+
+	if ($ExitCode -eq 0) {
+		$Output = (& 7z x "$Archive" "-o$Destination" -aoa -bd -y -r)
+		$ExitCode = $LASTEXITCODE
+	}
+
+	if ($ExitCode -ne 0) {
+		throw "An error occurred while unzipping '${Archive}' to '${Destination}'. ${Output}"
+	}
+}
diff --git a/.gitignore b/.gitignore
index 4c82adf..8448412 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,18 +19,23 @@ mkinstalldirs
 run-tests.php
 Makefile
 Makefile.*
+!Makefile.frag
 libtool
 *~
 modules/
 .libs/
 *.la
 *.lo
+tests/**/*.*
+!tests/**/*.phpt
+!tests/smoke.php
 php_test_results_*.txt
-tests/*
-!tests/*.phpt
 http_message.loT
 *.loT
 configure.ac
 cmake-build-*
 vendor
 composer.lock
+tmp-php.ini
+*.gpg
+/*.tgz
diff --git a/.travis.yml b/.travis.yml
index 05c47f1..960f0a3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,22 +1,22 @@
 language: php
 php:
-  - 7.0
-  - 7.1
   - 7.2
+  - 7.3
+  - 7.4snapshot
   - nightly
 
 matrix:
   allow_failures:
-    - php: 7.0
     - php: nightly
 
-
 branches:
   only:
   - master
+  - travis
+  - /^v\d+\.\d+\.\d+$/
 
 before_install:
-  # Install external libraries
+  - pecl install --nodeps psr
 
 install:
   - phpize
@@ -29,3 +29,29 @@ before_script:
 script:
   make test
 
+after_failure:
+  - |
+    for FILE in $(find tests -name '*.diff'); do
+      echo "$FILE"
+      cat "$FILE"
+      echo
+    done
+
+before_deploy:
+  - openssl aes-256-cbc -K $encrypted_36ea4391f664_key -iv $encrypted_36ea4391f664_iv -in sign.gpg.enc -out sign.gpg -d
+  - gpg --import sign.gpg
+  - rm sign.gpg
+  - pecl package
+  - export RELEASE_PACKAGE=$(ls http_message-*.tgz)
+#  - echo | pecl sign "$RELEASE_PACKAGE"
+
+deploy:
+  provider: releases
+  api_key:
+    secure: "HuYxK89LGMINNzDyrA9xXVzQB7YKo563Q1EVkAOrrv7cDKbfVLTPHfI9huB8XnzDJgp1EhrXA3hMKeJdpbtzoEhiaVDc/eeFcCubhf+VEsMbke9TOYIEx7mSNDpV0igyWDR1bt+8GrCFkP04NMzzlqU+KoW0ceK/xCaMRhNWiskf3Z+e2Xne3yEVd64A61xtyPMbaOIqqmjkmqCyrN9DT+5g+zZVMz1UssdfRqMCKTyHcaYYFayQwSZgmAxZO3WKoHnd5ydoii2XIBHto7RESqAoyemw/Ksw7YdplxsZMfbCsmiSuwrlmd1t/BX7QIULeJsVwPiv7MWJWjh8VRIiFCw5FdZ+n2kLfL32MY8r4hl7Uo9nEkNaIYRBX0ryV5XXRU/2q1nuz8Knaz++Q+tPodmMp8vzmDGl2cguq9KuKbVEbiFjR+PO96YFlKdtDosUQFjbSbQOFDQdstPFSZr5s/PKPVj5Va33PyyrLI9TMZwgWLPzMEqerGXy8meigeZ0r0m3DG4slzxhxE9y30fFEx9fWPNR3AN/1NGi7WTsqonPQitu1M7YyKCdMqjWJkr++geapJwKVX/poxExQ4i0TEkgqGWmV9nJehGJ5nS8s2tH4PR8+3bA2gvNoqBO+jmd8CLjkPGbCPu8OlvdwXmeKJYsX382iGG8CxrDDPhGlr4="
+  file: "$RELEASE_PACKAGE"
+  skip_cleanup: true
+  name: "$TRAVIS_TAG"
+  prerelease: false
+  on:
+    tags: true
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 685f779..ea3d6b2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,11 +1,20 @@
 cmake_minimum_required(VERSION 3.8)
-project(http_message)
+project(http_message C)
 
-add_custom_target(extension COMMAND phpize && ./configure && make && make install
-                  WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
+add_compile_definitions(HAVE_HTTP_MESSAGE)
 
 set(SOURCE_FILES php_http_message
-        http_message.c message.c request.c uri.c server_request.c response.c stream.c)
+        http_message.c
+        message.c
+        request.c
+        server_request.c
+        response.c
+        uri.c
+        stream.c
+        uploaded_file.c
+        factory.c
+        emitter.c
+)
 
 execute_process (
         COMMAND php-config --include-dir
@@ -18,6 +27,12 @@ message("Using source directory: ${PHP_SOURCE}")
 include_directories(${PHP_SOURCE})
 include_directories(${PHP_SOURCE}/main)
 include_directories(${PHP_SOURCE}/Zend)
+include_directories(${PHP_SOURCE}/TSRM)
 include_directories(${PROJECT_SOURCE_DIR})
 
-add_executable(http_message ${SOURCE_FILES})
+add_custom_target(configure
+        COMMAND phpize && ./configure
+        DEPENDS ${SOURCE_FILES}
+        WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
+
+add_library(___ EXCLUDE_FROM_ALL ${SOURCE_FILES})
diff --git a/Makefile.frag b/Makefile.frag
new file mode 100644
index 0000000..8177ecd
--- /dev/null
+++ b/Makefile.frag
@@ -0,0 +1,25 @@
+PHP_TEST_SHARED_EXTENSIONS =  ` \
+	echo "-d extension=$(EXTENSION_DIR)/psr.so"; \
+	if test "x$(PHP_MODULES)" != "x"; then \
+		for i in $(PHP_MODULES)""; do \
+			. $$i; $(top_srcdir)/build/shtool echo -n -- " -d extension=$$dlname"; \
+		done; \
+	fi; \
+	if test "x$(PHP_ZEND_EX)" != "x"; then \
+		for i in $(PHP_ZEND_EX)""; do \
+			. $$i; $(top_srcdir)/build/shtool echo -n -- " -d $(ZEND_EXT_TYPE)=$(top_builddir)/modules/$$dlname"; \
+		done; \
+	fi`
+
+clean-tests:
+	rm -f tests/*/*.diff tests/*/*.exp tests/*/*.log tests/*/*.out tests/*/*.php tests/*/*.sh
+
+mrproper: clean
+	rm -rf autom4te.cache build modules vendor
+	rm -f acinclude.m4 aclocal.m4 config.guess config.h config.h.in config.log config.nice config.status config.sub \
+		configure configure.ac install-sh libtool ltmain.sh Makefile Makefile.fragments Makefile.global \
+		Makefile.objects missing mkinstalldirs run-tests.php
+
+package.xml: php_$(PHP_PECL_EXTENSION).h
+	$(PHP_EXECUTABLE) build-packagexml.php
+
diff --git a/README.md b/README.md
index ff61dc8..77c7b27 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,31 @@
-# PSR-7 HTTP Message as Pecl extension
+![improved PHP library](https://user-images.githubusercontent.com/100821/46372249-e5eb7500-c68a-11e8-801a-2ee57da3e5e3.png)
 
-[![Build Status](https://travis-ci.org/jasny/http_message-php-ext.svg?branch=master)](https://travis-ci.org/jasny/http_message-php-ext)
-[![Build status](https://ci.appveyor.com/api/projects/status/7rof1vr8mv4kam17/branch/master?svg=true)](https://ci.appveyor.com/project/jasny/http_message-php-ext/branch/master)
+# PSR-7 HTTP Message as PHP extension
 
-[PSR-7 HTTP Message](https://www.php-fig.org/psr/psr-7/) implementation as PHP extension written in C.
+[![Build Status](https://travis-ci.org/improved-php-library/http-message.svg?branch=master)](https://travis-ci.org/improved-php-library/http-message)
+[![Build status](https://ci.appveyor.com/api/projects/status/7rof1vr8mv4kam17/branch/master?svg=true)](https://ci.appveyor.com/project/jasny/http-message/branch/master)
+
+[PSR-7 HTTP Message](https://www.php-fig.org/psr/psr-7/) implementation as PHP extension written in C. Includes a
+[PSR-17](https://www.php-fig.org/psr/psr-17/) compatilbe factory and an emitter.
 
 ---
 
 ## Requirements
 
-* PHP 7.x
+* PHP 7.2+
+* [psr extension](https://github.com/jbboehr/php-psr)
 
 ## Installation
 
+The extension is [available from pecl](https://pecl.php.net/package/http_message).
+
+    pecl install psr
+    pecl install http_message-beta
+
+### Manual build
+
+Instead of installing this extension from pecl, you can build it manually
+
     phpize
     ./configure
     make
@@ -26,4 +39,20 @@ Add the following line to your `php.ini`
 To try out the extension, you can run the following command
 
     php -a -d extension=modules/http_message.so
+    
+# Usage
+
+```php
+use HttpMessage\Emitter;
+use HttpMessage\Factory;
+use HttpMessage\ServerRequest;
+
+$request = new ServerRequest($_SERVER, $_COOKIE, $_GET, $_POST, $_FILES);
+
+$handler = new App\Psr15Handler(); // Any PSR-15 handler.
+$response = $handler->handle($request);
+
+$emitter = new Emitter();
+$emitter->emit($response);
+```
 
diff --git a/autoconf/pecl.m4 b/autoconf/pecl.m4
new file mode 100644
index 0000000..ffa45ac
--- /dev/null
+++ b/autoconf/pecl.m4
@@ -0,0 +1,415 @@
+
+yes() {
+	true
+}
+no() {
+	false
+}
+dnl
+dnl PECL_INIT(name)
+dnl
+dnl Start configuring the PECL extension.
+dnl
+AC_DEFUN([PECL_INIT], [dnl
+	m4_define([PECL_NAME],[$1])dnl
+])dnl
+dnl
+dnl
+dnl PECL_VAR(name)
+dnl
+AC_DEFUN([PECL_VAR], [dnl
+AS_TR_CPP([PHP_]PECL_NAME[_$1])dnl
+])dnl
+dnl
+dnl PECL_CACHE_VAR(name)
+dnl
+AC_DEFUN([PECL_CACHE_VAR], [dnl
+AS_TR_SH([PECL_cv_$1])dnl
+])dnl
+dnl
+dnl PECL_SAVE_VAR(name)
+dnl
+AC_DEFUN([PECL_SAVE_VAR], [dnl
+AS_TR_SH([PECL_sv_$1])dnl
+])dnl
+dnl
+dnl PECL_DEFINE(what, to[, desc])
+dnl
+AC_DEFUN([PECL_DEFINE], [dnl
+	AC_DEFINE(PECL_VAR([$1]), ifelse([$2],,1,[$2]), ifelse([$3],,[ ],[$3]))
+])dnl
+dnl
+dnl PECL_DEFINE_UQ(what, to[, desc])
+dnl
+AC_DEFUN([PECL_DEFINE_UQ], [dnl
+	AC_DEFINE_UNQUOTED(PECL_VAR([$1]), [$2], ifelse([$3],,[ ],[$3]))
+])dnl
+dnl
+dnl PECL_DEFINE_SH(what, to[, desc])
+dnl
+AC_DEFUN([PECL_DEFINE_SH], [dnl
+	PECL_VAR([$1])=$2
+	PECL_DEFINE_UQ([$1], [$2], [$3])
+])
+dnl
+dnl PECL_DEFINE_FN(fn)
+dnl
+AC_DEFUN([PECL_DEFINE_FN], [
+	AC_DEFINE(AS_TR_CPP([HAVE_$1]), [1], [ ])
+])
+dnl
+dnl PECL_SAVE_ENV(var, ns)
+dnl
+AC_DEFUN([PECL_SAVE_ENV], [
+	PECL_SAVE_VAR([$2_$1])=[$]$1
+])
+dnl
+dnl PECL_RESTORE_ENV(var, ns)
+dnl
+AC_DEFUN([PECL_RESTORE_ENV], [
+	$1=$PECL_SAVE_VAR([$2_$1])
+])
+dnl
+dnl PECL_EVAL_LIBLINE(libline)
+dnl
+AC_DEFUN([PECL_EVAL_LIBLINE], [
+	PECL_SAVE_ENV(ext_shared, pecl)
+	ext_shared=no
+	PHP_EVAL_LIBLINE([$1], _pecl_eval_libline_dummy)
+	PECL_RESTORE_ENV(ext_shared, pecl)
+])
+dnl
+dnl PECL_PROG_EGREP
+dnl
+dnl Checks for an egrep. Defines $EGREP.
+dnl
+AC_DEFUN([PECL_PROG_EGREP], [
+	ifdef([AC_PROG_EGREP], [
+		AC_PROG_EGREP
+	], [
+		AC_CHECK_PROG(EGREP, egrep, egrep)
+	])
+])
+dnl
+dnl PECL_PROG_AWK
+dnl
+dnl Checks for an awk. Defines $AWK.
+dnl
+AC_DEFUN([PECL_PROG_AWK], [
+	ifdef([AC_PROG_AWK], [
+		AC_PROG_AWK
+	], [
+		AC_CHECK_PROG(AWK, awk, awk)
+	])
+])
+dnl
+dnl PECL_PROG_SED
+dnl
+dnl Checks for the sed program. Defines $SED.
+dnl
+AC_DEFUN([PECL_PROG_SED], [
+	ifdef([AC_PROG_SED], [
+		AC_PROG_SED
+	], [
+		ifdef([LT_AC_PROG_SED], [
+			LT_AC_PROG_SED
+		], [
+			AC_CHECK_PROG(SED, sed, sed)
+		])
+	])
+])
+dnl
+dnl PECL_PROG_PKGCONFIG
+dnl
+dnl Checks for pkg-config program and defines $PKG_CONFIG (to false if not found).
+dnl
+AC_DEFUN([PECL_PROG_PKGCONFIG], [
+	if test -z "$PKG_CONFIG"; then
+		AC_PATH_PROG([PKG_CONFIG], [pkg-config], [false])
+	fi
+])
+dnl
+dnl PECL_HAVE_PHP_EXT(name[, code-if-yes[, code-if-not]])
+dnl
+dnl Check whether ext/$name is enabled in $PHP_EXECUTABLE (PECL build)
+dnl or if $PHP_<NAME> is defined to anything else than "no" (in-tree build).
+dnl Defines shell var PECL_VAR(HAVE_EXT_<NAME>) to true or false.
+dnl
+AC_DEFUN([PECL_HAVE_PHP_EXT], [
+	AC_REQUIRE([PECL_PROG_EGREP])dnl
+	AC_CACHE_CHECK([whether ext/$1 is enabled], PECL_CACHE_VAR([HAVE_EXT_$1]), [
+		PECL_CACHE_VAR([HAVE_EXT_$1])=no
+		if test -x "$PHP_EXECUTABLE"; then
+			if $PHP_EXECUTABLE -m | $EGREP -q ^$1\$; then
+				PECL_CACHE_VAR([HAVE_EXT_$1])=yes
+			fi
+		elif test -n "$AS_TR_CPP([PHP_$1])" && test "$AS_TR_CPP([PHP_$1])" != "no"; then
+			PECL_CACHE_VAR([HAVE_EXT_$1])=yes
+		fi
+	])
+	if $PECL_CACHE_VAR([HAVE_EXT_$1]); then
+		PECL_VAR([HAVE_EXT_$1])=true
+		PECL_DEFINE([HAVE_EXT_$1])
+		$2
+	else
+		PECL_VAR([HAVE_EXT_$1])=false
+		$3
+	fi
+])
+dnl
+dnl PECL_HAVE_PHP_EXT_HEADER(ext[, header])
+dnl
+dnl Check where to find a header for ext and add the found dir to $INCLUDES.
+dnl If header is not specified php_<ext>.h is assumed.
+dnl Defines shell var PHP_<PECL_NAME>_EXT_<EXT>_INCDIR to the found dir.
+dnl Defines PHP_<PECL_NAME>_HAVE_<HEADER> to the found path.
+dnl
+AC_DEFUN([PECL_HAVE_PHP_EXT_HEADER], [dnl
+	AC_REQUIRE([PECL_PROG_SED])dnl
+	m4_define([EXT_HEADER], ifelse([$2],,php_$1.h,[$2]))dnl
+	AC_CACHE_CHECK([for EXT_HEADER of ext/$1], PECL_CACHE_VAR([EXT_$1]_INCDIR), [
+		for i in $(printf "%s" "$INCLUDES" | $SED -e's/-I//g') $abs_srcdir ../$1; do
+			if test -d $i; then
+				for j in $i/EXT_HEADER $i/ext/$1/EXT_HEADER; do
+					if test -f $j; then
+						PECL_CACHE_VAR([EXT_$1]_INCDIR)=$(dirname "$j")
+						break
+					fi
+				done
+			fi
+		done
+	])
+	PECL_VAR([EXT_$1]_INCDIR)=$PECL_CACHE_VAR([EXT_$1]_INCDIR)
+	PHP_ADD_INCLUDE([$PECL_VAR([EXT_$1]_INCDIR)])
+	PECL_DEFINE_UQ([HAVE_]EXT_HEADER, "$PECL_VAR([EXT_$1]_INCDIR)/EXT_HEADER")
+])
+dnl
+dnl PECL_HAVE_CONST(header, const[, type=int[, code-if-yes[, code-if-mno]]])
+dnl
+AC_DEFUN([PECL_HAVE_CONST], [dnl
+	AC_REQUIRE([PECL_PROG_EGREP])dnl
+	AC_CACHE_CHECK([for $2 in $1], PECL_CACHE_VAR([HAVE_$1_$2]), [
+		AC_TRY_COMPILE([
+			#include "$1"
+		], [
+			]ifelse([$3],,int,[$3])[ _c = $2;
+			(void) _c;
+		], [
+			PECL_CACHE_VAR([HAVE_$1_$2])=yes
+		], [
+			PECL_CACHE_VAR([HAVE_$1_$2])=no
+		])
+	])
+	if $PECL_CACHE_VAR([HAVE_$1_$2]); then
+		PECL_DEFINE([HAVE_$2])
+		$4
+	else
+		ifelse([$5],,:,[$5])
+	fi
+])
+dnl
+dnl _PECL_TR_VERSION(version)
+dnl
+AC_DEFUN([_PECL_TR_VERSION], [dnl
+AC_REQUIRE([PECL_PROG_AWK])dnl
+$(printf "%s" $1 | $AWK -F "[.]" '{print $[]1*1000000 + $[]2*10000 + $[]3*100 + $[]4}')
+])
+dnl
+dnl PECL_CHECKED_VERSION(name)
+dnl
+dnl Shell var name of an already checked version.
+dnl
+AC_DEFUN([PECL_CHECKED_VERSION], [PECL_VAR([$1][_VERSION])])
+dnl
+dnl PECL_HAVE_VERSION(name, min-version[, code-if-yes[, code-if-not]])
+dnl
+dnl Perform a min-version check while in an PECL_CHECK_* block.
+dnl Expands AC_MSG_ERROR when code-if-not is empty and the version check fails.
+dnl
+AC_DEFUN([PECL_HAVE_VERSION], [
+	aversion=_PECL_TR_VERSION([$PECL_CHECKED_VERSION([$1])])
+	mversion=_PECL_TR_VERSION([$2])
+	AC_MSG_CHECKING([whether $1 version $PECL_CHECKED_VERSION([$1]) >= $2])
+	if test -z "$aversion" || test "$aversion" -lt "$mversion"; then
+		ifelse($4,,AC_MSG_ERROR([no]), [
+			AC_MSG_RESULT([no])
+			$4
+		])
+	else
+		AC_MSG_RESULT([ok])
+		$3
+	fi
+])
+dnl
+dnl PECL_CHECK_CUSTOM(name, path, header, lib, version)
+dnl
+AC_DEFUN([PECL_CHECK_CUSTOM], [
+	PECL_SAVE_ENV([CPPFLAGS], [$1])
+	PECL_SAVE_ENV([LDFLAGS], [$1])
+	PECL_SAVE_ENV([LIBS], [$1])
+
+	AC_MSG_CHECKING([for $1])
+	AC_CACHE_VAL(PECL_CACHE_VAR([$1_prefix]), [
+		for path in $2 /usr/local /usr /opt; do
+			if test "$path" = "" || test "$path" = "yes" || test "$path" = "no"; then
+				continue
+			elif test -f "$path/include/$3"; then
+				PECL_CACHE_VAR([$1_prefix])="$path"
+				break
+			fi
+		done
+	])
+	if test -n "$PECL_CACHE_VAR([$1_prefix])"; then
+		CPPFLAGS="-I$PECL_CACHE_VAR([$1_prefix])/include"
+		LDFLAGS="-L$PECL_CACHE_VAR([$1_prefix])/$PHP_LIBDIR"
+		LIBS="-l$4"
+		PECL_EVAL_LIBLINE([$LDFLAGS $LIBS])
+		
+		AC_CACHE_VAL(PECL_CACHE_VAR([$1_version]), [
+			pushd $PECL_CACHE_VAR([$1_prefix]) >/dev/null
+			PECL_CACHE_VAR([$1_version])=$5
+			popd >/dev/null
+		])
+		PECL_CHECKED_VERSION([$1])=$PECL_CACHE_VAR([$1_version])
+
+		if test -n "$PECL_CHECKED_VERSION([$1])"; then
+			PECL_VAR([HAVE_$1])=true
+			PECL_DEFINE([HAVE_$1])
+			PECL_DEFINE_UQ($1[_VERSION], "$PECL_CHECKED_VERSION([$1])")
+		else
+			PECL_VAR([HAVE_$1])=false
+		fi
+	else
+		PECL_VAR([HAVE_$1])=false
+	fi
+	AC_MSG_RESULT([${PECL_CHECKED_VERSION([$1]):-no}])
+])
+dnl
+dnl PECL_CHECK_CONFIG(name, prog-config, version-flag, cppflags-flag, ldflags-flag, libs-flag)
+dnl
+AC_DEFUN([PECL_CHECK_CONFIG], [
+	PECL_SAVE_ENV([CPPFLAGS], [$1])
+	PECL_SAVE_ENV([LDFLAGS], [$1])
+	PECL_SAVE_ENV([LIBS], [$1])
+
+
+	AC_MSG_CHECKING([for $1])
+	ifelse($2, [$PKG_CONFIG $1], [
+		AC_CACHE_VAL(PECL_CACHE_VAR([$1_exists]), [
+			if $($2 --exists); then
+				PECL_CACHE_VAR([$1_exists])=yes
+			else
+				PECL_CACHE_VAR([$1_exists])=no
+			fi
+		])
+		if $PECL_CACHE_VAR([$1_exists]); then
+	])
+		AC_CACHE_VAL(PECL_CACHE_VAR([$1_version]), [
+			PECL_CACHE_VAR([$1_version])=$($2 $3)
+		])
+		PECL_CHECKED_VERSION([$1])=$PECL_CACHE_VAR([$1_version])
+		AC_CACHE_VAL(PECL_CACHE_VAR([$1_cppflags]), [
+			PECL_CACHE_VAR([$1_cppflags])=$($2 $4)
+		])
+		CPPFLAGS=$PECL_CACHE_VAR([$1_cppflags])
+		AC_CACHE_VAL(PECL_CACHE_VAR([$1_ldflags]), [
+			PECL_CACHE_VAR([$1_ldflags])=$($2 $5)
+		])
+		LDFLAGS=$PECL_CACHE_VAR([$1_ldflags])
+		AC_CACHE_VAL(PECL_CACHE_VAR([$1_libs]), [
+			PECL_CACHE_VAR([$1_libs])=$($2 $6)
+		])
+		LIBS=$PECL_CACHE_VAR([$1_libs])
+		PECL_EVAL_LIBLINE([$LDFLAGS $LIBS])
+	ifelse($2, [$PKG_CONFIG $1], [
+		fi
+	])
+
+	if test -n "$PECL_CHECKED_VERSION([$1])"; then
+		PECL_VAR([HAVE_$1])=true
+		PECL_DEFINE([HAVE_$1])
+		PECL_DEFINE_UQ([$1_VERSION], "$PECL_CHECKED_VERSION([$1])")
+	else
+		PECL_VAR([HAVE_$1])=false
+	fi
+
+	AC_MSG_RESULT([${PECL_CHECKED_VERSION([$1]):-no}])
+])
+dnl
+dnl PECL_CHECK_PKGCONFIG(pkg[, additional-pkg-config-path])
+dnl
+AC_DEFUN([PECL_CHECK_PKGCONFIG], [dnl
+	AC_REQUIRE([PECL_PROG_PKGCONFIG])dnl
+	ifelse($2,,, [
+		PECL_SAVE_VAR(pkgconfig_path)="$PKG_CONFIG_PATH"
+		if test -d "$2"; then
+			export PKG_CONFIG_PATH="$2/lib/pkgconfig:$PKG_CONFIG_PATH"
+		fi
+	])
+	PECL_CHECK_CONFIG([$1], [$PKG_CONFIG $1], [--modversion], [--cflags-only-I], [--libs-only-L], [--libs-only-l])
+	ifelse($2,,, [
+		PKG_CONFIG_PATH="$PECL_SAVE_VAR(pkgconfig_path)"
+	])
+])
+dnl
+dnl PECL_CHECK_DONE(name, success[, incline, libline])
+dnl
+AC_DEFUN([PECL_CHECK_DONE], [
+	if $2; then
+		incline=$CPPFLAGS
+		libline="$LDFLAGS $LIBS"
+		PECL_DEFINE([HAVE_$1])
+	else
+		incline=$3
+		libline=$4
+	fi
+
+	PECL_RESTORE_ENV([CPPFLAGS], [$1])
+	PECL_RESTORE_ENV([LDFLAGS], [$1])
+	PECL_RESTORE_ENV([LIBS], [$1])
+
+	PHP_EVAL_INCLINE([$incline])
+	PHP_EVAL_LIBLINE([$libline], AS_TR_CPP(PECL_NAME[_SHARED_LIBADD]))
+])
+
+dnl
+dnl PECL_CHECK_CA([additional-ca-paths,[ additional-ca-bundles]])
+dnl
+AC_DEFUN([PECL_CHECK_CA], [
+	AC_CACHE_CHECK([for default CA path], PECL_CACHE_VAR([CAPATH]), [
+		PECL_VAR([CAPATH])=
+		for ca_path in $1 \
+			/etc/ssl/certs \
+			/System/Library/OpenSSL
+		do
+			# check if it's actually a hashed directory
+			if test -d "$ca_path" && ls "$ca_path"/@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@@<:@0-9a-f@:>@.0 >/dev/null 2>&1; then
+				PECL_CACHE_VAR([CAPATH])=$ca_path
+				break
+			fi
+		done
+	])
+	if test -n "$PECL_CACHE_VAR([CAPATH])"; then
+		PECL_DEFINE_SH([CAPATH], "$PECL_CACHE_VAR([CAPATH])")
+	fi
+
+	AC_CACHE_CHECK([for default CA info], PECL_CACHE_VAR([CAINFO]), [
+		for ca_info in $2 \
+			/etc/ssl/{cert,ca-bundle}.pem \
+			/{etc,usr/share}/ssl/certs/ca-{bundle,ceritifcates}.crt \
+			/etc/{pki/ca-trust,ca-certificates}/extracted/pem/tls-ca-bundle.pem \
+			/etc/pki/tls/certs/ca-bundle{,.trust}.crt \
+			/usr/local/etc/{,open}ssl/cert.pem \
+			/usr/local/share/certs/ca-root-nss.crt \
+			/{usr,usr/local,opt}/local/share/curl/curl-ca-bundle.crt
+		do
+			if test -f "$ca_info"; then
+				PECL_CACHE_VAR([CAINFO])=$ca_info
+				break
+			fi
+		done
+	])
+	if test -n "$PECL_CACHE_VAR([CAINFO])"; then
+		PECL_DEFINE_SH([CAINFO], "$PECL_CACHE_VAR([CAINFO])")
+	fi
+])
diff --git a/autoconf/php-executable.m4 b/autoconf/php-executable.m4
new file mode 100644
index 0000000..2e6e16e
--- /dev/null
+++ b/autoconf/php-executable.m4
@@ -0,0 +1,7 @@
+dnl
+dnl Generate run-php bash script
+dnl
+AC_CONFIG_COMMANDS_POST([
+  rm -f build/php
+  ln -s "$PHP_EXECUTABLE" build/php
+])
diff --git a/build-packagexml.php b/build-packagexml.php
new file mode 100644
index 0000000..0784030
--- /dev/null
+++ b/build-packagexml.php
@@ -0,0 +1,453 @@
+<?php
+
+/**
+ * Generate package.xml for pecl extensions.
+ * Works for new and existing extensions.
+ *
+ * Add it to `Makefile.frag`
+ *   package.xml: php_$(PHP_PECL_EXTENSION).h
+ *   	$(PHP_EXECUTABLE) build-packagexml.php
+ *
+ * Usage
+ *   cat ~/my-release-notes.txt | make package.xml
+ *
+ * @author Arnold Daniels <arnold@jasny.net>
+ * @license PHP License
+ * @license MIT
+ */
+
+declare (strict_types = 1);
+
+set_exception_handler(function (Throwable $exception): void {
+    $msg = ($exception instanceof RuntimeException ? "" : "Unexpected error: ") . $exception->getMessage();
+
+    fwrite(STDERR, "\033[0;31m" /* red */ . $msg . "\033[0m" . "\n");
+    exit(1);
+});
+
+if (!extension_loaded('dom') || !extension_loaded('simplexml') || !extension_loaded('spl')) {
+    throw new RuntimeException("Following extensions are required: DOM, SimpleXML and SPL");
+}
+
+// 1. Load package.xml and create release
+$package = (function (): PackageXMLElement {
+    return file_exists('package.xml')
+        ? simplexml_load_file('package.xml', PackageXMLElement::class)
+        : PackageXMLElement::create();
+})();
+
+$release = new Release();
+
+// 2. Process php_{extname}.h
+(function () use ($package, $release): void {
+    $extname = (string)$package->name ?: getenv('PHP_PECL_EXTENSION') ?: null;
+
+    if ($extname !== null) {
+        $filename = "php_{$extname}.h";
+
+        if (!file_exists($filename)) {
+            throw new RuntimeException("{$filename} not found");
+        }
+    } else {
+        $filename = glob('php_*.h')[0] ?? null;
+
+        if ($filename === null) {
+            throw new RuntimeException("Couldn't find the main header file (php_*.h)");
+        }
+
+        $extname = preg_replace('/^php_(.+)\.h$/', '$1', $filename);
+    }
+
+    if ((string)$package->name === '') {
+        $package->name = $extname;
+    }
+
+
+    $macroPrefix = strtoupper(pathinfo($filename, PATHINFO_FILENAME));
+    $contents = file_get_contents($filename);
+
+    preg_match_all("/^[ \t]*#define\s+{$macroPrefix}_(?<key>\w+)[ \t]+\"(?<value>.+)\"/m", $contents, $matches,
+        PREG_PATTERN_ORDER);
+    $macros = array_combine($matches['key'], $matches['value']);
+
+    // Package name
+    if (isset($macros['EXTNAME'])) {
+        if ((string)$package->name !== '' && (string)$package->name !== $macros['EXTNAME']) {
+            throw new RuntimeException("Package name '{$package->name}' (package.xml) doesn't match "
+                . "{$macroPrefix}_EXTNAME '{$macros['EXTNAME']}' ($filename)");
+        }
+    }
+
+    // Release version
+    if (!isset($macros['VERSION'])) {
+        throw new RuntimeException("$filename does not contain {$macroPrefix}_VERSION macro");
+    }
+    $release->version = $macros['VERSION'];
+
+    if (strpos($release->version, 'dev') !== false) {
+        throw new RuntimeException("Development versions shouldn't be released ({$macros['VERSION']}). "
+            . "Please change {$macroPrefix}_VERSION in $filename.");
+    }
+    if ($release->existsIn($package->changelog)) {
+        throw new RuntimeException("Version {$macros['VERSION']} already released. "
+            . "Please change {$macroPrefix}_VERSION in $filename.");
+    }
+})();
+
+// 3. Copy info from package to release
+(function () use ($package, $release): void {
+    $release->license = (string)$package->license;
+    $release->licenseUri = (string)$package->license['uri'];
+
+    if (strpos($release->version, 'alpha') !== false) {
+        $release->stability = 'alpha';
+    } else if (strpos($release->version, 'beta') !== false || strpos($release->version, 'RC') !== false) {
+        $release->stability = 'beta';
+    } else if (substr($release->version, 0, 1) === '0') {
+        $release->stability = ((string)$package->stability->release) ?: 'alpha';
+    } else {
+        $release->stability = 'stable';
+    }
+
+    if ($release->isMajor((string)$package->version->release)) {
+        $release->apiVersion = $release->version;
+        $release->apiStability = $release->stability;
+    } else {
+        $release->apiVersion = (string)$package->version->api;
+        $release->apiStability = (string)$package->stability->api;
+    }
+})();
+
+// 4. Get release notes from stdin
+$release->notes = (function (string $name, string $version): string {
+    if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
+        fwrite(STDOUT, "Enter the release notes for $name $version (end with Ctrl-D):\n");
+    }
+
+    $notes = '';
+
+    do {
+        $notes .= fread(STDIN, 1024);
+    } while (!feof(STDIN));
+
+    return trim($notes);
+})((string)$package->name, $release->version);
+
+// 5. Add release to package
+if (!isset($package->changelog)) {
+    $package->addChild('changelog');
+}
+$release->update($package->changelog->prependChild('release'));
+$release->update($package);
+
+// 6. Removes files that no longer exist from package.xml
+(function () use ($package): void {
+    $fileElements = $package->xpath('p:contents//p:file');
+
+    foreach ($fileElements as $element) {
+        $path = join("/", array_map('strval', $element->xpath('ancestor-or-self::*[@name!="/"]/@name')));
+        
+        if (!file_exists($path)) {
+            unset($element[0]);
+        }
+    };
+})();
+
+// 7. Add new files to package.xml
+(function () use ($package): void {
+    $ext = ['c', 'h', 'phpt'];
+
+    $dir = new RecursiveDirectoryIterator('.', FilesystemIterator::SKIP_DOTS | FilesystemIterator::CURRENT_AS_PATHNAME);
+    $itDir = new RecursiveIteratorIterator($dir);
+    $itReg = new RegexIterator($itDir, '~^./(.+\.(?:' . join('|', $ext) . '))$~', RegexIterator::GET_MATCH);
+    $files = iterable_column($itReg, 1);
+
+    $newFiles = new CallbackFilterIterator($files, function ($path) use ($package): bool {
+        $file = basename($path);
+        $dirs = dirname($path) !== '.' ? explode('/', dirname($path)) : [];
+        array_unshift($dirs, '/');
+
+        $xpath = 'p:dir[@name="' . join('"]/p:dir[@name="', $dirs) . '"]/p:file[@name="' . $file . '"]';
+        return count($package->contents->xpath($xpath)) === 0;
+    });
+
+    if (is_dir('.git')) {
+        $newFiles = gitignore_filter($newFiles);
+    }
+
+    foreach ($newFiles as $file) {
+        $dirs = dirname($file) !== '.' ? explode('/', dirname($file)) : [];
+
+        $dirElement = array_reduce($dirs, function (SimpleXMLElement $parent, string $dir): SimpleXMLElement {
+            $cur = $parent->xpath('p:dir[@name="' . $dir . '"]')[0] ?? null;
+
+            if ($cur === null) {
+                $cur = $parent->addChild('dir');
+                $cur['name'] = $dir;
+            }
+
+            return $cur;
+        }, $package->contents->dir[0]);
+
+        $fileElement = $dirElement->addChild('file');
+        $fileElement['name'] = basename($file);
+        $fileElement['role'] = pathinfo($file, PATHINFO_EXTENSION) === 'phpt' ? 'test' : 'src';
+    }
+})();
+
+// 8. Remove empty dirs from package.xml
+(function () use ($package): void {
+    $emptyDirs = $package->xpath('p:contents//p:dir[not(descendant::*[local-name()="file"])]');
+
+    foreach (array_reverse($emptyDirs) as $element) { // reverse so children are deleted first
+        unset($element[0]);
+    }
+})();
+
+// 9. Sort files
+(function () use ($package): void {
+    $dirElements = $package->xpath('p:contents//p:dir');
+
+    $sorter = function(SimpleXMLElement $first, SimpleXMLElement $second): int {
+        return
+            (($first->getName() !== 'file') <=> ($second->getName() !== 'file')) ?:
+            strcasecmp((string)$first['name'], (string)$second['name']);
+    };
+
+    foreach ($dirElements as $element) {
+        $element->sort($sorter);
+    }
+})();
+
+// 10. Save package.xml
+$package->asXML('package.xml');
+
+// :) friendly message
+echo "\033[0;32m", /* green */ "Updated package.xml for {$package->name} {$release->version}", "\033[0;0m", "\n";
+
+// ================= classes and functions ====================
+
+class Release
+{
+    public $version;
+    public $apiVersion;
+    public $stability;
+    public $apiStability;
+
+    public $license;
+    public $licenseUri;
+    public $notes;
+
+    public function isMajor(string $oldVersion): bool
+    {
+        if ($oldVersion === '') {
+            return true;
+        }
+
+        [$major, $minor] = explode('.', $this->version);
+        [$oldMajor, $oldMinor] = explode('.', $oldVersion);
+
+        return ($major !== $oldMajor) || ($major === '0' && $minor !== $oldMinor);
+    }
+
+    public function existsIn(SimpleXMLElement $changelog): bool
+    {
+        if (!isset($changelog->release)) {
+            return false;
+        }
+
+        foreach ($changelog->release as $release) {
+            if ((string)($release->version->release) === $this->version) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public function update(SimpleXMLElement $element)
+    {
+        $noTime = isset($element->date) && !isset($element->time);
+
+        $element->date = date('Y-m-d');
+        if (!$noTime) {
+            $element->time = date('H:i:s');
+        }
+        
+        $element->version->release = $this->version;
+        $element->version->api = $this->apiVersion;
+        $element->stability->release = $this->stability;
+        $element->stability->api = $this->apiStability;
+
+        $element->license = $this->license;
+        $element->license['uri'] = $this->licenseUri;
+
+        $indent = str_repeat(' ', count($element->xpath('ancestor::*')) + 1);
+        $element->notes = "\n" . $this->notes . "\n$indent";
+    }
+};
+
+class PackageXMLElement extends SimpleXMLElement
+{
+    private const BLANK_PACKAGE = <<<XML
+<?xml version="1.0" encoding="UTF-8"?>
+<package version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
+ <name></name>
+ <channel>pecl.php.net</channel>
+ <summary></summary>
+ <description></description>
+ <lead>
+  <name></name>
+  <user></user>
+  <email></email>
+  <active>yes</active>
+ </lead>
+ <date></date>
+ <version>
+  <release></release>
+  <api></api>
+ </version>
+ <stability>
+  <release></release>
+  <api></api>
+ </stability>
+ <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+ <notes></notes>
+ <contents>
+  <dir name="/">
+   <file name="config.m4" role="src"/>
+   <file name="config.m32" role="src"/>
+   <file name="CREDITS" role="doc"/>
+   <file name="LICENSE" role="doc"/>
+   <file name="README.md" role="doc"/>
+   <dir name="autoconf">
+    <file name="pecl.m4" role="src"/>
+    <file name="php-executable.m4" role="src"/>
+   </dir>
+  </dir>
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>7.0.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.3</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <providesextension></providesextension>
+ <extsrcrelease />
+ <changelog></changelog>
+</package>
+XML;
+
+    public static function create(): self
+    {
+        return new static(self::BLANK_PACKAGE);
+    }
+
+    public function prependChild($name, $value = '')
+    {
+        $dom = dom_import_simplexml($this);
+
+        $new = $dom->insertBefore(
+            $dom->ownerDocument->createElement($name, $value),
+            $dom->firstChild
+        );
+
+        return simplexml_import_dom($new, __CLASS__);
+    }
+
+    public function sort(callable $sorter): void
+    {
+        if ($this->count() === 0) {
+            return;
+        }
+
+        $children = iterator_to_array($this->children(), false);
+        usort($children, $sorter);
+
+        $dom = dom_import_simplexml($this);
+
+        while ($dom->hasChildNodes()) {
+            $dom->removeChild($dom->firstChild);
+        }
+
+        foreach ($children as $child) {
+            $dom->appendChild(dom_import_simplexml($child));
+        }
+    }
+
+    public function asXML($filename = null)
+    {
+        $dom = new DOMDocument("1.0");
+        $dom->preserveWhiteSpace = false;
+        $dom->formatOutput = true;
+        $dom->loadXML(parent::asXML());
+
+        $xml = $dom->saveXML();
+
+        // Go from 2 space indentation to 1.
+        $xml = preg_replace('~^([ ]+)\1<(?!/notes>)~m', '\1<', $xml);
+
+        if ($filename === null) {
+            return $xml;
+        }
+
+        if (file_exists($filename)) {
+            rename($filename, $filename . '~');
+        }
+        file_put_contents($filename, $xml);
+    }
+
+    public function xpath($xpath)
+    {
+        $this->registerXPathNamespace('p', 'http://pear.php.net/dtd/package-2.0');
+
+        return parent::xpath($xpath);
+    }
+}
+
+function iterable_column(iterable $iterable, $column): Generator
+{
+    foreach ($iterable as $key => $item) {
+        yield $key => $item[$column];
+    }
+}
+
+function gitignore_filter(iterable $files): Generator
+{
+    $spec = [
+       ["pipe", "r"],
+       ["pipe", "w"],
+       ["pipe", "w"],
+    ];
+
+    $process = proc_open('git check-ignore --non-matching --verbose --stdin', $spec, $pipes);
+    stream_set_timeout($pipes[1], 1);
+    stream_set_blocking($pipes[2], false);
+
+    foreach ($files as $file) {
+        fwrite($pipes[0], $file . "\n");
+        $line = fgets($pipes[1], 1024);
+
+        if ($line === false || substr($line, 0, 2) === '::') {
+            yield $file;
+        }
+    }
+
+    $err = fread($pipes[2], 10000);
+
+    fclose($pipes[0]);
+    fclose($pipes[1]);
+    fclose($pipes[2]);
+
+    $ret = proc_close($process);
+
+    if ($ret !== 0 && $err !== '') {
+        fwrite(STDERR, "\033[0;31m" /* red */ . $err . "\033[0m" /* no color */ . "\n");
+        throw new RuntimeException("Checking .gitignore failed");
+    }
+}
+
diff --git a/composer.json b/composer.json
deleted file mode 100644
index 17f6005..0000000
--- a/composer.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-    "require-dev": {
-        "phpunit/phpunit": "^7.5",
-        "php-http/psr7-integration-tests": "dev-master",
-        "http-interop/http-factory-tests": "dev-master"
-    }
-}
-
diff --git a/config.m4 b/config.m4
index fd7df05..6bfded3 100644
--- a/config.m4
+++ b/config.m4
@@ -1,14 +1,19 @@
 dnl $Id$
 dnl config.m4 for extension http_message
 
-PHP_ARG_WITH(http_message, for http_message support,
-[  --with-http_message       Include http_message support])
+sinclude(./autoconf/pecl.m4)
+sinclude(./autoconf/php-executable.m4)
+
+PECL_INIT([http_message])
+
+PHP_ARG_ENABLE(http-message, whether to enable http-message, [ --enable-http-message   Enable http_message])
 
 if test "$PHP_HTTP_MESSAGE" != "no"; then
-  # TODO: Load external depenencies here  
+	PECL_HAVE_PHP_EXT([psr], [PECL_HAVE_PHP_EXT_HEADER([psr])], [AC_MSG_ERROR([please install and enable the psr extension])])
 
-  AC_DEFINE(HAVE_HTTP_MESSAGE, 1, [Whether you have http_message support])
-  PHP_NEW_EXTENSION(http_message, http_message.c message.c request.c server_request.c response.c stream.c uri.c,
-        $ext_shared)
-fi
+    AC_DEFINE(HAVE_HTTP_MESSAGE, 1, [Whether you have http_message])
+    PHP_NEW_EXTENSION(http_message, http_message.c message.c request.c server_request.c response.c stream.c uri.c uploaded_file.c factory.c emitter.c, $ext_shared)
 
+	PHP_ADD_MAKEFILE_FRAGMENT
+	PHP_INSTALL_HEADERS([ext/http_message], [php_http_message.h])
+fi
diff --git a/config.w32 b/config.w32
index 4feb5bc..13006d3 100644
--- a/config.w32
+++ b/config.w32
@@ -1,13 +1,10 @@
 // $Id$
 // vim:ft=javascript
 
-ARG_WITH("http_message", "Include http_message support", "no");
+ARG_ENABLE("http-message", "enable http_message", "no");
 
 if (PHP_HTTP_MESSAGE != "no") {
-    if (true /* TODO: check external libs */) {
-		EXTENSION("http_message", "http_message.c message.c request.c server_request.c response.c stream.c uri.c");
-   		AC_DEFINE('HAVE_HTTP_MESSAGE', 1 , 'Have http_message support');
-    	PHP_INSTALL_HEADERS("ext/http_message/", "php_http_message.h");
-	}
+	EXTENSION("http_message", "http_message.c message.c request.c server_request.c response.c stream.c uri.c uploaded_file.c factory.c emitter.c");
+	AC_DEFINE('HAVE_HTTP_MESSAGE', 1 , 'enable http_message support');
+	PHP_INSTALL_HEADERS("ext/http_message/", "php_http_message.h");
 }
-
diff --git a/emitter.c b/emitter.c
new file mode 100644
index 0000000..7003918
--- /dev/null
+++ b/emitter.c
@@ -0,0 +1,214 @@
+/*
+  +----------------------------------------------------------------------+
+  | HTTP Message PHP extension - Stream class                            |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 2019 Arnold Daniels                                    |
+  +----------------------------------------------------------------------+
+  | Permission is hereby granted, free of charge, to any person          |
+  | obtaining a copy of this software and associated documentation files |
+  | (the "Software"), to deal in the Software without restriction,       |
+  | including without limitation the rights to use, copy, modify, merge, |
+  | publish, distribute, sublicense, and/or sell copies of the Software, |
+  | and to permit persons to whom the Software is furnished to do so,    |
+  | subject to the following conditions:                                 |
+  |                                                                      |
+  | The above copyright notice and this permission notice shall be       |
+  | included in all copies or substantial portions of the Software.      |
+  |                                                                      |
+  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,      |
+  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF   |
+  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                |
+  | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS  |
+  | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN   |
+  | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN    |
+  | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE     |
+  | SOFTWARE.                                                            |
+  +----------------------------------------------------------------------+
+  | Author: Arnold Daniels <arnold@jasny.net>                            |
+  +----------------------------------------------------------------------+
+*/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "SAPI.h"
+#include "php.h"
+#include "macros.h"
+#include "response.h"
+#include "zend_exceptions.h"
+#include "zend_interfaces.h"
+#include "ext/spl/spl_exceptions.h"
+
+#if HAVE_HTTP_MESSAGE
+
+zend_class_entry *HttpMessage_Emitter_ce = NULL;
+
+int assert_no_headers_sent()
+{
+    if (!SG(headers_sent)) {
+        return SUCCESS;
+    }
+
+    const char *file = php_output_get_start_filename();
+    int line = php_output_get_start_lineno();
+
+    if (file != NULL) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0,
+                "Cannot send session cookie - headers already sent by (output started at %s:%d)", file, line);
+    } else {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Cannot send session cookie - headers already sent");
+    }
+
+    return FAILURE;
+}
+
+int read_response_body(zval *response, zval *contents)
+{
+    zval body;
+
+    ZVAL_NULL(&body);
+    ZVAL_NULL(contents);
+
+    zend_call_method_with_0_params(PROPERTY_ARG(response), NULL, NULL, "getBody", &body);
+
+    if (EXPECTED(Z_TYPE(body) == IS_OBJECT)) {
+        zend_call_method_with_0_params(PROPERTY_ARG(&body), NULL, NULL, "__toString", contents);
+    }
+
+    if (UNEXPECTED(Z_TYPE_P(contents) != IS_STRING)) {
+        return FAILURE; // Something went wrong, assuming an error was already thrown.
+    }
+
+    return SUCCESS;
+}
+
+void emit_status(zval *response)
+{
+    zval version, status_code, reason_phrase;
+    zend_long code;
+    char *phrase = NULL;
+    size_t phrase_len = 0;
+    sapi_header_line ctr = {NULL, 0, 0};
+
+    ZVAL_NULL(&status_code);
+    ZVAL_NULL(&reason_phrase);
+
+    zend_call_method_with_0_params(PROPERTY_ARG(response), NULL, NULL, "getProtocolVersion", &version);
+    zend_call_method_with_0_params(PROPERTY_ARG(response), NULL, NULL, "getStatusCode", &status_code);
+    zend_call_method_with_0_params(PROPERTY_ARG(response), NULL, NULL, "getReasonPhrase", &reason_phrase);
+    
+    code = Z_LVAL(status_code);
+
+    if (Z_STRLEN(reason_phrase) > 0) {
+        phrase = Z_STRVAL(reason_phrase);
+        phrase_len = Z_STRLEN(reason_phrase);
+    } else {
+        phrase = (char *)get_status_string((int)code);
+        phrase_len = strlen(phrase);
+    }
+
+    ctr.line_len = Z_STRLEN(version) + phrase_len + 10;
+    ctr.line = emalloc(ctr.line_len);
+    zend_sprintf((char *)ctr.line, "HTTP/%.*s %3lu %.*s", (int)Z_STRLEN(version), Z_STRVAL(version), code, (int)phrase_len, phrase);
+    ctr.response_code = code;
+
+    sapi_header_op(SAPI_HEADER_REPLACE, &ctr);
+
+    efree((char *)ctr.line);
+}
+
+void emit_header(zend_string *header_name, zend_array *header_lines)
+{
+    zval *header_line;
+    size_t max_header_size = 256;
+    sapi_header_line ctr = {NULL, 0, 0};
+    ctr.line = emalloc(256);
+
+    ZEND_HASH_FOREACH_VAL(header_lines, header_line) {
+        ctr.line_len = ZSTR_LEN(header_name) + Z_STRLEN_P(header_line) + 2;
+
+        // Reuse ctr: resize if needed.
+        if (ctr.line_len >= max_header_size) {
+            efree((char *)ctr.line);
+            max_header_size = (ctr.line_len + 255) - ((ctr.line_len + 255) % 256);
+            ctr.line = emalloc(max_header_size);
+        }
+
+        zend_sprintf((char *)ctr.line, "%s: %s", ZSTR_VAL(header_name), Z_STRVAL_P(header_line));
+        sapi_header_op(SAPI_HEADER_ADD, &ctr);
+    } ZEND_HASH_FOREACH_END();
+
+    efree((char *)ctr.line);
+}
+
+void emit_headers(zval *response)
+{
+    zval headers;
+    zval *header_lines;
+    zend_string *header_name;
+    zend_long index;
+
+    zend_call_method_with_0_params(PROPERTY_ARG(response), NULL, NULL, "getHeaders", &headers);
+
+    ZEND_HASH_FOREACH_KEY_VAL(Z_ARR(headers), index, header_name, header_lines) {
+        if (UNEXPECTED(header_name == NULL)) {
+            zend_error(E_WARNING, "Unexpected response header key '%ld': header names should not be numeric", index);
+            continue;
+        }
+
+        emit_header(header_name, Z_ARR_P(header_lines));
+    } ZEND_HASH_FOREACH_END();
+}
+
+ZEND_BEGIN_ARG_INFO_EX(arginfo_HttpMessageEmitter_emit, 0, 0, 0)
+    ZEND_ARG_OBJ_INFO(0, serverParams, Psr\\Http\\Message\\ResponseInterface, 0)
+ZEND_END_ARG_INFO()
+
+PHP_METHOD(Emitter, emit)
+{
+    zval *response = NULL;
+    zval contents;
+
+    zend_class_entry *response_interface = HTTP_MESSAGE_PSR_INTERFACE("response");
+
+    if (UNEXPECTED(response_interface == NULL)) {
+        zend_throw_error(NULL, "Psr\\Http\\Message\\ResponseInterface not found");
+        return;
+    }
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
+        Z_PARAM_OBJECT_OF_CLASS(response, response_interface)
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (UNEXPECTED(assert_no_headers_sent() == FAILURE)) {
+        return;
+    }
+
+    if (UNEXPECTED(read_response_body(response, &contents) == FAILURE)) {
+        return;
+    }
+
+    emit_headers(response);
+    emit_status(response);
+    zend_write(Z_STRVAL(contents), Z_STRLEN(contents));
+}
+
+/* Define HttpMessage\Emitter class */
+
+static const zend_function_entry methods[] = {
+    PHP_ME(Emitter, emit, arginfo_HttpMessageEmitter_emit, ZEND_ACC_PUBLIC)
+    PHP_FE_END
+};
+
+PHP_MINIT_FUNCTION(http_message_emitter)
+{
+    zend_class_entry ce;
+
+    INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "Emitter", methods);
+    HttpMessage_Emitter_ce = zend_register_internal_class(&ce);
+
+    return SUCCESS;
+}
+
+#endif
diff --git a/factory.c b/factory.c
new file mode 100644
index 0000000..003e063
--- /dev/null
+++ b/factory.c
@@ -0,0 +1,273 @@
+/*
+  +----------------------------------------------------------------------+
+  | HTTP Message PHP extension - Factory methods                         |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 2019 Arnold Daniels                                    |
+  +----------------------------------------------------------------------+
+  | Permission is hereby granted, free of charge, to any person          |
+  | obtaining a copy of this software and associated documentation files |
+  | (the "Software"), to deal in the Software without restriction,       |
+  | including without limitation the rights to use, copy, modify, merge, |
+  | publish, distribute, sublicense, and/or sell copies of the Software, |
+  | and to permit persons to whom the Software is furnished to do so,    |
+  | subject to the following conditions:                                 |
+  |                                                                      |
+  | The above copyright notice and this permission notice shall be       |
+  | included in all copies or substantial portions of the Software.      |
+  |                                                                      |
+  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,      |
+  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF   |
+  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                |
+  | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS  |
+  | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN   |
+  | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN    |
+  | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE     |
+  | SOFTWARE.                                                            |
+  +----------------------------------------------------------------------+
+  | Author: Arnold Daniels <arnold@jasny.net>                            |
+  +----------------------------------------------------------------------+
+*/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "php.h"
+#include "php_http_message.h"
+#include "macros.h"
+#include "response.h"
+#include "stream.h"
+#include "uploaded_file.h"
+#include "zend_exceptions.h"
+#include "zend_interfaces.h"
+#include "zend_string.h"
+#include "ext/psr/psr_http_factory.h"
+#include "ext/spl/spl_exceptions.h"
+
+#if HAVE_HTTP_MESSAGE
+
+zend_class_entry *HttpMessage_Factory_ce = NULL;
+
+int uri_param_as_object(zval *uri)
+{
+    zval uri_str;
+    zend_class_entry *uri_interface = HTTP_MESSAGE_PSR_INTERFACE("uri");
+
+    if (uri_interface == NULL) {
+        zend_throw_error(NULL, "Psr\\Http\\Message\\UriInterface not found");
+        return FAILURE;
+    }
+
+    if (Z_TYPE_P(uri) == IS_STRING) {
+        ZVAL_COPY(&uri_str, uri);
+        NEW_OBJECT_CONSTRUCT(uri, HttpMessage_Uri_ce, 1, &uri_str);
+    } else if (UNEXPECTED(Z_TYPE_P(uri) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(uri), uri_interface))) {
+        custom_parameter_type_error(1, "a string or object that implements Psr\\Http\\Message\\UriInterface", uri);
+        return FAILURE;
+    }
+
+    return SUCCESS;
+}
+
+PHP_METHOD(Factory, createRequest)
+{
+    zend_string *method = NULL;
+    zval *uri = NULL;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2)
+        Z_PARAM_STR(method)
+        Z_PARAM_ZVAL(uri)
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (uri_param_as_object(uri) == FAILURE) return;
+
+    NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_Request_ce, 0);
+    zend_update_property_str(HttpMessage_Request_ce, PROPERTY_ARG(return_value), ZEND_STRL("method"), method);
+    zend_update_property(HttpMessage_Request_ce, PROPERTY_ARG(return_value), ZEND_STRL("uri"), uri);
+}
+
+PHP_METHOD(Factory, createResponse)
+{
+    zend_long code = 200;
+    zend_bool code_is_null = 0;
+    zend_string *phrase = NULL;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 2)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_LONG_EX(code, code_is_null, 0, 0)
+        Z_PARAM_STR(phrase)
+    ZEND_PARSE_PARAMETERS_END();
+
+    NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_Response_ce, 0);
+    response_set_status(return_value, !code_is_null ? code : 200, phrase);
+}
+
+PHP_METHOD(Factory, createServerRequest)
+{
+    zend_string *method = NULL;
+    zval *uri = NULL, *serverParams = NULL;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 3)
+        Z_PARAM_STR(method)
+        Z_PARAM_ZVAL(uri)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_ARRAY_EX(serverParams, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (uri_param_as_object(uri) == FAILURE) return;
+
+    if (serverParams == NULL) {
+        NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_ServerRequest_ce, 0);
+    } else {
+        NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_ServerRequest_ce, 1, serverParams);
+    }
+
+    zend_update_property_str(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("method"), method);
+    zend_update_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("uri"), uri);
+}
+
+PHP_METHOD(Factory, createStream)
+{
+    php_stream *stream;
+    zval resource;
+    zend_string *content = NULL;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_STR(content)
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (open_temp_stream(&resource) == FAILURE) return;
+
+    if (content != NULL && ZSTR_LEN(content) > 0) {
+        php_stream_from_zval(stream, &resource);
+        php_stream_write(stream, ZSTR_VAL(content), ZSTR_LEN(content));
+    }
+
+    NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_Stream_ce, 1, &resource);
+}
+
+PHP_METHOD(Factory, createStreamFromFile)
+{
+    char *file = NULL, *mode = NULL;
+    size_t file_len = 0, mode_len = 0;
+    php_stream *stream;
+    zval resource;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 2)
+        Z_PARAM_STRING(file, file_len)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_STRING(mode, mode_len)
+    ZEND_PARSE_PARAMETERS_END();
+
+    file[file_len] = '\0';
+    stream = php_stream_open_wrapper(file, mode != NULL ? mode : "r", 0, NULL);
+
+    if (stream == NULL) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Failed to open '%s' stream", file);
+        return;
+    }
+
+    php_stream_to_zval(stream, &resource);
+    NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_Stream_ce, 1, &resource);
+}
+
+PHP_METHOD(Factory, createStreamFromResource)
+{
+    zval *resource = NULL;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
+        Z_PARAM_ZVAL(resource)
+    ZEND_PARSE_PARAMETERS_END();
+
+    NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_Stream_ce, 1, resource);
+}
+
+PHP_METHOD(Factory, createUploadedFile)
+{
+    zval *stream = NULL;
+    zend_long size = -1, error = 0;
+    zend_string *clientFilename = NULL, *clientMediaType = NULL;
+    zend_bool size_is_null = 1;
+    zend_class_entry *stream_interface = HTTP_MESSAGE_PSR_INTERFACE("stream");
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 5)
+        Z_PARAM_OBJECT_OF_CLASS(stream, stream_interface)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_LONG_EX(size, size_is_null, 1, 0)
+        Z_PARAM_LONG(error)
+        Z_PARAM_STR_EX(clientFilename, 1, 0)
+        Z_PARAM_STR_EX(clientMediaType, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
+
+    NEW_OBJECT(return_value, HttpMessage_UploadedFile_ce);
+    construct_uploaded_file(return_value, stream, NULL, size_is_null ? -1 : size, error, clientFilename,
+            clientMediaType, -1);
+}
+
+PHP_METHOD(Factory, createUri)
+{
+    zend_string *uri = NULL;
+    zval zuri;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_STR_EX(uri, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (uri == NULL) {
+        NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_Uri_ce, 0);
+    } else {
+        ZVAL_STR(&zuri, uri);
+        NEW_OBJECT_CONSTRUCT(return_value, HttpMessage_Uri_ce, 1, &zuri);
+    }
+}
+
+/* Define HttpMessage\Factory class */
+
+static const zend_function_entry methods[] = {
+    HTTP_MESSAGE_ME_EX(Factory, RequestFactory, createRequest)
+    HTTP_MESSAGE_ME_EX(Factory, ResponseFactory, createResponse)
+    HTTP_MESSAGE_ME_EX(Factory, ServerRequestFactory, createServerRequest)
+    HTTP_MESSAGE_ME_EX(Factory, StreamFactory, createStream)
+    HTTP_MESSAGE_ME_EX(Factory, StreamFactory, createStreamFromFile)
+    HTTP_MESSAGE_ME_EX(Factory, StreamFactory, createStreamFromResource)
+    HTTP_MESSAGE_ME_EX(Factory, UploadedFileFactory, createUploadedFile)
+    HTTP_MESSAGE_ME_EX(Factory, UriFactory, createUri)
+    PHP_FE_END
+};
+
+PHP_MINIT_FUNCTION(http_message_factory)
+{
+    zend_class_entry ce;
+    zend_class_entry *request_factory = HTTP_MESSAGE_PSR_INTERFACE("requestfactory");
+    zend_class_entry *response_factory = HTTP_MESSAGE_PSR_INTERFACE("responsefactory");
+    zend_class_entry *serverrequest_factory = HTTP_MESSAGE_PSR_INTERFACE("serverrequestfactory");
+    zend_class_entry *stream_factory = HTTP_MESSAGE_PSR_INTERFACE("streamfactory");
+    zend_class_entry *uploadedfile_factory = HTTP_MESSAGE_PSR_INTERFACE("uploadedfilefactory");
+    zend_class_entry *uri_factory = HTTP_MESSAGE_PSR_INTERFACE("urifactory");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(request_factory, "Factory", "RequestFactory");
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(response_factory, "Factory", "ResponseFactory");
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(serverrequest_factory, "Factory", "ServerRequestFactory");
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(stream_factory, "Factory", "StreamFactory");
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(uploadedfile_factory, "Factory", "UploadedFileFactory");
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(uri_factory, "Factory", "UriFactory");
+
+    if (UNEXPECTED(
+            HttpMessage_Request_ce == NULL || HttpMessage_Response_ce == NULL || HttpMessage_ServerRequest_ce == NULL ||
+            HttpMessage_Stream_ce == NULL || HttpMessage_UploadedFile_ce == NULL || HttpMessage_Uri_ce == NULL
+    )) {
+        return FAILURE;
+    }
+
+    INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "Factory", methods);
+
+    HttpMessage_Factory_ce = zend_register_internal_class(&ce);
+    zend_class_implements(HttpMessage_Factory_ce, 6, request_factory, response_factory, serverrequest_factory,
+            stream_factory, uploadedfile_factory, uri_factory);
+
+    return SUCCESS;
+}
+
+#endif
diff --git a/http_message.c b/http_message.c
index c8463d7..76ec02c 100644
--- a/http_message.c
+++ b/http_message.c
@@ -40,8 +40,31 @@
 
 #if HAVE_HTTP_MESSAGE
 
+static const zend_module_dep deps[] = {
+    ZEND_MOD_REQUIRED("psr")
+    {NULL, NULL, NULL}
+};
+
+PHP_MINIT_FUNCTION(http_message)
+{
+    int success =
+        PHP_MINIT(http_message_message)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_request)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_serverrequest)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_response)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_stream)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_uri)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_uploadedfile)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_factory)(INIT_FUNC_ARGS_PASSTHRU) +
+        PHP_MINIT(http_message_emitter)(INIT_FUNC_ARGS_PASSTHRU);
+
+    return ZEND_NORMALIZE_BOOL(success);
+}
+
 zend_module_entry http_message_module_entry = {
-    STANDARD_MODULE_HEADER,
+    STANDARD_MODULE_HEADER_EX,
+    NULL,
+    deps,
     PHP_HTTP_MESSAGE_EXTNAME,
     NULL,
     PHP_MINIT(http_message),
@@ -53,18 +76,6 @@ zend_module_entry http_message_module_entry = {
     STANDARD_MODULE_PROPERTIES
 };
 
-PHP_MINIT_FUNCTION(http_message)
-{
-    PHP_MINIT(http_message_message)(INIT_FUNC_ARGS_PASSTHRU);
-    PHP_MINIT(http_message_request)(INIT_FUNC_ARGS_PASSTHRU);
-    PHP_MINIT(http_message_serverrequest)(INIT_FUNC_ARGS_PASSTHRU);
-    PHP_MINIT(http_message_response)(INIT_FUNC_ARGS_PASSTHRU);
-    PHP_MINIT(http_message_stream)(INIT_FUNC_ARGS_PASSTHRU);
-    PHP_MINIT(http_message_uri)(INIT_FUNC_ARGS_PASSTHRU);
-
-    return SUCCESS;
-}
-
 #ifdef COMPILE_DL_HTTP_MESSAGE
 ZEND_GET_MODULE(http_message)
 #endif
diff --git a/macros.h b/macros.h
index 43ade64..4be105b 100644
--- a/macros.h
+++ b/macros.h
@@ -1,6 +1,7 @@
 /*
   +----------------------------------------------------------------------+
   | HTTP Message PHP extension                                           |
+  | Generic macros for this library                                      |
   +----------------------------------------------------------------------+
   | Copyright (c) 2019 Arnold Daniels                                    |
   +----------------------------------------------------------------------+
@@ -32,17 +33,120 @@
 #ifndef HTTP_MESSAGE_MACROS_H
 #define HTTP_MESSAGE_MACROS_H
 
+#define EXPAND( x ) x
+
+#if PHP_VERSION_ID < 80000
+#define PROPERTY_ARG(zv) (zv)
+#else
+#define PROPERTY_ARG(zv) Z_OBJ_P(zv)
+#endif
+
 ZEND_BEGIN_ARG_INFO_EX(arginfo_none, 0, 0, 0)
 ZEND_END_ARG_INFO()
 
 #define INIT_ARRAY_PROPERTY(className, property, rv) \
-        array_init(zend_read_property(className, getThis(), ZEND_STRL(property), 0, &rv));
+        array_init(zend_read_property(className, PROPERTY_ARG(getThis()), ZEND_STRL(property), 0, &rv))
+
+#define SET_ARRAY_PROPERTY(className, property, zval, rv) \
+        if (zval == NULL) array_init(zend_read_property(className, PROPERTY_ARG(getThis()), ZEND_STRL(property), 0, &rv)); \
+        else zend_update_property(className, PROPERTY_ARG(getThis()), ZEND_STRL(property), zval)
+
+#define SET_STRING_PROPERTY(className, property, val) \
+        if (val != NULL) zend_update_property_stringl(className, PROPERTY_ARG(getThis()), ZEND_STRL(property), val, strlen(val))
 
-#define SET_STRING_PROPERTY(className, property, value) \
-        zend_update_property_stringl(className, getThis(), ZEND_STRL(property), value ?: "", value ? strlen(value) : 0);
+#define SET_STR_PROPERTY(className, property, val) \
+        if (val != NULL) zend_update_property_str(className, PROPERTY_ARG(getThis()), ZEND_STRL(property), val)
+
+#if PHP_VERSION_ID < 70300
+#define SET_URI_PROPERTY(className, property, val) SET_STRING_PROPERTY(className, property, val)
+#else
+#define SET_URI_PROPERTY(className, property, val) SET_STR_PROPERTY(className, property, val)
+#endif
+
+#define HTTP_MESSAGE_ME_EX(className, interfaceName, method) \
+        PHP_ME(className, method, arginfo_PsrHttpMessage ## interfaceName ## Interface_ ## method, ZEND_ACC_PUBLIC)
 
 #define HTTP_MESSAGE_ME(className, method) \
-        PHP_ME(className, method, arginfo_PsrHttpMessage ## className ## Interface_ ## method, ZEND_ACC_PUBLIC)
+        HTTP_MESSAGE_ME_EX(className, className, method)
+
+#define HTTP_MESSAGE_PSR_INTERFACE(interfaceName) \
+        get_internal_ce(ZEND_STRL("psr\\http\\message\\" interfaceName "interface"))
+
+#define NEW_OBJECT(zval, ce) \
+        object_init_ex(zval, ce); \
+        if (EXPECTED(zval != NULL)) object_properties_init(Z_OBJ_P(zval), ce)
+
+#define NEW_OBJECT_CONSTRUCT(zval, ce, argc, ...) \
+        NEW_OBJECT(zval, ce); \
+        if (EXPECTED(zval != NULL)) \
+            EXPAND(zend_call_method_with_## argc ##_params(PROPERTY_ARG(zval), ce, &ce->constructor, "__construct", NULL, ## __VA_ARGS__))
+
+#define IS_STREAM_RESOURCE(zstream) \
+        ( \
+            Z_TYPE_P(zstream) == IS_RESOURCE && \
+            (Z_RES_P(zstream)->type == php_file_le_stream() || Z_RES_P(zstream)->type == php_file_le_pstream()) \
+        )
+
+#define ARRAY_ADD(arr, index, key) \
+        key == NULL ? zend_hash_index_add_empty_element(arr, index) : zend_hash_add_empty_element(arr, key)
+
+#define ARRAY_GET(arr, index, key) \
+        arr != NULL ? (key == NULL ? zend_hash_index_find(arr, index) : zend_hash_find(arr, key)) : NULL
+
+#define COPY_PROPERTY_FROM_ARRAY(arr, key, object, className, property, type, val) \
+        val = zend_hash_str_find(arr, ZEND_STRL(key)); \
+        if (val != NULL && EXPECTED(Z_TYPE_P(val) == type)) { \
+            zend_update_property(className, PROPERTY_ARG(object), ZEND_STRL(property), val); \
+        }
+
+#define Z_ARR_P_NULL(zval) zval != NULL ? Z_ARR_P(zval) : NULL
+
+#define Z_STRCMP(zval, match, def) \
+        (zval == NULL || Z_TYPE_P(zval) != IS_STRING || Z_STRLEN_P(zval) == 0 ? def : \
+        strncmp(match, Z_STRVAL_P(zval), Z_STRLEN_P(zval)))
+
+#define Z_STRVAL_P_NULL(zval) zval != NULL ? Z_STRVAL_P(zval) : NULL
+#define Z_STRLEN_P_NULL(zval) zval != NULL ? Z_STRLEN_P(zval) : 0
+
+#define ZSTR_VAL_LEN(zstr) zstr != NULL ? ZSTR_VAL(zstr) : NULL, zstr != NULL ? ZSTR_LEN(zstr) : 0
+
+#define STRLEN_NULL(str) str != NULL ? strlen(str) : 0
+
+#define STR_CLOSE(str, len) str[len] = '\0'  // Is buffer big enough? yolo!
+
+static zend_always_inline zend_class_entry* get_internal_ce(const char *class_name, size_t class_name_len)
+{
+    zend_class_entry* temp_ce;
+
+    if ((temp_ce = zend_hash_str_find_ptr(CG(class_table), class_name, class_name_len)) == NULL) {
+        return NULL;
+    }
+
+    return temp_ce;
+}
+
+static zend_always_inline void custom_parameter_type_error(int num, char *expected, zval *arg)
+{
+    const char *space;
+    const char *class_name;
+
+    if (EG(exception)) {
+        return;
+    }
+    class_name = get_active_class_name(&space);
+    zend_type_error("%s%s%s() expects parameter %d to be %s, %s given",
+                    class_name, space, get_active_function_name(), num, expected, zend_zval_type_name(arg));
+}
+
+#define ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(ce, className, psrClassName) \
+    if (UNEXPECTED(ce == NULL)) { \
+        zend_error(E_CORE_WARNING, \
+                "Failed to initialize 'HttpMessage\\%s': 'Psr\\Http\\Message\\%sInterace' not found", \
+                className, psrClassName); \
+        return FAILURE; \
+    }
 
+#define ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(ce, className) \
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND_EX(ce, className, className)
 
 #endif //HTTP_MESSAGE_MACROS_H
diff --git a/message.c b/message.c
index dd40cfa..5d4d38a 100644
--- a/message.c
+++ b/message.c
@@ -33,20 +33,43 @@
 #endif
 
 #include "php.h"
-#include "php_ini.h"
 #include "php_http_message.h"
 #include "macros.h"
 #include "zend_exceptions.h"
 #include "zend_interfaces.h"
 #include "zend_string.h"
-#include "ext/standard/info.h"
 #include "ext/standard/php_string.h"
 #include "ext/psr/psr_http_message.h"
 
 #if HAVE_HTTP_MESSAGE
 
-zend_class_entry *HttpMessage_Message_ce;
+zend_class_entry *HttpMessage_Message_ce = NULL;
 
+void add_header(zval *object, zend_string *name, zend_string *value, zend_bool added)
+{
+    zval rv, *headers_prop, *header_values;
+    HashTable *headers;
+
+    headers_prop = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(object), ZEND_STRL("headers"), 0, &rv);
+
+    if (UNEXPECTED(Z_TYPE_P(headers_prop) != IS_ARRAY)) {
+        return; // Shouldn't happen
+    }
+
+    headers = zend_array_dup(Z_ARR_P(headers_prop));
+
+    header_values = zend_hash_find(headers, name);
+    if (header_values == NULL) {
+        header_values = zend_hash_add_empty_element(headers, name);
+        array_init(header_values);
+    } else if (!added) {
+        ZVAL_DEREF(header_values);
+        array_init(header_values);
+    }
+    add_next_index_str(header_values, zend_string_copy(value));
+
+    ZVAL_ARR(headers_prop, headers);
+}
 
 /* __construct */
 
@@ -55,13 +78,8 @@ PHP_METHOD(Message, __construct)
     zval rv, *body;
 
     /* $this->body = new Stream() */
-    body = zend_read_property(HttpMessage_Request_ce, getThis(), ZEND_STRL("body"), 0, &rv);
-    object_init_ex(body, HttpMessage_Stream_ce);
-    if (body != NULL && Z_TYPE_P(body) == IS_OBJECT) { /* Should always be true */
-        zend_call_method_with_0_params(
-                body, HttpMessage_Stream_ce, &HttpMessage_Stream_ce->constructor, "__construct", NULL
-        );
-    }
+    body = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(getThis()), ZEND_STRL("body"), 0, &rv);
+    NEW_OBJECT_CONSTRUCT(body, HttpMessage_Stream_ce, 0);
 
     INIT_ARRAY_PROPERTY(HttpMessage_Message_ce, "headers", rv);
 }
@@ -73,25 +91,22 @@ PHP_METHOD(Message, getProtocolVersion)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("protocolVersion"), 0, &rv);
+    value = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(getThis()), ZEND_STRL("protocolVersion"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Message, withProtocolVersion)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(
-            HttpMessage_Message_ce, return_value, ZEND_STRL("protocolVersion"), value, value_len
-    );
+    zend_update_property_str(HttpMessage_Message_ce, PROPERTY_ARG(return_value), ZEND_STRL("protocolVersion"), value);
 }
 
 
@@ -101,7 +116,7 @@ PHP_METHOD(Message, getHeaders)
 {
     zval rv, *headers;
 
-    headers = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("headers"), 0, &rv);
+    headers = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(getThis()), ZEND_STRL("headers"), 0, &rv);
 
     RETURN_ZVAL(headers, 1, 0);
 }
@@ -109,17 +124,15 @@ PHP_METHOD(Message, getHeaders)
 PHP_METHOD(Message, hasHeader)
 {
     zval rv, *headers;
-    char *name;
-    size_t name_len;
+    zend_string *name;
     zend_bool exists;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(name, name_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
-
-    headers = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("headers"), 0, &rv);
+        Z_PARAM_STR(name)
+    ZEND_PARSE_PARAMETERS_END();
 
-    exists = zend_hash_str_exists(Z_ARRVAL_P(headers), name, name_len);
+    headers = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(getThis()), ZEND_STRL("headers"), 0, &rv);
+    exists = zend_hash_exists(Z_ARRVAL_P(headers), name);
 
     RETVAL_BOOL(exists);
 }
@@ -127,18 +140,18 @@ PHP_METHOD(Message, hasHeader)
 PHP_METHOD(Message, getHeader)
 {
     zval rv, *headers, *header_values;
-    char *name;
-    size_t name_len;
+    zend_string *name;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(name, name_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(name)
+    ZEND_PARSE_PARAMETERS_END();
 
-    headers = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("headers"), 0, &rv);
+    headers = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(getThis()), ZEND_STRL("headers"), 0, &rv);
+    header_values = zend_hash_find(Z_ARRVAL_P(headers), name);
 
-    header_values = zend_hash_str_find(Z_ARRVAL_P(headers), name, name_len);
-    if (header_values != NULL) {
-        array_init(header_values);
+    if (header_values == NULL) {
+        array_init(return_value);
+        return;
     }
 
     RETURN_ZVAL(header_values, 1, 0);
@@ -146,94 +159,75 @@ PHP_METHOD(Message, getHeader)
 
 PHP_METHOD(Message, getHeaderLine)
 {
-    zval rv, *headers, *header_values, *header_string;
-    char *name;
-    size_t name_len;
-    zend_string *glue;
+    zval rv, *headers, *header_values;
+    zend_string *name, *glue;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(name, name_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(name)
+    ZEND_PARSE_PARAMETERS_END();
 
-    headers = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("headers"), 0, &rv);
-    header_values = zend_hash_str_find(Z_ARRVAL_P(headers), name, name_len);
+    headers = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(getThis()), ZEND_STRL("headers"), 0, &rv);
+    header_values = zend_hash_find(Z_ARRVAL_P(headers), name);
 
-    glue = zend_string_init(ZEND_STRL(", "), 0);
-    php_implode(glue, header_values, header_string);
+    if (header_values == NULL) {
+        RETURN_EMPTY_STRING();
+    }
 
+    glue = zend_string_init(ZEND_STRL(", "), 0);
+#if PHP_VERSION_ID < 80000
+    php_implode(glue, header_values, return_value);
+#else
+    php_implode(glue, Z_ARRVAL_P(header_values), return_value);
+#endif
     zend_string_free(glue);
-
-    RETURN_ZVAL(header_string, 0, 0)
 }
 
 PHP_METHOD(Message, withHeader)
 {
-    zval rv, *headers, new_headers, header_values;
-    char *name;
-    size_t name_len;
-    zend_string *value;
+    zend_string *name = NULL, *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2)
-        Z_PARAM_STRING(name, name_len)
+        Z_PARAM_STR(name)
         Z_PARAM_STR(value)
-    ZEND_PARSE_PARAMETERS_END_EX();
-
-    headers = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("headers"), 0, &rv);
-    ZVAL_COPY(&new_headers, headers);
+    ZEND_PARSE_PARAMETERS_END();
 
-    array_init(&header_values);
-    add_next_index_str(&header_values, value);
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    add_assoc_zval_ex(&new_headers, name, name_len, &header_values);
-
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
-
-    zend_update_property(HttpMessage_Message_ce, return_value, ZEND_STRL("headers"), &new_headers);
+    add_header(return_value, name, value, 0);
 }
 
 PHP_METHOD(Message, withAddedHeader)
 {
-    zval rv, *headers, *header_values;
-    char *name;
-    size_t name_len;
-    zend_string *value;
+    zend_string *name = NULL, *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2)
-        Z_PARAM_STRING(name, name_len)
+        Z_PARAM_STR(name)
         Z_PARAM_STR(value)
-    ZEND_PARSE_PARAMETERS_END_EX();
-
-    headers = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("headers"), 0, &rv);
+    ZEND_PARSE_PARAMETERS_END();
 
-    header_values = zend_hash_str_find(Z_ARRVAL_P(headers), name, name_len);
-    if (header_values == NULL) {
-        array_init(header_values);
-    }
-    add_next_index_str(header_values, value);
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    add_assoc_zval_ex(headers, name, name_len, header_values);
-
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
-
-    zend_update_property(HttpMessage_Message_ce, return_value, ZEND_STRL("headers"), headers);
+    add_header(return_value, name, value, 1);
 }
 
 PHP_METHOD(Message, withoutHeader)
 {
-    zval rv, *headers;
-    char *name;
-    size_t name_len;
+    zval rv, *headers_prop;
+    HashTable *headers;
+    zend_string *name = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(name, name_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(name)
+    ZEND_PARSE_PARAMETERS_END();
 
-    headers = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("headers"), 0, &rv);
-    zend_hash_str_del(Z_ARRVAL_P(headers), name, name_len);
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    headers_prop = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(return_value), ZEND_STRL("headers"), 0, &rv);
 
-    zend_update_property(HttpMessage_Message_ce, return_value, ZEND_STRL("headers"), headers);
+    headers = zend_array_dup(Z_ARR_P(headers_prop));
+    zend_hash_del(headers, name);
+
+    ZVAL_ARR(headers_prop, headers);
 }
 
 
@@ -243,7 +237,7 @@ PHP_METHOD(Message, getBody)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("body"), 0, &rv);
+    value = zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(getThis()), ZEND_STRL("body"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
@@ -251,14 +245,20 @@ PHP_METHOD(Message, getBody)
 PHP_METHOD(Message, withBody)
 {
     zval *value;
+    zend_class_entry *stream_interface = HTTP_MESSAGE_PSR_INTERFACE("stream");
+
+    if (stream_interface == NULL) {
+        zend_throw_error(NULL, "Psr\\Http\\Message\\StreamInterface not found");
+        return;
+    }
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_OBJECT_OF_CLASS(value, PsrHttpMessageStreamInterface_ce_ptr)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_OBJECT_OF_CLASS(value, stream_interface)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property(HttpMessage_Message_ce, return_value, ZEND_STRL("body"), value);
+    zend_update_property(HttpMessage_Message_ce, PROPERTY_ARG(return_value), ZEND_STRL("body"), value);
 }
 
 
@@ -283,18 +283,22 @@ static const zend_function_entry message_functions[] = {
 PHP_MINIT_FUNCTION(http_message_message)
 {
     zend_class_entry ce;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("message");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(interface, "Message");
+
     INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "Message", message_functions);
 
     HttpMessage_Message_ce = zend_register_internal_class(&ce);
     HttpMessage_Message_ce->ce_flags |= ZEND_ACC_EXPLICIT_ABSTRACT_CLASS;
-    zend_class_implements(HttpMessage_Message_ce, 1, PsrHttpMessageMessageInterface_ce_ptr);
+    zend_class_implements(HttpMessage_Message_ce, 1, interface);
 
     /* Properties */
     zend_declare_property_string(
-            HttpMessage_Message_ce, ZEND_STRL("protocolVersion"), "1.1", ZEND_ACC_PROTECTED
+            HttpMessage_Message_ce, ZEND_STRL("protocolVersion"), "1.1", ZEND_ACC_PRIVATE
     );
-    zend_declare_property_null(HttpMessage_Message_ce, ZEND_STRL("headers"), ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_Message_ce, ZEND_STRL("body"), ZEND_ACC_PROTECTED);
+    zend_declare_property_null(HttpMessage_Message_ce, ZEND_STRL("headers"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_Message_ce, ZEND_STRL("body"), ZEND_ACC_PRIVATE);
 
     return SUCCESS;
 }
diff --git a/package.xml b/package.xml
new file mode 100644
index 0000000..b6c89f2
--- /dev/null
+++ b/package.xml
@@ -0,0 +1,394 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
+ <name>http_message</name>
+ <channel>pecl.php.net</channel>
+ <summary>PSR-7 HTTP Message implementation</summary>
+ <description>PSR-7 compatible HTTP Message implementation as PHP extension</description>
+ <lead>
+  <name>Arnold Daniels</name>
+  <user>jasny</user>
+  <email>jasny@php.net</email>
+  <active>yes</active>
+ </lead>
+ <date>2020-12-27</date>
+ <version>
+  <release>1.0.0</release>
+  <api>1.0.0</api>
+ </version>
+ <stability>
+  <release>stable</release>
+  <api>stable</api>
+ </stability>
+ <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+ <notes>
+Release as v1.0.0
+ </notes>
+ <contents>
+  <dir name="/">
+   <file name="config.m4" role="src"/>
+   <file name="config.w32" role="src"/>
+   <file name="CREDITS" role="doc"/>
+   <file name="emitter.c" role="src"/>
+   <file name="factory.c" role="src"/>
+   <file name="http_message.c" role="src"/>
+   <file name="LICENSE" role="doc"/>
+   <file name="macros.h" role="src"/>
+   <file name="Makefile.frag" role="src"/>
+   <file name="message.c" role="src"/>
+   <file name="php_http_message.h" role="src"/>
+   <file name="README.md" role="doc"/>
+   <file name="request.c" role="src"/>
+   <file name="response.c" role="src"/>
+   <file name="response.h" role="src"/>
+   <file name="server_request.c" role="src"/>
+   <file name="stream.c" role="src"/>
+   <file name="stream.h" role="src"/>
+   <file name="uploaded_file.c" role="src"/>
+   <file name="uploaded_file.h" role="src"/>
+   <file name="uri.c" role="src"/>
+   <dir name="autoconf">
+    <file name="pecl.m4" role="src"/>
+    <file name="php-executable.m4" role="src"/>
+   </dir>
+   <dir name="tests">
+    <dir name="Emitter">
+     <file name="emit_001.phpt" role="test"/>
+     <file name="emit_002.phpt" role="test"/>
+     <file name="emit_003.phpt" role="test"/>
+     <file name="emit_004.phpt" role="test"/>
+     <file name="emit_005.phpt" role="test"/>
+     <file name="emit_006.phpt" role="test"/>
+     <file name="emit_007.phpt" role="test"/>
+    </dir>
+    <dir name="Factory">
+     <file name="createRequest_001.phpt" role="test"/>
+     <file name="createRequest_002.phpt" role="test"/>
+     <file name="createResponse_001.phpt" role="test"/>
+     <file name="createResponse_002.phpt" role="test"/>
+     <file name="createResponse_003.phpt" role="test"/>
+     <file name="createResponse_004.phpt" role="test"/>
+     <file name="createResponse_err01.phpt" role="test"/>
+     <file name="createResponse_err02.phpt" role="test"/>
+     <file name="createServerRequest_001.phpt" role="test"/>
+     <file name="createServerRequest_002.phpt" role="test"/>
+     <file name="createServerRequest_003.phpt" role="test"/>
+     <file name="createServerRequest_err01.phpt" role="test"/>
+     <file name="createStream_001.phpt" role="test"/>
+     <file name="createStream_002.phpt" role="test"/>
+     <file name="createStream_err01.phpt" role="test"/>
+     <file name="createStreamFromFile_001.phpt" role="test"/>
+     <file name="createStreamFromFile_002.phpt" role="test"/>
+     <file name="createStreamFromFile_003.phpt" role="test"/>
+     <file name="createStreamFromFile_err01.phpt" role="test"/>
+     <file name="createStreamFromFile_err02.phpt" role="test"/>
+     <file name="createStreamFromResource_001.phpt" role="test"/>
+     <file name="createStreamFromResource_err01.phpt" role="test"/>
+     <file name="createStreamFromResource_err02.phpt" role="test"/>
+     <file name="createUploadedFile_001.phpt" role="test"/>
+     <file name="createUploadedFile_002.phpt" role="test"/>
+     <file name="createUploadedFile_003.phpt" role="test"/>
+     <file name="createUploadedFile_err01.phpt" role="test"/>
+     <file name="createUploadedFile_err02.phpt" role="test"/>
+     <file name="createUri_001.phpt" role="test"/>
+     <file name="createUri_002.phpt" role="test"/>
+    </dir>
+    <dir name="Message">
+     <file name="body_001.phpt" role="test"/>
+     <file name="body_err01.phpt" role="test"/>
+     <file name="headers_001.phpt" role="test"/>
+     <file name="headers_002.phpt" role="test"/>
+     <file name="headers_003.phpt" role="test"/>
+     <file name="headers_err01.phpt" role="test"/>
+     <file name="headers_err02.phpt" role="test"/>
+     <file name="headers_err03.phpt" role="test"/>
+     <file name="headers_err04.phpt" role="test"/>
+     <file name="headers_err05.phpt" role="test"/>
+     <file name="protocolVersion_001.phpt" role="test"/>
+     <file name="protocolVersion_002.phpt" role="test"/>
+     <file name="protocolVersion_err01.phpt" role="test"/>
+    </dir>
+    <dir name="Request">
+     <file name="__construct_001.phpt" role="test"/>
+     <file name="method_001.phpt" role="test"/>
+     <file name="method_err01.phpt" role="test"/>
+     <file name="requestTarget_001.phpt" role="test"/>
+     <file name="requestTarget_002.phpt" role="test"/>
+     <file name="requestTarget_err01.phpt" role="test"/>
+     <file name="uri_001.phpt" role="test"/>
+     <file name="uri_err01.phpt" role="test"/>
+    </dir>
+    <dir name="Response">
+     <file name="__construct_001.phpt" role="test"/>
+     <file name="status_001.phpt" role="test"/>
+     <file name="status_002.phpt" role="test"/>
+     <file name="status_003.phpt" role="test"/>
+     <file name="status_004.phpt" role="test"/>
+     <file name="status_err01.phpt" role="test"/>
+     <file name="status_err02.phpt" role="test"/>
+    </dir>
+    <dir name="ServerRequest">
+     <file name="__construct_001.phpt" role="test"/>
+     <file name="__construct_002.phpt" role="test"/>
+     <file name="__construct_003.phpt" role="test"/>
+     <file name="__construct_004.phpt" role="test"/>
+     <file name="__construct_005.phpt" role="test"/>
+     <file name="__construct_err01.phpt" role="test"/>
+     <file name="attributes_001.phpt" role="test"/>
+     <file name="attributes_002.phpt" role="test"/>
+     <file name="attributes_err01.phpt" role="test"/>
+     <file name="cookieParams_001.phpt" role="test"/>
+     <file name="cookieParams_err01.phpt" role="test"/>
+     <file name="parsedBody_001.phpt" role="test"/>
+     <file name="parsedBody_err01.phpt" role="test"/>
+     <file name="queryParams_001.phpt" role="test"/>
+     <file name="queryParams_err01.phpt" role="test"/>
+    </dir>
+    <dir name="Stream">
+     <file name="__construct_001.phpt" role="test"/>
+     <file name="__construct_002.phpt" role="test"/>
+     <file name="__construct_003.phpt" role="test"/>
+     <file name="__construct_004.phpt" role="test"/>
+     <file name="__construct_err01.phpt" role="test"/>
+     <file name="__construct_err02.phpt" role="test"/>
+     <file name="__construct_err03.phpt" role="test"/>
+     <file name="__toString_001.phpt" role="test"/>
+     <file name="__toString_002.phpt" role="test"/>
+     <file name="__toString_003.phpt" role="test"/>
+     <file name="close_001.phpt" role="test"/>
+     <file name="close_002.phpt" role="test"/>
+     <file name="close_003.phpt" role="test"/>
+     <file name="detach_001.phpt" role="test"/>
+     <file name="detach_002.phpt" role="test"/>
+     <file name="detach_003.phpt" role="test"/>
+     <file name="eof_001.phpt" role="test"/>
+     <file name="eof_002.phpt" role="test"/>
+     <file name="eof_003.phpt" role="test"/>
+     <file name="getContents_001.phpt" role="test"/>
+     <file name="getContents_002.phpt" role="test"/>
+     <file name="getContents_err01.phpt" role="test"/>
+     <file name="getMetadata_001.phpt" role="test"/>
+     <file name="getMetadata_002.phpt" role="test"/>
+     <file name="getMetadata_003.phpt" role="test"/>
+     <file name="getSize_001.phpt" role="test"/>
+     <file name="isReadable_001.phpt" role="test"/>
+     <file name="isReadable_002.phpt" role="test"/>
+     <file name="isSeekable_001.phpt" role="test"/>
+     <file name="isSeekable_002.phpt" role="test"/>
+     <file name="isWritable_001.phpt" role="test"/>
+     <file name="isWritable_002.phpt" role="test"/>
+     <file name="read_001.phpt" role="test"/>
+     <file name="read_err01.phpt" role="test"/>
+     <file name="read_err02.phpt" role="test"/>
+     <file name="read_err03.phpt" role="test"/>
+     <file name="rewind_001.phpt" role="test"/>
+     <file name="seek_001.phpt" role="test"/>
+     <file name="seek_err01.phpt" role="test"/>
+     <file name="seek_err02.phpt" role="test"/>
+     <file name="seek_err03.phpt" role="test"/>
+     <file name="seek_err04.phpt" role="test"/>
+     <file name="tell_001.phpt" role="test"/>
+     <file name="write_001.phpt" role="test"/>
+     <file name="write_err01.phpt" role="test"/>
+     <file name="write_err02.phpt" role="test"/>
+     <file name="write_err03.phpt" role="test"/>
+    </dir>
+    <dir name="UploadedFile">
+     <file name="__construct_001.phpt" role="test"/>
+     <file name="__construct_002.phpt" role="test"/>
+     <file name="__construct_003.phpt" role="test"/>
+     <file name="__construct_004.phpt" role="test"/>
+     <file name="__construct_005.phpt" role="test"/>
+     <file name="__construct_err01.phpt" role="test"/>
+     <file name="__construct_err02.phpt" role="test"/>
+     <file name="getStream_001.phpt" role="test"/>
+     <file name="getStream_002.phpt" role="test"/>
+     <file name="getStream_err01.phpt" role="test"/>
+     <file name="getStream_err02.phpt" role="test"/>
+     <file name="getStream_err03.phpt" role="test"/>
+     <file name="moveTo_001.phpt" role="test"/>
+     <file name="moveTo_002.phpt" role="test"/>
+     <file name="moveTo_003.phpt" role="test"/>
+     <file name="moveTo_err01.phpt" role="test"/>
+     <file name="moveTo_err02.phpt" role="test"/>
+     <file name="moveTo_err03.phpt" role="test"/>
+     <file name="moveTo_err04.phpt" role="test"/>
+     <file name="moveTo_err05.phpt" role="test"/>
+    </dir>
+    <dir name="Uri">
+     <file name="__construct_001.phpt" role="test"/>
+     <file name="__construct_002.phpt" role="test"/>
+     <file name="__construct_003.phpt" role="test"/>
+     <file name="__construct_004.phpt" role="test"/>
+     <file name="__construct_005.phpt" role="test"/>
+     <file name="__construct_006.phpt" role="test"/>
+     <file name="__construct_007.phpt" role="test"/>
+     <file name="__construct_008.phpt" role="test"/>
+     <file name="__construct_err01.phpt" role="test"/>
+     <file name="__construct_err02.phpt" role="test"/>
+     <file name="__toString_001.phpt" role="test"/>
+     <file name="__toString_002.phpt" role="test"/>
+     <file name="__toString_003.phpt" role="test"/>
+     <file name="__toString_004.phpt" role="test"/>
+     <file name="__toString_005.phpt" role="test"/>
+     <file name="__toString_006.phpt" role="test"/>
+     <file name="__toString_007.phpt" role="test"/>
+     <file name="__toString_008.phpt" role="test"/>
+     <file name="__toString_009.phpt" role="test"/>
+     <file name="authority_001.phpt" role="test"/>
+     <file name="authority_002.phpt" role="test"/>
+     <file name="authority_003.phpt" role="test"/>
+     <file name="fragment_001.phpt" role="test"/>
+     <file name="fragment_002.phpt" role="test"/>
+     <file name="fragment_err01.phpt" role="test"/>
+     <file name="fragment_err02.phpt" role="test"/>
+     <file name="host_001.phpt" role="test"/>
+     <file name="host_002.phpt" role="test"/>
+     <file name="host_error.phpt" role="test"/>
+     <file name="path_001.phpt" role="test"/>
+     <file name="path_002.phpt" role="test"/>
+     <file name="path_err01.phpt" role="test"/>
+     <file name="path_err02.phpt" role="test"/>
+     <file name="port_001.phpt" role="test"/>
+     <file name="port_002.phpt" role="test"/>
+     <file name="port_err01.phpt" role="test"/>
+     <file name="port_err02.phpt" role="test"/>
+     <file name="query_001.phpt" role="test"/>
+     <file name="query_002.phpt" role="test"/>
+     <file name="query_err01.phpt" role="test"/>
+     <file name="query_err02.phpt" role="test"/>
+     <file name="scheme_001.phpt" role="test"/>
+     <file name="scheme_002.phpt" role="test"/>
+     <file name="scheme_err01.phpt" role="test"/>
+     <file name="scheme_err02.phpt" role="test"/>
+     <file name="userInfo_001.phpt" role="test"/>
+     <file name="userInfo_002.phpt" role="test"/>
+     <file name="userInfo_err01.phpt" role="test"/>
+     <file name="userInfo_err02.phpt" role="test"/>
+    </dir>
+   </dir>
+  </dir>
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>7.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.4.3</min>
+   </pearinstaller>
+   <extension>
+    <name>psr</name>
+    <min>0.6.0</min>
+   </extension>
+  </required>
+ </dependencies>
+ <providesextension>http_message</providesextension>
+ <extsrcrelease/>
+ <changelog>
+  <release>
+   <date>2020-12-27</date>
+   <time>00:57:18</time>
+   <version>
+    <release>1.0.0</release>
+    <api>1.0.0</api>
+   </version>
+   <stability>
+    <release>stable</release>
+    <api>stable</api>
+   </stability>
+   <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+   <notes>
+Release as v1.0.0
+   </notes>
+  </release>
+  <release>
+   <date>2020-08-25</date>
+   <time>11:20:40</time>
+   <version>
+    <release>0.2.2</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+   <notes>
+Fixed HttpMessage\Factory::createUri()
+   </notes>
+  </release>
+  <release>
+   <date>2019-11-07</date>
+   <time>01:45:11</time>
+   <version>
+    <release>0.2.1</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+   <notes>
+Added `HttpMessage\Emitter`.
+Emitter outputs header and body.
+XDebug is needed to test emitter.
+
+Fixed segfault for `withHeader()` when using a non-string var.
+   </notes>
+  </release>
+  <release>
+   <date>2019-09-05</date>
+   <time>01:05:20</time>
+   <version>
+    <release>0.2.0</release>
+    <api>0.2.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+   <notes>
+Added Factory class that implements all PSR-17 interfaces.
+All properties private.
+Allow filename and mode for Stream constructor.
+Allow StreamInterface object for UploadedFile constructor.
+Move uploaded file copies a stream if stream is supplied.
+Fixes and cleanup.
+   </notes>
+  </release>
+  <release>
+   <date>2019-07-28</date>
+   <time>14:50:00</time>
+   <version>
+    <release>0.1.1</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+   <notes>
+Fixed default value issue with ServerRequest::getAttribute(). The method only accepted one argument.
+   </notes>
+  </release>
+  <release>
+   <date>2019-07-10</date>
+   <time>12:00:00</time>
+   <version>
+    <release>0.1.0</release>
+    <api>0.1.0</api>
+   </version>
+   <stability>
+    <release>beta</release>
+    <api>beta</api>
+   </stability>
+   <license uri="https://opensource.org/licenses/MIT">MIT License</license>
+   <notes>
+Initial release.
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/php_http_message.h b/php_http_message.h
index 5846a2f..0e6ea6e 100644
--- a/php_http_message.h
+++ b/php_http_message.h
@@ -31,10 +31,7 @@
 #ifndef PHP_HTTP_MESSAGE_H
 #define PHP_HTTP_MESSAGE_H 1
 
-/* Temp for cmake */
-#define HAVE_HTTP_MESSAGE 1
-
-#define PHP_HTTP_MESSAGE_VERSION "0.0.1"
+#define PHP_HTTP_MESSAGE_VERSION "1.0.0"
 #define PHP_HTTP_MESSAGE_EXTNAME "http_message"
 
 #ifdef PHP_WIN32
@@ -45,15 +42,15 @@
 # define PHP_HTTP_MESSAGE_API
 #endif
 
-static PHP_MINFO_FUNCTION(http_message);
-static PHP_MINIT_FUNCTION(http_message);
-
 extern PHP_MINIT_FUNCTION(http_message_message);
 extern PHP_MINIT_FUNCTION(http_message_request);
 extern PHP_MINIT_FUNCTION(http_message_serverrequest);
 extern PHP_MINIT_FUNCTION(http_message_response);
 extern PHP_MINIT_FUNCTION(http_message_stream);
 extern PHP_MINIT_FUNCTION(http_message_uri);
+extern PHP_MINIT_FUNCTION(http_message_uploadedfile);
+extern PHP_MINIT_FUNCTION(http_message_factory);
+extern PHP_MINIT_FUNCTION(http_message_emitter);
 
 extern zend_module_entry http_message_module_entry;
 
@@ -63,6 +60,8 @@ extern zend_class_entry *HttpMessage_ServerRequest_ce;
 extern zend_class_entry *HttpMessage_Response_ce;
 extern zend_class_entry *HttpMessage_Stream_ce;
 extern zend_class_entry *HttpMessage_Uri_ce;
+extern zend_class_entry *HttpMessage_UploadedFile_ce;
+extern zend_class_entry *HttpMessage_Factory_ce;
+extern zend_class_entry *HttpMessage_Emitter_ce;
 
 #endif
-
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
deleted file mode 100644
index 813111e..0000000
--- a/phpunit.xml.dist
+++ /dev/null
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<phpunit
-    colors="true"
-    bootstrap="vendor/autoload.php"
-    convertErrorsToExceptions="true"
-    convertNoticesToExceptions="true"
-    convertWarningsToExceptions="true"
->
-    <testsuites>
-        <testsuite name="tests">
-            <directory>tests/phpunit/</directory>
-        </testsuite>
-    </testsuites>
-</phpunit>
-
diff --git a/request.c b/request.c
index 328faf6..63e4af6 100644
--- a/request.c
+++ b/request.c
@@ -33,42 +33,37 @@
 #endif
 
 #include "php.h"
-#include "php_ini.h"
 #include "php_http_message.h"
 #include "macros.h"
 #include "zend_exceptions.h"
 #include "zend_interfaces.h"
-#include "ext/standard/info.h"
+#include "zend_smart_str.h"
 #include "ext/psr/psr_http_message.h"
 
 #if HAVE_HTTP_MESSAGE
 
-zend_class_entry *HttpMessage_Request_ce;
+zend_class_entry *HttpMessage_Request_ce = NULL;
 
 
 /* __construct */
 
+/* unused
 ZEND_BEGIN_ARG_INFO_EX(arginfo_Request_construct, 0, 0, 0)
 ZEND_END_ARG_INFO()
+*/
 
 PHP_METHOD(Request, __construct)
 {
-    zval rv, *uri, uri_string;
+    zval rv, *uri;
 
     /* parent::__construct() */
     zend_call_method_with_0_params(
-            getThis(), HttpMessage_Message_ce, &HttpMessage_Message_ce->constructor, "__construct", NULL
+            PROPERTY_ARG(getThis()), HttpMessage_Message_ce, &HttpMessage_Message_ce->constructor, "__construct", NULL
     );
 
     /* $this->uri = new Uri() */
-    uri = zend_read_property(HttpMessage_Request_ce, getThis(), ZEND_STRL("uri"), 0, &rv);
-    object_init_ex(uri, HttpMessage_Uri_ce);
-    if (uri != NULL && Z_TYPE_P(uri) == IS_OBJECT) { /* Should always be true */
-        ZVAL_EMPTY_STRING(&uri_string);
-        zend_call_method_with_1_params(
-                uri, HttpMessage_Uri_ce, &HttpMessage_Uri_ce->constructor, "__construct", NULL, &uri_string
-        );
-    }
+    uri = zend_read_property(HttpMessage_Request_ce, PROPERTY_ARG(getThis()), ZEND_STRL("uri"), 0, &rv);
+    NEW_OBJECT(uri, HttpMessage_Uri_ce);
 }
 
 
@@ -76,27 +71,50 @@ PHP_METHOD(Request, __construct)
 
 PHP_METHOD(Request, getRequestTarget)
 {
-    zval rv, *value;
+    zval rv, *value, *uri, path, query;
+    smart_str buf = {0};
 
-    value = zend_read_property(HttpMessage_Request_ce, getThis(), ZEND_STRL("requestTarget"), 0, &rv);
+    value = zend_read_property(HttpMessage_Request_ce, PROPERTY_ARG(getThis()), ZEND_STRL("requestTarget"), 0, &rv);
 
-    RETURN_ZVAL(value, 1, 0);
+    if (!ZVAL_IS_NULL(value)) {
+        RETURN_ZVAL(value, 1, 0);
+    }
+
+    uri = zend_read_property(HttpMessage_Request_ce, PROPERTY_ARG(getThis()), ZEND_STRL("uri"), 0, &rv);
+    zend_call_method_with_0_params(PROPERTY_ARG(uri), NULL, NULL, "getPath", &path);
+    zend_call_method_with_0_params(PROPERTY_ARG(uri), NULL, NULL, "getQuery", &query);
+
+    if (UNEXPECTED(Z_TYPE(path) != IS_STRING) || Z_STRLEN(path) == 0) {
+        RETURN_STRING("/");
+    }
+
+    if (Z_TYPE(query) != IS_STRING || Z_STRLEN(query) == 0) {
+        RETURN_ZVAL(&path, 1, 0);
+    }
+
+    smart_str_appendl(&buf, Z_STRVAL(path), Z_STRLEN(path));
+    smart_str_appends(&buf, "?");
+    smart_str_appendl(&buf, Z_STRVAL(query), Z_STRLEN(query));
+
+    RETVAL_STR_COPY(buf.s);
+    zend_string_release(buf.s);
 }
 
 PHP_METHOD(Request, withRequestTarget)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR_EX(value, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(
-            HttpMessage_Request_ce, return_value, ZEND_STRL("requestTarget"), value, value_len
-    );
+    if (EXPECTED(value != NULL)) {
+        zend_update_property_str(HttpMessage_Request_ce, PROPERTY_ARG(return_value), ZEND_STRL("requestTarget"), value);
+    } else {
+        zend_update_property_null(HttpMessage_Request_ce, PROPERTY_ARG(return_value), ZEND_STRL("requestTarget"));
+    }
 }
 
 
@@ -106,25 +124,22 @@ PHP_METHOD(Request, getMethod)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Request_ce, getThis(), ZEND_STRL("method"), 0, &rv);
+    value = zend_read_property(HttpMessage_Request_ce, PROPERTY_ARG(getThis()), ZEND_STRL("method"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Request, withMethod)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(
-            HttpMessage_Request_ce, return_value, ZEND_STRL("method"), value, value_len
-    );
+    zend_update_property_str(HttpMessage_Request_ce, PROPERTY_ARG(return_value), ZEND_STRL("method"), value);
 }
 
 
@@ -134,22 +149,28 @@ PHP_METHOD(Request, getUri)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Request_ce, getThis(), ZEND_STRL("uri"), 0, &rv);
+    value = zend_read_property(HttpMessage_Request_ce, PROPERTY_ARG(getThis()), ZEND_STRL("uri"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Request, withUri)
 {
-    zval *value;
+    zval *value = NULL;
+    zend_class_entry *uri_interface = HTTP_MESSAGE_PSR_INTERFACE("uri");
+
+    if (uri_interface == NULL) {
+        zend_throw_error(NULL, "Psr\\Http\\Message\\UriInterface not found");
+        return;
+    }
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_OBJECT_OF_CLASS(value, PsrHttpMessageUriInterface_ce_ptr)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_OBJECT_OF_CLASS(value, uri_interface)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property(HttpMessage_Request_ce, return_value, ZEND_STRL("uri"), value);
+    zend_update_property(HttpMessage_Request_ce, PROPERTY_ARG(return_value), ZEND_STRL("uri"), value);
 }
 
 
@@ -169,15 +190,20 @@ static const zend_function_entry request_functions[] = {
 PHP_MINIT_FUNCTION(http_message_request)
 {
     zend_class_entry ce;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("request");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(interface, "Request");
+    if (HttpMessage_Message_ce == NULL) return FAILURE;
+
     INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "Request", request_functions);
 
     HttpMessage_Request_ce = zend_register_internal_class_ex(&ce, HttpMessage_Message_ce);
-    zend_class_implements(HttpMessage_Request_ce, 1, PsrHttpMessageRequestInterface_ce_ptr);
+    zend_class_implements(HttpMessage_Request_ce, 1, interface);
 
     /* Properties */
-    zend_declare_property_string(HttpMessage_Request_ce, ZEND_STRL("requestTarget"), "/", ZEND_ACC_PROTECTED);
-    zend_declare_property_string(HttpMessage_Request_ce, ZEND_STRL("method"), "", ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_Request_ce, ZEND_STRL("uri"), ZEND_ACC_PROTECTED);
+    zend_declare_property_null(HttpMessage_Request_ce, ZEND_STRL("requestTarget"), ZEND_ACC_PRIVATE);
+    zend_declare_property_string(HttpMessage_Request_ce, ZEND_STRL("method"), "", ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_Request_ce, ZEND_STRL("uri"), ZEND_ACC_PRIVATE);
 
     return SUCCESS;
 }
diff --git a/response.c b/response.c
index 510ea0a..9787501 100644
--- a/response.c
+++ b/response.c
@@ -33,43 +33,65 @@
 #endif
 
 #include "php.h"
-#include "php_ini.h"
+#include "ext/psr/psr_http_message.h"
+#include "ext/spl/spl_exceptions.h"
 #include "php_http_message.h"
 #include "macros.h"
+#include "response.h"
 #include "zend_exceptions.h"
 #include "zend_interfaces.h"
-#include "http_status_codes.h"
-#include "ext/standard/info.h"
-#include "ext/psr/psr_http_message.h"
 
 #if HAVE_HTTP_MESSAGE
 
 zend_class_entry *HttpMessage_Response_ce;
 
+int response_set_status(zval *obj, zend_long code, zend_string *phrase)
+{
+    const char *suggested_phrase;
 
-/* Helper function to get reason phrase from status code */
+    if (code < 100 || code > 999) {
+        zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, "Invalid HTTP status code %ld", code);
+        return FAILURE;
+    }
 
-static int status_comp(const void *a, const void *b)
-{
-    const http_response_status_code_pair *pa = (const http_response_status_code_pair *) a;
-    const http_response_status_code_pair *pb = (const http_response_status_code_pair *) b;
+    zend_update_property_long(HttpMessage_Response_ce, PROPERTY_ARG(obj), ZEND_STRL("statusCode"), code);
 
-    if (pa->code < pb->code) {
-        return -1;
-    } else if (pa->code > pb->code) {
-        return 1;
+    if (phrase != NULL) {
+        zend_update_property_str(HttpMessage_Response_ce, PROPERTY_ARG(obj), ZEND_STRL("reasonPhrase"), phrase);
+    } else {
+        suggested_phrase = get_status_string((int)code);
+        zend_update_property_stringl(
+                HttpMessage_Response_ce, PROPERTY_ARG(obj), ZEND_STRL("reasonPhrase"),
+                suggested_phrase, strlen(suggested_phrase)
+        );
     }
 
-    return 0;
+    return SUCCESS;
 }
 
-static const char *get_status_string(int code)
+/* __construct */
+
+/* unused
+ZEND_BEGIN_ARG_INFO_EX(arginfo_HttpMessageServerRequest_construct, 0, 0, 0)
+    ZEND_ARG_TYPE_INFO(0, statusCode, IS_LONG, 0)
+    ZEND_ARG_TYPE_INFO(0, reasonPhrase, IS_STRING, 0)
+ZEND_END_ARG_INFO()
+*/
+
+PHP_METHOD(Response, __construct)
 {
-    http_response_status_code_pair needle = {code, NULL}, *result = NULL;
+    zend_long code = 0;
+    zend_string *phrase = NULL;
 
-    result = bsearch(&needle, http_status_map, http_status_map_len, sizeof(needle), status_comp);
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 2)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_LONG(code);
+        Z_PARAM_STR_EX(phrase, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
 
-    return result ? result->str : "";
+    if (code > 0) {
+        response_set_status(getThis(), code, phrase);
+    }
 }
 
 
@@ -79,7 +101,7 @@ PHP_METHOD(Response, getStatusCode)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Response_ce, getThis(), ZEND_STRL("statusCode"), 0, &rv);
+    value = zend_read_property(HttpMessage_Response_ce, PROPERTY_ARG(getThis()), ZEND_STRL("statusCode"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
@@ -88,37 +110,25 @@ PHP_METHOD(Response, getReasonPhrase)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Response_ce, getThis(), ZEND_STRL("reasonPhrase"), 0, &rv);
+    value = zend_read_property(HttpMessage_Response_ce, PROPERTY_ARG(getThis()), ZEND_STRL("reasonPhrase"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Response, withStatus)
 {
-    zend_long code;
-    char *phrase;
-    size_t phrase_len;
-    const char *suggested_phrase;
+    zend_long code = 0;
+    zend_string *phrase = NULL;
 
-    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 2)
         Z_PARAM_LONG(code)
-        Z_PARAM_STRING(phrase, phrase_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
-
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+        Z_PARAM_OPTIONAL
+        Z_PARAM_STR_EX(phrase, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
 
-    zend_update_property_long(HttpMessage_Response_ce, return_value, ZEND_STRL("statusCode"), code);
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    if (phrase_len > 0) {
-        zend_update_property_stringl(
-                HttpMessage_Response_ce, return_value, ZEND_STRL("reasonPhrase"), phrase, phrase_len
-        );
-    } else {
-        suggested_phrase = get_status_string((int)code);
-        zend_update_property_stringl(
-                HttpMessage_Response_ce, return_value, ZEND_STRL("reasonPhrase"), ZEND_STRL(suggested_phrase)
-        );
-    }
+    response_set_status(return_value, code, phrase);
 }
 
 
@@ -134,14 +144,19 @@ static const zend_function_entry response_functions[] = {
 PHP_MINIT_FUNCTION(http_message_response)
 {
     zend_class_entry ce;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("response");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(interface, "Response");
+    if (HttpMessage_Message_ce == NULL) return FAILURE;
+
     INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "Response", response_functions);
 
     HttpMessage_Response_ce = zend_register_internal_class_ex(&ce, HttpMessage_Message_ce);
-    zend_class_implements(HttpMessage_Response_ce, 1, PsrHttpMessageResponseInterface_ce_ptr);
+    zend_class_implements(HttpMessage_Response_ce, 1, interface);
 
     /* Properties */
-    zend_declare_property_long(HttpMessage_Response_ce, ZEND_STRL("statusCode"), 0, ZEND_ACC_PROTECTED);
-    zend_declare_property_string(HttpMessage_Response_ce, ZEND_STRL("reasonPhrase"), "", ZEND_ACC_PROTECTED);
+    zend_declare_property_long(HttpMessage_Response_ce, ZEND_STRL("statusCode"), 0, ZEND_ACC_PRIVATE);
+    zend_declare_property_string(HttpMessage_Response_ce, ZEND_STRL("reasonPhrase"), "", ZEND_ACC_PRIVATE);
 
     return SUCCESS;
 }
diff --git a/response.h b/response.h
new file mode 100644
index 0000000..d7aab2f
--- /dev/null
+++ b/response.h
@@ -0,0 +1,62 @@
+/*
+  +----------------------------------------------------------------------+
+  | HTTP Message PHP extension                                           |
+  | Helper function to get reason phrase from status code                |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 2019 Arnold Daniels                                    |
+  +----------------------------------------------------------------------+
+  | Permission is hereby granted, free of charge, to any person          |
+  | obtaining a copy of this software and associated documentation files |
+  | (the "Software"), to deal in the Software without restriction,       |
+  | including without limitation the rights to use, copy, modify, merge, |
+  | publish, distribute, sublicense, and/or sell copies of the Software, |
+  | and to permit persons to whom the Software is furnished to do so,    |
+  | subject to the following conditions:                                 |
+  |                                                                      |
+  | The above copyright notice and this permission notice shall be       |
+  | included in all copies or substantial portions of the Software.      |
+  |                                                                      |
+  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,      |
+  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF   |
+  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                |
+  | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS  |
+  | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN   |
+  | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN    |
+  | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE     |
+  | SOFTWARE.                                                            |
+  +----------------------------------------------------------------------+
+  | Author: Arnold Daniels <arnold@jasny.net>                            |
+  +----------------------------------------------------------------------+
+*/
+
+#ifndef HTTP_MESSAGE_RESPONSE_H
+#define HTTP_MESSAGE_RESPONSE_H
+
+#include "http_status_codes.h"
+
+static int status_comp(const void *a, const void *b)
+{
+    const http_response_status_code_pair *pa = (const http_response_status_code_pair *) a;
+    const http_response_status_code_pair *pb = (const http_response_status_code_pair *) b;
+
+    if (pa->code < pb->code) {
+        return -1;
+    } else if (pa->code > pb->code) {
+        return 1;
+    }
+
+    return 0;
+}
+
+static const char *get_status_string(int code)
+{
+    http_response_status_code_pair needle = {code, NULL}, *result = NULL;
+
+    result = bsearch(&needle, http_status_map, http_status_map_len, sizeof(needle), status_comp);
+
+    return result ? result->str : "";
+}
+
+int response_set_status(zval *obj, zend_long code, zend_string *phrase);
+
+#endif //HTTP_MESSAGE_RESPONSE_H
diff --git a/server_request.c b/server_request.c
index ca6167e..daa9a51 100644
--- a/server_request.c
+++ b/server_request.c
@@ -32,19 +32,155 @@
 # include "config.h"
 #endif
 
-#include "php.h"
-#include "php_ini.h"
+#include <php.h>
+#include "ext/spl/spl_exceptions.h"
+#include "ext/psr/psr_http_message.h"
 #include "php_http_message.h"
 #include "macros.h"
+#include "uploaded_file.h"
 #include "zend_exceptions.h"
 #include "zend_interfaces.h"
-#include "ext/standard/info.h"
-#include "ext/psr/psr_http_message.h"
 
 #if HAVE_HTTP_MESSAGE
 
-zend_class_entry *HttpMessage_ServerRequest_ce;
+zend_class_entry *HttpMessage_ServerRequest_ce = NULL;
+
+int assert_uploaded_files(HashTable *array)
+{
+    zval *entry;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("uploadedfile");
+
+    if (interface == NULL) {
+        zend_throw_error(NULL, "Psr\\Http\\Message\\UploadedFileInterface not found");
+        return FAILURE;
+    }
+
+    ZEND_HASH_FOREACH_VAL(array, entry) {
+        if (Z_TYPE_P(entry) == IS_OBJECT && EXPECTED(instanceof_function(Z_OBJCE_P(entry), interface))) {
+            continue;
+        }
+
+        if (UNEXPECTED(Z_TYPE_P(entry) != IS_ARRAY || assert_uploaded_files(Z_ARR_P(entry)) == FAILURE)) { // recursion
+            zend_throw_exception(spl_ce_InvalidArgumentException,
+                     "Expected all elements to implement Psr\\Http\\Message\\UploadedFileInterface", 0);
+            return FAILURE;
+        }
+    } ZEND_HASH_FOREACH_END();
+
+    return SUCCESS;
+}
+
+void add_header_from_param(HashTable *headers, char *key, size_t keylen, zval *val)
+{
+    zval *new_header, valcpy;
+    char header[256];
+    size_t i;
+    zend_bool lower; // Turn next char to lower case
+
+    if (UNEXPECTED(keylen > 255)) {
+        php_error_docref(NULL, E_WARNING, "Ignoring header '%s'; field to long", key);
+        return;
+    }
+
+    strncpy(header, key, keylen);
+    header[keylen] = '\0';
+
+    for (i = 0, lower = 0; i < keylen; i++) {
+        if (lower && header[i] >= 65 && header[i] <= 90) { // is ASCII A-Z
+            header[i] += 32; // to lower case
+        }
+
+        if (header[i] == '_') {
+            header[i] = '-';
+            lower = 0;
+        } else {
+            lower = 1;
+        }
+    }
+
+    new_header = zend_hash_str_add_empty_element(headers, header, keylen);
+
+    array_init(new_header);
+    ZVAL_COPY(&valcpy, val);
+    add_next_index_zval(new_header, &valcpy);
+}
 
+void init_headers_from_params(zval *object, HashTable *serverParams)
+{
+    zval rv, *val;
+    HashTable *headers;
+    zend_long index;
+    zend_string *key;
+
+    headers = Z_ARR_P(zend_read_property(HttpMessage_Message_ce, PROPERTY_ARG(object), ZEND_STRL("headers"), 0, &rv));
+
+    ZEND_HASH_FOREACH_KEY_VAL(serverParams, index, key, val) {
+        (void)index; /* NOOP, to avoid unused warning */
+        if (UNEXPECTED(key == NULL)) continue;
+
+        if (ZSTR_LEN(key) > 5 && strncmp("HTTP_", ZSTR_VAL(key), 5) == 0 && EXPECTED(Z_TYPE_P(val) == IS_STRING)) {
+            add_header_from_param(headers, ZSTR_VAL(key) + 5, ZSTR_LEN(key) - 5, val);
+        }
+    } ZEND_HASH_FOREACH_END();
+
+    val = zend_hash_str_find(serverParams, ZEND_STRL("CONTENT_TYPE"));
+    if (val != NULL && EXPECTED(Z_TYPE_P(val) == IS_STRING)) {
+        add_header_from_param(headers, ZEND_STRL("CONTENT_TYPE"), val);
+    }
+
+    val = zend_hash_str_find(serverParams, ZEND_STRL("CONTENT_LENGTH"));
+    if (val != NULL && EXPECTED(Z_TYPE_P(val) == IS_LONG)) {
+        add_header_from_param(headers, ZEND_STRL("CONTENT_LENGTH"), val);
+    }
+}
+
+void init_uri_from_params(zval *object, HashTable *serverParams)
+{
+    zval rv, *uri, *tmp, *request_target, *protocol, *https, *user, *pass;
+    zend_long port = -1, default_port = -1;
+    zend_bool is_http, is_https;
+
+    uri = zend_read_property(HttpMessage_Request_ce, PROPERTY_ARG(object), ZEND_STRL("uri"), 0, &rv);
+
+    request_target = zend_hash_str_find(serverParams, ZEND_STRL("REQUEST_URI"));
+    zend_call_method(PROPERTY_ARG(uri), HttpMessage_Uri_ce, &HttpMessage_Uri_ce->constructor, ZEND_STRL("__construct"), NULL,
+             request_target == NULL ? 0 : 1, request_target, NULL);
+
+    COPY_PROPERTY_FROM_ARRAY(serverParams, "HTTP_HOST", uri, HttpMessage_Uri_ce, "host", IS_STRING, tmp);
+    COPY_PROPERTY_FROM_ARRAY(serverParams, "QUERY_STRING", uri, HttpMessage_Uri_ce, "query", IS_STRING, tmp);
+
+    tmp = zend_hash_str_find(serverParams, ZEND_STRL("SERVER_PORT"));
+    if (tmp != NULL && EXPECTED(Z_TYPE_P(tmp) == IS_LONG && Z_LVAL_P(tmp) > 0)) {
+        port = Z_LVAL_P(tmp);
+    }
+
+    protocol = zend_hash_str_find(serverParams, ZEND_STRL("SERVER_PROTOCOL"));
+    https = zend_hash_str_find(serverParams, ZEND_STRL("HTTPS"));
+    is_https = https != NULL && Z_STRCMP(https, "off", 0) != 0;
+    is_http = protocol != NULL && Z_TYPE_P(protocol) == IS_STRING
+        ? (strncmp("HTTP/", Z_STRVAL_P(protocol), 5) == 0)
+        : (port > 0 && port == (is_https ? 443 : 80));
+
+    if (!is_http) {
+        // do nothing
+    } else if (!is_https) {
+        default_port = 80;
+        zend_update_property_stringl(HttpMessage_Uri_ce, PROPERTY_ARG(uri), ZEND_STRL("scheme"), ZEND_STRL("http"));
+    } else {
+        default_port = 443;
+        zend_update_property_stringl(HttpMessage_Uri_ce, PROPERTY_ARG(uri), ZEND_STRL("scheme"), ZEND_STRL("https"));
+    }
+
+    if (port != default_port && port > 0) {
+        zend_update_property_long(HttpMessage_Uri_ce, PROPERTY_ARG(uri), ZEND_STRL("port"), port);
+    }
+
+    user = zend_hash_str_find(serverParams, ZEND_STRL("PHP_AUTH_USER"));
+    if (user != NULL) {
+        pass = zend_hash_str_find(serverParams, ZEND_STRL("PHP_AUTH_PASS"));
+        uri_set_userinfo(uri, Z_STRVAL_P(user), Z_STRLEN_P(user), Z_STRVAL_P_NULL(pass), Z_STRLEN_P_NULL(pass));
+    }
+}
 
 /* __construct */
 
@@ -52,23 +188,51 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_HttpMessageServerRequest_construct, 0, 0, 0)
     ZEND_ARG_TYPE_INFO(0, serverParams, IS_ARRAY, 0)
     ZEND_ARG_TYPE_INFO(0, cookieParams, IS_ARRAY, 0)
     ZEND_ARG_TYPE_INFO(0, queryParams, IS_ARRAY, 0)
-    ZEND_ARG_INFO(0, parsedBody)
+    ZEND_ARG_TYPE_INFO(0, postParams, IS_ARRAY, 0)
     ZEND_ARG_TYPE_INFO(0, files, IS_ARRAY, 0)
 ZEND_END_ARG_INFO()
 
 PHP_METHOD(ServerRequest, __construct)
 {
-    zval rv;
+    zval rv, *uploadedFiles, *val;
+    zval *serverParams = NULL, *cookieParams = NULL, *queryParams = NULL, *post = NULL, *files = NULL;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 5)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_ARRAY_EX(serverParams, 1, 0)
+        Z_PARAM_ARRAY_EX(cookieParams, 1, 0)
+        Z_PARAM_ARRAY_EX(queryParams, 1, 0)
+        Z_PARAM_ARRAY_EX(post, 1, 0)
+        Z_PARAM_ARRAY_EX(files, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
 
     /* parent::__construct() */
     zend_call_method_with_0_params(
-            getThis(), HttpMessage_Request_ce, &HttpMessage_Request_ce->constructor, "__construct", NULL
+            PROPERTY_ARG(getThis()), HttpMessage_Request_ce, &HttpMessage_Request_ce->constructor, "__construct", NULL
     );
 
-    INIT_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "serverParams", rv);
-    INIT_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "cookieParams", rv);
-    INIT_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "queryParams", rv);
-    INIT_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "uploadedFiles", rv);
+    SET_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "serverParams", serverParams, rv);
+    SET_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "cookieParams", cookieParams, rv);
+    SET_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "queryParams", queryParams, rv);
+
+    if (files != NULL) {
+        uploadedFiles = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("uploadedFiles"), 0, &rv);
+        create_uploaded_files(uploadedFiles, Z_ARR_P(files));
+    } else {
+        INIT_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "uploadedFiles", rv);
+    }
+
+    if (post != NULL) {
+        zend_update_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("parsedBody"), post);
+    }
+
+    if (serverParams != NULL) {
+        COPY_PROPERTY_FROM_ARRAY(Z_ARR_P(serverParams), "REQUEST_METHOD", getThis(), HttpMessage_ServerRequest_ce,
+                "method", IS_STRING, val);
+        init_headers_from_params(getThis(), Z_ARR_P(serverParams));
+        init_uri_from_params(getThis(), Z_ARR_P(serverParams));
+    }
+
     INIT_ARRAY_PROPERTY(HttpMessage_ServerRequest_ce, "attributes", rv);
 }
 
@@ -79,7 +243,7 @@ PHP_METHOD(ServerRequest, getServerParams)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_ServerRequest_ce, getThis(), ZEND_STRL("serverParams"), 0, &rv);
+    value = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("serverParams"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
@@ -91,7 +255,7 @@ PHP_METHOD(ServerRequest, getCookieParams)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_ServerRequest_ce, getThis(), ZEND_STRL("cookieParams"), 0, &rv);
+    value = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("cookieParams"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
@@ -102,11 +266,11 @@ PHP_METHOD(ServerRequest, withCookieParams)
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
         Z_PARAM_ARRAY(value);
-    ZEND_PARSE_PARAMETERS_END_EX();
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property(HttpMessage_ServerRequest_ce, return_value, ZEND_STRL("cookieParams"), value);
+    zend_update_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("cookieParams"), value);
 }
 
 
@@ -116,7 +280,7 @@ PHP_METHOD(ServerRequest, getQueryParams)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_ServerRequest_ce, getThis(), ZEND_STRL("queryParams"), 0, &rv);
+    value = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("queryParams"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
@@ -127,21 +291,21 @@ PHP_METHOD(ServerRequest, withQueryParams)
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
         Z_PARAM_ARRAY(value);
-    ZEND_PARSE_PARAMETERS_END_EX();
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property(HttpMessage_ServerRequest_ce, return_value, ZEND_STRL("queryParams"), value);
+    zend_update_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("queryParams"), value);
 }
 
 
-/* queryParams */
+/* uploadedFiles */
 
 PHP_METHOD(ServerRequest, getUploadedFiles)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_ServerRequest_ce, getThis(), ZEND_STRL("uploadedFiles"), 0, &rv);
+    value = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("uploadedFiles"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
@@ -152,11 +316,15 @@ PHP_METHOD(ServerRequest, withUploadedFiles)
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
         Z_PARAM_ARRAY(value);
-    ZEND_PARSE_PARAMETERS_END_EX();
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (UNEXPECTED(assert_uploaded_files(Z_ARR_P(value)) == FAILURE)) {
+        return;
+    }
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property(HttpMessage_ServerRequest_ce, return_value, ZEND_STRL("uploadedFiles"), value);
+    zend_update_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("uploadedFiles"), value);
 }
 
 
@@ -166,22 +334,26 @@ PHP_METHOD(ServerRequest, getParsedBody)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_ServerRequest_ce, getThis(), ZEND_STRL("parsedBody"), 0, &rv);
+    value = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("parsedBody"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(ServerRequest, withParsedBody)
 {
-    zval *value;
+    zval *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_ARRAY_OR_OBJECT(value, 0, 0);
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_ARRAY_OR_OBJECT_EX(value, 1, 0);
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property(HttpMessage_ServerRequest_ce, return_value, ZEND_STRL("parsedBody"), value);
+    if (EXPECTED(value != NULL)) {
+        zend_update_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("parsedBody"), value);
+    } else {
+        zend_update_property_null(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("parsedBody"));
+    }
 }
 
 
@@ -191,105 +363,116 @@ PHP_METHOD(ServerRequest, getAttributes)
 {
     zval rv, *attributes;
 
-    attributes = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("attributes"), 0, &rv);
+    attributes = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("attributes"), 0, &rv);
 
     RETURN_ZVAL(attributes, 1, 0);
 }
 
 PHP_METHOD(ServerRequest, getAttribute)
 {
-    zval rv, *attributes, *value, *default_value;
-    char *name;
-    size_t name_len;
+    zval rv, *attributes, *value, *default_value = NULL;
+    zend_string *name;
 
-    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(name, name_len)
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 2)
+        Z_PARAM_STR(name)
+        Z_PARAM_OPTIONAL
         Z_PARAM_ZVAL(default_value)
-    ZEND_PARSE_PARAMETERS_END_EX();
+    ZEND_PARSE_PARAMETERS_END();
 
-    attributes = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("attributes"), 0, &rv);
+    attributes = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(getThis()), ZEND_STRL("attributes"), 0, &rv);
 
-    value = zend_hash_str_find(Z_ARRVAL_P(attributes), name, name_len);
+    value = zend_hash_find(Z_ARRVAL_P(attributes), name);
 
     /* value is only NULL if the entry wasn't found. A null value is still a zval. */
-    RETURN_ZVAL(value != NULL ? value : default_value, 1, 0);
+    if (value != NULL) {
+        RETURN_ZVAL(value, 1, 0);
+    } else if (default_value != NULL) {
+        RETURN_ZVAL(default_value, 1, 0);
+    } else {
+        RETURN_NULL();
+    }
 }
 
 PHP_METHOD(ServerRequest, withAttribute)
 {
-    zval rv, *value, *attributes, new_attributes;
-    char *name;
-    size_t name_len;
+    zval rv, *value, *attributes_prop;
+    HashTable *attributes;
+    zend_string *name;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 2, 2)
-        Z_PARAM_STRING(name, name_len)
+        Z_PARAM_STR(name)
         Z_PARAM_ZVAL(value)
-    ZEND_PARSE_PARAMETERS_END_EX();
-
-    attributes = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("attributes"), 0, &rv);
-    ZVAL_COPY(&new_attributes, attributes);
+    ZEND_PARSE_PARAMETERS_END();
 
-    add_assoc_zval_ex(&new_attributes, name, name_len, value);
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    attributes_prop = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("attributes"), 0, &rv);
+    attributes = zend_array_dup(Z_ARR_P(attributes_prop));
 
-    zend_update_property(HttpMessage_Message_ce, return_value, ZEND_STRL("attributes"), &new_attributes);
+    zend_symtable_update(attributes, name, value);
+    ZVAL_ARR(attributes_prop, attributes);
 }
 
 PHP_METHOD(ServerRequest, withoutAttribute)
 {
-    zval rv, *attributes;
-    char *name;
-    size_t name_len;
+    zend_string *name;
+    zval rv, *attributes_prop;
+    HashTable *attributes;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(name, name_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(name)
+    ZEND_PARSE_PARAMETERS_END();
 
-    attributes = zend_read_property(HttpMessage_Message_ce, getThis(), ZEND_STRL("attributes"), 0, &rv);
-    zend_hash_str_del(Z_ARRVAL_P(attributes), name, name_len);
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    attributes_prop = zend_read_property(HttpMessage_ServerRequest_ce, PROPERTY_ARG(return_value), ZEND_STRL("attributes"), 0, &rv);
+    attributes = zend_array_dup(Z_ARR_P(attributes_prop));
 
-    zend_update_property(HttpMessage_Message_ce, return_value, ZEND_STRL("attributes"), attributes);
+    zend_symtable_del(attributes, name);
+    ZVAL_ARR(attributes_prop, attributes);
 }
 
 
 /* Define HttpMessage\ServerRequest class */
 
 static const zend_function_entry request_functions[] = {
-        PHP_ME(ServerRequest, __construct, arginfo_HttpMessageServerRequest_construct, ZEND_ACC_PUBLIC)
-        HTTP_MESSAGE_ME(ServerRequest, getServerParams)
-        HTTP_MESSAGE_ME(ServerRequest, getCookieParams)
-        HTTP_MESSAGE_ME(ServerRequest, withCookieParams)
-        HTTP_MESSAGE_ME(ServerRequest, getQueryParams)
-        HTTP_MESSAGE_ME(ServerRequest, withQueryParams)
-        HTTP_MESSAGE_ME(ServerRequest, getUploadedFiles)
-        HTTP_MESSAGE_ME(ServerRequest, withUploadedFiles)
-        HTTP_MESSAGE_ME(ServerRequest, getParsedBody)
-        HTTP_MESSAGE_ME(ServerRequest, withParsedBody)
-        HTTP_MESSAGE_ME(ServerRequest, getAttributes)
-        HTTP_MESSAGE_ME(ServerRequest, getAttribute)
-        HTTP_MESSAGE_ME(ServerRequest, withAttribute)
-        HTTP_MESSAGE_ME(ServerRequest, withoutAttribute)
-        PHP_FE_END
+    PHP_ME(ServerRequest, __construct, arginfo_HttpMessageServerRequest_construct, ZEND_ACC_PUBLIC)
+    HTTP_MESSAGE_ME(ServerRequest, getServerParams)
+    HTTP_MESSAGE_ME(ServerRequest, getCookieParams)
+    HTTP_MESSAGE_ME(ServerRequest, withCookieParams)
+    HTTP_MESSAGE_ME(ServerRequest, getQueryParams)
+    HTTP_MESSAGE_ME(ServerRequest, withQueryParams)
+    HTTP_MESSAGE_ME(ServerRequest, getUploadedFiles)
+    HTTP_MESSAGE_ME(ServerRequest, withUploadedFiles)
+    HTTP_MESSAGE_ME(ServerRequest, getParsedBody)
+    HTTP_MESSAGE_ME(ServerRequest, withParsedBody)
+    HTTP_MESSAGE_ME(ServerRequest, getAttributes)
+    HTTP_MESSAGE_ME(ServerRequest, getAttribute)
+    HTTP_MESSAGE_ME(ServerRequest, withAttribute)
+    HTTP_MESSAGE_ME(ServerRequest, withoutAttribute)
+    PHP_FE_END
 };
 
 PHP_MINIT_FUNCTION(http_message_serverrequest)
 {
     zend_class_entry ce;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("serverrequest");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(interface, "ServerRequest");
+    if (HttpMessage_Request_ce == NULL) return FAILURE;
+
     INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "ServerRequest", request_functions);
 
     HttpMessage_ServerRequest_ce = zend_register_internal_class_ex(&ce, HttpMessage_Request_ce);
-    zend_class_implements(HttpMessage_ServerRequest_ce, 1, PsrHttpMessageServerRequestInterface_ce_ptr);
+    zend_class_implements(HttpMessage_ServerRequest_ce, 1, interface);
 
     /* Properties */
-    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("serverParams"), ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("cookieParams"), ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("queryParams"), ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("uploadedFiles"), ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("parsedBody"), ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("attributes"), ZEND_ACC_PROTECTED);
+    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("serverParams"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("cookieParams"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("queryParams"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("uploadedFiles"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("parsedBody"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_ServerRequest_ce, ZEND_STRL("attributes"), ZEND_ACC_PRIVATE);
 
     return SUCCESS;
 }
diff --git a/sign.gpg.enc b/sign.gpg.enc
new file mode 100644
index 0000000..54b7188
Binary files /dev/null and b/sign.gpg.enc differ
diff --git a/stream.c b/stream.c
index 976c976..226558f 100644
--- a/stream.c
+++ b/stream.c
@@ -33,98 +33,167 @@
 #endif
 
 #include "php.h"
-#include "php_ini.h"
-#include "php_http_message.h"
 #include "macros.h"
-#include "php_streams.h"
+#include "stream.h"
 #include "zend_exceptions.h"
 #include "zend_interfaces.h"
-#include "http_status_codes.h"
-#include "ext/standard/info.h"
 #include "ext/psr/psr_http_message.h"
 #include "ext/spl/spl_exceptions.h"
 
 #if HAVE_HTTP_MESSAGE
 
-zend_class_entry *HttpMessage_Stream_ce;
+zend_class_entry *HttpMessage_Stream_ce = NULL;
 
-zend_bool string_contains_char(char *haystack, char chr)
+void stream_seek(zval *this, zend_long offset, zend_long whence, zval* return_value)
 {
-    char *p = strchr(haystack, chr);
+    zval rv, *resource;
+    php_stream *stream;
+
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(this), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream is %s",
+                                Z_TYPE_P(resource) == IS_RESOURCE ? "closed" : "detached");
+        return;
+    }
+
+    if (whence > 3) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Invalid value for whence");
+        return;
+    }
 
-    return (p != NULL);
+    php_stream_from_zval(stream, resource);
+
+    if (!stream->ops->seek || (stream->flags & PHP_STREAM_FLAG_NO_SEEK) != 0) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream is not seekable");
+        return;
+    };
+
+    php_stream_seek(stream, offset, whence);
 }
 
 /* __construct */
 
 ZEND_BEGIN_ARG_INFO_EX(arginfo_HttpMessageStream_construct, 0, 0, 0)
-        ZEND_ARG_TYPE_INFO(0, uri, IS_RESOURCE, 0)
+    ZEND_ARG_INFO(0, uri)
 ZEND_END_ARG_INFO()
 
 PHP_METHOD(Stream, __construct)
 {
-    zval rv, *zstream = NULL, newstream;
+    zval *input = NULL, resource;
+    char *file = NULL, *mode = NULL;
+    size_t mode_len = 0;
     php_stream *stream;
 
-    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1)
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 2)
         Z_PARAM_OPTIONAL
-        Z_PARAM_RESOURCE(zstream)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_ZVAL(input)
+        Z_PARAM_STRING(mode, mode_len)
+    ZEND_PARSE_PARAMETERS_END();
 
-    if (zstream == NULL) {
-        RETURN_NULL();
+    if (UNEXPECTED(input != NULL && Z_TYPE_P(input) != IS_STRING && Z_TYPE_P(input) != IS_RESOURCE)) {
+        zend_type_error("Expected parameter 1 to be a string or resource, %s given ", zend_zval_type_name(input));
+        return;
+    }
 
-        stream = php_stream_fopen("php://temp", "r+", NULL);
-        ZVAL_RES(&newstream, (stream)->res); // php_stream_to_zval(stream, &newstream); // segfault ?
-        zstream = &newstream;
-    } else if (Z_RES_P(zstream)->type != php_file_le_stream() && Z_RES_P(zstream)->type != php_file_le_pstream()) {
-        zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Resource is not a stream");
+    if (input == NULL) {
+        if (open_temp_stream(&resource) == FAILURE) return;
+    } else if (Z_TYPE_P(input) == IS_STRING) {
+        file = Z_STRVAL_P(input);
+        file[Z_STRLEN_P(input)] = '\0';
+        stream = php_stream_open_wrapper(file, mode != NULL ? mode : "r", 0, NULL);
+
+        if (stream == NULL) {
+            zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Failed to open '%s' stream", file);
+            return;
+        }
+
+        php_stream_to_zval(stream, &resource);
+    } else if (EXPECTED(IS_STREAM_RESOURCE(input))) {
+        ZVAL_COPY(&resource, input);
+    } else {
+        zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, "Resource is not a stream");
+        return;
     }
 
-    zend_update_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), zstream);
+    zend_update_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), &resource);
 }
 
 PHP_METHOD(Stream, __toString)
 {
+    zval rv, *resource;
+    php_stream *stream;
+    zend_string *contents;
+
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
+        RETURN_EMPTY_STRING();
+    }
+
+    php_stream_from_zval(stream, resource);
+
+    if (!string_contains_char(stream->mode, 'r') && !string_contains_char(stream->mode, '+')) {
+        RETURN_EMPTY_STRING();
+    }
+
+    if ((stream->ops->seek) && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) {
+        php_stream_seek(stream, 0, SEEK_SET);
+    }
+
+    // Special case for 'php://input'. Need to reopen the resource, because seek doesn't work.
+    if (
+        stream->wrapper != NULL &&
+        strcmp(stream->wrapper->wops->label, "PHP") == 0 &&
+        strcmp(stream->ops->label, "Input") == 0
+    ) {
+        stream = php_stream_open_wrapper(stream->orig_path, stream->mode, 0, NULL);
+        php_stream_to_zval(stream, resource);
+    }
+
+    if ((contents = php_stream_copy_to_mem(stream, (ssize_t)PHP_STREAM_COPY_ALL, 0))) {
+        RETURN_STR(contents);
+    } else {
+        RETURN_EMPTY_STRING();
+    }
 }
 
 PHP_METHOD(Stream, close)
 {
-    zval rv, *zstream;
+    zval rv, *resource;
     php_stream *stream;
 
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
-        RETURN_NULL();
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (Z_TYPE_P(resource) != IS_RESOURCE) {
+        return;
     }
 
-    php_stream_from_zval(stream, zstream);
-    php_stream_close(stream);
+    if (IS_STREAM_RESOURCE(resource)) {
+        php_stream_from_zval(stream, resource);
+        php_stream_close(stream);
+    }
 }
 
 PHP_METHOD(Stream, detach)
 {
-    zval rv, *zstream;
-
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
+    zval rv, *resource;
 
-    zend_update_property_null(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"));
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    ZVAL_COPY(return_value, resource);
 
-    RETURN_ZVAL(zstream, 1, 0);
+    zend_update_property_null(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"));
 }
 
 PHP_METHOD(Stream, getSize)
 {
-    zval rv, *zstream;
+    zval rv, *resource;
     php_stream *stream;
     php_stream_statbuf ssb;
 
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
         RETURN_NULL();
     }
 
-    php_stream_from_zval(stream, zstream);
+    php_stream_from_zval(stream, resource);
     php_stream_stat(stream, &ssb);
 
     RETURN_LONG(ssb.sb.st_size);
@@ -132,16 +201,18 @@ PHP_METHOD(Stream, getSize)
 
 PHP_METHOD(Stream, tell)
 {
-    zval rv, *zstream;
+    zval rv, *resource;
     php_stream *stream;
     size_t pos;
 
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
-        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "The stream has been detached");
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream is %s",
+                Z_TYPE_P(resource) == IS_RESOURCE ? "closed" : "detached");
+        return;
     }
 
-    php_stream_from_zval(stream, zstream);
+    php_stream_from_zval(stream, resource);
     pos = php_stream_tell(stream);
 
     RETURN_LONG(pos);
@@ -149,16 +220,16 @@ PHP_METHOD(Stream, tell)
 
 PHP_METHOD(Stream, eof)
 {
-    zval rv, *zstream;
+    zval rv, *resource;
     php_stream *stream;
     zend_bool eof;
 
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
         RETURN_TRUE;
     }
 
-    php_stream_from_zval(stream, zstream);
+    php_stream_from_zval(stream, resource);
     eof = php_stream_eof(stream);
 
     RETURN_BOOL(eof);
@@ -166,16 +237,16 @@ PHP_METHOD(Stream, eof)
 
 PHP_METHOD(Stream, isSeekable)
 {
-    zval rv, *zstream;
+    zval rv, *resource;
     php_stream *stream;
     zend_bool seekable;
 
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
         RETURN_FALSE;
     }
 
-    php_stream_from_zval(stream, zstream);
+    php_stream_from_zval(stream, resource);
     seekable = (stream->ops->seek) && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0;
 
     RETURN_BOOL(seekable);
@@ -183,128 +254,219 @@ PHP_METHOD(Stream, isSeekable)
 
 PHP_METHOD(Stream, seek)
 {
-    zval rv, *zstream;
-    php_stream *stream;
-    size_t offset, whence;
+    zend_long offset = 0, whence = SEEK_SET;
 
-    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 2)
         Z_PARAM_LONG(offset)
+        Z_PARAM_OPTIONAL
         Z_PARAM_LONG(whence)
-    ZEND_PARSE_PARAMETERS_END_EX();
-
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
-        RETURN_FALSE;
-    }
-
-    php_stream_from_zval(stream, zstream);
+    ZEND_PARSE_PARAMETERS_END();
 
-    php_stream_seek(stream, offset, whence);
+    stream_seek(getThis(), offset, whence, return_value);
 }
 
 PHP_METHOD(Stream, rewind)
 {
-    zval offset;
-
-    ZVAL_LONG(&offset, 0);
-
-    zend_call_method_with_1_params(getThis(), HttpMessage_Stream_ce, NULL, "seek", return_value, &offset);
+    stream_seek(getThis(), 0, SEEK_SET, return_value);
 }
 
 PHP_METHOD(Stream, isWritable)
 {
-    zval rv, *zstream;
+    zval rv, *resource;
     php_stream *stream;
-    zend_bool writable;
 
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
         RETURN_FALSE;
     }
 
-    php_stream_from_zval(stream, zstream);
-    writable = !string_contains_char(stream->mode, 'r') || string_contains_char(stream->mode, '+');
+    php_stream_from_zval(stream, resource);
 
-    RETURN_BOOL(writable);
+    RETURN_BOOL(stream_is_writable(stream));
 }
 
 PHP_METHOD(Stream, write)
 {
+    zval rv, *resource;
+    char *input = NULL;
+    size_t len = 0, ret = 0;
+    php_stream *stream;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
+        Z_PARAM_STRING(input, len)
+    ZEND_PARSE_PARAMETERS_END();
+
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream is %s",
+                Z_TYPE_P(resource) == IS_RESOURCE ? "closed" : "detached");
+        return;
+    }
+
+    php_stream_from_zval(stream, resource);
+
+    if (!stream_is_writable(stream)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream not writable");
+        return;
+    }
+
+    if (len > 0) {
+        ret = php_stream_write(stream, input, len);
+    }
+
+    RETURN_LONG(ret);
 }
 
 PHP_METHOD(Stream, isReadable)
 {
-    zval rv, *zstream;
+    zval rv, *resource;
     php_stream *stream;
-    zend_bool writable;
 
-    zstream = zend_read_property(HttpMessage_Stream_ce, getThis(), ZEND_STRL("stream"), 0, &rv);
-    if (Z_TYPE_P(zstream) != IS_RESOURCE) {
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
         RETURN_FALSE;
     }
 
-    php_stream_from_zval(stream, zstream);
-    writable = string_contains_char(stream->mode, 'r') || string_contains_char(stream->mode, '+');
+    php_stream_from_zval(stream, resource);
 
-    RETURN_BOOL(writable);
+    RETURN_BOOL(stream_is_readable(stream));
 }
 
 PHP_METHOD(Stream, read)
 {
+    zval rv, *resource;
+    php_stream *stream;
+    zend_long len = 0;
+    zend_string *contents;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
+        Z_PARAM_LONG(len)
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (len < 0) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Length parameter must be equal or greater than 0");
+        return;
+    }
+
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream is %s",
+                Z_TYPE_P(resource) == IS_RESOURCE ? "closed" : "detached");
+        return;
+    }
+
+    php_stream_from_zval(stream, resource);
+
+    if (!string_contains_char(stream->mode, 'r') && !string_contains_char(stream->mode, '+')) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream not readable");
+        return;
+    }
+
+    if ((contents = php_stream_copy_to_mem(stream, (size_t)len, 0))) {
+        RETURN_STR(contents);
+    } else {
+        RETURN_EMPTY_STRING();
+    }
 }
 
 PHP_METHOD(Stream, getContents)
 {
+    zval rv, *resource;
+    php_stream *stream;
+    zend_string *contents;
+
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    if (!IS_STREAM_RESOURCE(resource)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream is %s",
+            Z_TYPE_P(resource) == IS_RESOURCE ? "closed" : "detached");
+        return;
+    }
+
+    php_stream_from_zval(stream, resource);
+
+    if (!string_contains_char(stream->mode, 'r') && !string_contains_char(stream->mode, '+')) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Stream not readable");
+        return;
+    }
+
+    if ((contents = php_stream_copy_to_mem(stream, (size_t)PHP_STREAM_COPY_ALL, 0))) {
+        RETURN_STR(contents);
+    } else {
+        RETURN_EMPTY_STRING();
+    }
 }
 
 PHP_METHOD(Stream, getMetadata)
 {
-    zval fname, zkey;
-    zend_string *key;
+    zval rv, fname, *resource, *zvalue;
+    zend_string *key = NULL;
 
-    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
+    resource = zend_read_property(HttpMessage_Stream_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1)
+        Z_PARAM_OPTIONAL
         Z_PARAM_STR(key)
-    ZEND_PARSE_PARAMETERS_END_EX();
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (!IS_STREAM_RESOURCE(resource)) {
+        if (key == NULL) {
+            array_init(return_value);
+        } else {
+            RETVAL_NULL();
+        }
+        return;
+    }
 
     ZVAL_STRING(&fname, "stream_get_meta_data");
-    ZVAL_STR(&zkey, key);
+    call_user_function(NULL, NULL, &fname, return_value, 1, resource);
 
-    call_user_function(NULL, NULL, &fname, return_value, 1, &zkey);
+    if (key != NULL) {
+        zvalue = zend_hash_find(Z_ARR(*return_value), key);
+
+        if (zvalue == NULL) {
+            ZVAL_NULL(return_value);
+        } else {
+            ZVAL_COPY(return_value, zvalue);
+        }
+    }
 }
 
 
 /* Define HttpMessage\Stream class */
 
 static const zend_function_entry stream_functions[] = {
-        PHP_ME(Stream, __construct, arginfo_HttpMessageStream_construct, ZEND_ACC_PUBLIC)
-        HTTP_MESSAGE_ME(Stream, __toString)
-        HTTP_MESSAGE_ME(Stream, close)
-        HTTP_MESSAGE_ME(Stream, detach)
-        HTTP_MESSAGE_ME(Stream, getSize)
-        HTTP_MESSAGE_ME(Stream, tell)
-        HTTP_MESSAGE_ME(Stream, eof)
-        HTTP_MESSAGE_ME(Stream, isSeekable)
-        HTTP_MESSAGE_ME(Stream, seek)
-        HTTP_MESSAGE_ME(Stream, rewind)
-        HTTP_MESSAGE_ME(Stream, isWritable)
-        HTTP_MESSAGE_ME(Stream, write)
-        HTTP_MESSAGE_ME(Stream, isReadable)
-        HTTP_MESSAGE_ME(Stream, read)
-        HTTP_MESSAGE_ME(Stream, getContents)
-        HTTP_MESSAGE_ME(Stream, getMetadata)
-        PHP_FE_END
+    PHP_ME(Stream, __construct, arginfo_HttpMessageStream_construct, ZEND_ACC_PUBLIC)
+    HTTP_MESSAGE_ME(Stream, __toString)
+    HTTP_MESSAGE_ME(Stream, close)
+    HTTP_MESSAGE_ME(Stream, detach)
+    HTTP_MESSAGE_ME(Stream, getSize)
+    HTTP_MESSAGE_ME(Stream, tell)
+    HTTP_MESSAGE_ME(Stream, eof)
+    HTTP_MESSAGE_ME(Stream, isSeekable)
+    HTTP_MESSAGE_ME(Stream, seek)
+    HTTP_MESSAGE_ME(Stream, rewind)
+    HTTP_MESSAGE_ME(Stream, isWritable)
+    HTTP_MESSAGE_ME(Stream, write)
+    HTTP_MESSAGE_ME(Stream, isReadable)
+    HTTP_MESSAGE_ME(Stream, read)
+    HTTP_MESSAGE_ME(Stream, getContents)
+    HTTP_MESSAGE_ME(Stream, getMetadata)
+    PHP_FE_END
 };
 
 PHP_MINIT_FUNCTION(http_message_stream)
 {
     zend_class_entry ce;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("stream");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(interface, "Stream");
+
     INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "Stream", stream_functions);
 
     HttpMessage_Stream_ce = zend_register_internal_class(&ce);
-    zend_class_implements(HttpMessage_Stream_ce, 1, PsrHttpMessageStreamInterface_ce_ptr);
+    zend_class_implements(HttpMessage_Stream_ce, 1, interface);
 
     /* Properties */
-    zend_declare_property_null(HttpMessage_Stream_ce, ZEND_STRL("stream"), ZEND_ACC_PROTECTED);
+    zend_declare_property_null(HttpMessage_Stream_ce, ZEND_STRL("stream"), ZEND_ACC_PRIVATE);
 
     return SUCCESS;
 }
diff --git a/stream.h b/stream.h
new file mode 100644
index 0000000..fa6d3c2
--- /dev/null
+++ b/stream.h
@@ -0,0 +1,68 @@
+/*
+  +----------------------------------------------------------------------+
+  | HTTP Message PHP extension                                           |
+  | Helper function to work with streams                                 |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 2019 Arnold Daniels                                    |
+  +----------------------------------------------------------------------+
+  | Permission is hereby granted, free of charge, to any person          |
+  | obtaining a copy of this software and associated documentation files |
+  | (the "Software"), to deal in the Software without restriction,       |
+  | including without limitation the rights to use, copy, modify, merge, |
+  | publish, distribute, sublicense, and/or sell copies of the Software, |
+  | and to permit persons to whom the Software is furnished to do so,    |
+  | subject to the following conditions:                                 |
+  |                                                                      |
+  | The above copyright notice and this permission notice shall be       |
+  | included in all copies or substantial portions of the Software.      |
+  |                                                                      |
+  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,      |
+  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF   |
+  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                |
+  | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS  |
+  | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN   |
+  | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN    |
+  | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE     |
+  | SOFTWARE.                                                            |
+  +----------------------------------------------------------------------+
+  | Author: Arnold Daniels <arnold@jasny.net>                            |
+  +----------------------------------------------------------------------+
+*/
+
+#ifndef HTTP_MESSAGE_STREAM_H
+#define HTTP_MESSAGE_STREAM_H
+
+#include "php_streams.h"
+
+static int open_temp_stream(zval *zstream)
+{
+    php_stream *stream = php_stream_open_wrapper("php://temp", "w+", 0, NULL);
+
+    if (stream == NULL) {
+        zend_throw_error(NULL, "Failed to open 'php://temp' stream");
+        return FAILURE;
+    }
+
+    php_stream_to_zval(stream, zstream);
+
+    return SUCCESS;
+}
+
+zend_always_inline static zend_bool string_contains_char(char *haystack, char chr)
+{
+    char *p = strchr(haystack, chr);
+
+    return (p != NULL);
+}
+
+zend_always_inline static zend_bool stream_is_writable(php_stream *stream)
+{
+    return !string_contains_char(stream->mode, 'r') || string_contains_char(stream->mode, '+');
+}
+
+zend_always_inline static zend_bool stream_is_readable(php_stream *stream)
+{
+    return string_contains_char(stream->mode, 'r') || string_contains_char(stream->mode, '+');
+}
+
+#endif //HTTP_MESSAGE_STREAM_H
diff --git a/tests/Emitter/emit_001.phpt b/tests/Emitter/emit_001.phpt
new file mode 100644
index 0000000..32ce78a
--- /dev/null
+++ b/tests/Emitter/emit_001.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Emitter::emit() with an empty response
+--FILE--
+<?php
+$emitter = new HttpMessage\Emitter();
+$response = new HttpMessage\Response();
+
+$emitter->emit($response);
+
+?>
+--EXPECTHEADERS--
+--EXPECT--
diff --git a/tests/Emitter/emit_002.phpt b/tests/Emitter/emit_002.phpt
new file mode 100644
index 0000000..87b0a4b
--- /dev/null
+++ b/tests/Emitter/emit_002.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Emitter::emit() with a response body
+--FILE--
+<?php
+$emitter = new HttpMessage\Emitter();
+$response = new HttpMessage\Response();
+$response->getBody()->write("Hello World");
+
+$emitter->emit($response);
+
+?>
+--EXPECTHEADERS--
+--EXPECT--
+Hello World
diff --git a/tests/Emitter/emit_003.phpt b/tests/Emitter/emit_003.phpt
new file mode 100644
index 0000000..e709216
--- /dev/null
+++ b/tests/Emitter/emit_003.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Emitter::emit() with a response status
+--INI--
+--FILE--
+<?php
+$emitter = new HttpMessage\Emitter();
+$response = (new HttpMessage\Response())
+    ->withStatus(200, "Ok");
+$response->getBody()->write("Hello World");
+
+$emitter->emit($response);
+?>
+--EXPECTHEADERS--
+HTTP/1.1 200 OK
+--EXPECT--
+Hello World
diff --git a/tests/Emitter/emit_004.phpt b/tests/Emitter/emit_004.phpt
new file mode 100644
index 0000000..4f9d8b7
--- /dev/null
+++ b/tests/Emitter/emit_004.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Emitter::emit() with a response with headers
+--FILE--
+<?php
+$emitter = new HttpMessage\Emitter();
+$response = (new HttpMessage\Response())
+    ->withHeader("Content-Length", 14)
+    ->withHeader("Foo", "bar");
+$response->getBody()->write("Page not found");
+
+$emitter->emit($response);
+
+?>
+--EXPECTHEADERS--
+Content-Length: 14
+Foo: bar
+--EXPECT--
+Page not found
diff --git a/tests/Emitter/emit_005.phpt b/tests/Emitter/emit_005.phpt
new file mode 100644
index 0000000..6afbf35
--- /dev/null
+++ b/tests/Emitter/emit_005.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Emitter::emit() with a response with duplicate headers
+--FILE--
+<?php
+$emitter = new HttpMessage\Emitter();
+$response = (new HttpMessage\Response())
+    ->withAddedHeader("X-Message", "This is a")
+    ->withAddedHeader("X-Message", "log message")
+    ->withAddedHeader("X-Message", str_repeat("x", 1000));
+
+$emitter->emit($response);
+
+?>
+--EXPECTHEADERS--
+X-Message: This is a
+X-Message: log message
+X-Message: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+--EXPECT--
diff --git a/tests/Emitter/emit_006.phpt b/tests/Emitter/emit_006.phpt
new file mode 100644
index 0000000..8d3c649
--- /dev/null
+++ b/tests/Emitter/emit_006.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Emitter::emit() with a response with response status
+--FILE--
+<?php
+$emitter = new HttpMessage\Emitter();
+$response = (new HttpMessage\Response())
+    ->withStatus(404)
+    ->withHeader("Foo", "Bar");
+
+$emitter->emit($response);
+
+?>
+--EXPECTHEADERS--
+HTTP/1.1 404 Not Found
+Foo: Bar
+--EXPECT--
diff --git a/tests/Emitter/emit_007.phpt b/tests/Emitter/emit_007.phpt
new file mode 100644
index 0000000..6b430f2
--- /dev/null
+++ b/tests/Emitter/emit_007.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Emitter::emit() with a response with response status and custom reason
+--FILE--
+<?php
+$emitter = new HttpMessage\Emitter();
+$response = (new HttpMessage\Response())
+    ->withProtocolVersion("1.0")
+    ->withStatus(404, "Please go away")
+    ->withHeader("Foo", "Bar");
+
+$emitter->emit($response);
+
+?>
+--EXPECTHEADERS--
+HTTP/1.0 404 Please go away
+Foo: Bar
+--EXPECT--
diff --git a/tests/Factory/createRequest_001.phpt b/tests/Factory/createRequest_001.phpt
new file mode 100644
index 0000000..c13a881
--- /dev/null
+++ b/tests/Factory/createRequest_001.phpt
@@ -0,0 +1,44 @@
+--TEST--
+Factory::createRequest()
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$request = $factory->createRequest('GET', 'https://www.example.com');
+
+var_dump($request);
+
+?>
+--EXPECTF--
+object(HttpMessage\Request)#%d (6) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(3) "GET"
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(5) "https"
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(15) "www.example.com"
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+}
diff --git a/tests/Factory/createRequest_002.phpt b/tests/Factory/createRequest_002.phpt
new file mode 100644
index 0000000..c7ca40e
--- /dev/null
+++ b/tests/Factory/createRequest_002.phpt
@@ -0,0 +1,48 @@
+--TEST--
+Factory::createRequest() with Uri object
+--FILE--
+<?php
+$uri = new HttpMessage\Uri('https://www.example.com');
+
+$factory = new HttpMessage\Factory();
+$request = $factory->createRequest('GET', $uri);
+
+var_dump($request);
+var_dump($request->getUri() === $uri);
+
+?>
+--EXPECTF--
+object(HttpMessage\Request)#%d (6) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(3) "GET"
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(5) "https"
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(15) "www.example.com"
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+}
+bool(true)
diff --git a/tests/Factory/createResponse_001.phpt b/tests/Factory/createResponse_001.phpt
new file mode 100644
index 0000000..8ea4c49
--- /dev/null
+++ b/tests/Factory/createResponse_001.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Factory::createResponse() without arguments
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$response = $factory->createResponse();
+
+var_dump($response);
+
+?>
+--EXPECTF--
+object(HttpMessage\Response)#%d (5) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["statusCode":"HttpMessage\Response":private]=>
+  int(200)
+  ["reasonPhrase":"HttpMessage\Response":private]=>
+  string(2) "OK"
+}
diff --git a/tests/Factory/createResponse_002.phpt b/tests/Factory/createResponse_002.phpt
new file mode 100644
index 0000000..bac0c42
--- /dev/null
+++ b/tests/Factory/createResponse_002.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Factory::createResponse() with status code
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+$response = $factory->createResponse(400);
+var_dump($response);
+
+var_dump($factory->createResponse(100)->getStatusCode());
+var_dump($factory->createResponse(100)->getReasonPhrase());
+var_dump($factory->createResponse(200)->getReasonPhrase());
+var_dump($factory->createResponse(404)->getReasonPhrase());
+
+?>
+--EXPECTF--
+object(HttpMessage\Response)#%d (5) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["statusCode":"HttpMessage\Response":private]=>
+  int(400)
+  ["reasonPhrase":"HttpMessage\Response":private]=>
+  string(11) "Bad Request"
+}
+int(100)
+string(8) "Continue"
+string(2) "OK"
+string(9) "Not Found"
diff --git a/tests/Factory/createResponse_003.phpt b/tests/Factory/createResponse_003.phpt
new file mode 100644
index 0000000..3018474
--- /dev/null
+++ b/tests/Factory/createResponse_003.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Factory::createResponse() with status code and reason phrase
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+$response = $factory->createResponse(400, "Foo Bar");
+var_dump($response);
+
+?>
+--EXPECTF--
+object(HttpMessage\Response)#%d (5) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["statusCode":"HttpMessage\Response":private]=>
+  int(400)
+  ["reasonPhrase":"HttpMessage\Response":private]=>
+  string(7) "Foo Bar"
+}
\ No newline at end of file
diff --git a/tests/Factory/createResponse_004.phpt b/tests/Factory/createResponse_004.phpt
new file mode 100644
index 0000000..b02e157
--- /dev/null
+++ b/tests/Factory/createResponse_004.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Factory::createResponse() with unknown status code
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+$response = $factory->createResponse(990);
+var_dump($response);
+
+?>
+--EXPECTF--
+object(HttpMessage\Response)#%d (5) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["statusCode":"HttpMessage\Response":private]=>
+  int(990)
+  ["reasonPhrase":"HttpMessage\Response":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Factory/createResponse_err01.phpt b/tests/Factory/createResponse_err01.phpt
new file mode 100644
index 0000000..5c741ec
--- /dev/null
+++ b/tests/Factory/createResponse_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Factory::createResponse() with invalid arguments
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createResponse('ok');
+} catch (TypeError $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    $factory->createResponse(200, ['foo', 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+%sttpMessage\Factory::createResponse()%sint, string given
+%sttpMessage\Factory::createResponse()%sstring, array given
diff --git a/tests/Factory/createResponse_err02.phpt b/tests/Factory/createResponse_err02.phpt
new file mode 100644
index 0000000..cc262bc
--- /dev/null
+++ b/tests/Factory/createResponse_err02.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Factory::createResponse() with invalid status code
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createResponse(-1);
+} catch (InvalidArgumentException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Invalid HTTP status code -1
\ No newline at end of file
diff --git a/tests/Factory/createServerRequest_001.phpt b/tests/Factory/createServerRequest_001.phpt
new file mode 100644
index 0000000..f25134a
--- /dev/null
+++ b/tests/Factory/createServerRequest_001.phpt
@@ -0,0 +1,80 @@
+--TEST--
+Factory::createServerRequest()
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$request = $factory->createServerRequest('GET', 'https://www.example.com');
+
+var_dump($request);
+
+?>
+--EXPECTF--
+object(HttpMessage\ServerRequest)#%d (14) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(0) ""
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+  ["serverParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["cookieParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["queryParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["uploadedFiles":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["parsedBody":"HttpMessage\ServerRequest":private]=>
+  NULL
+  ["attributes":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["method"]=>
+  string(3) "GET"
+  ["uri"]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(5) "https"
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(15) "www.example.com"
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+}
\ No newline at end of file
diff --git a/tests/Factory/createServerRequest_002.phpt b/tests/Factory/createServerRequest_002.phpt
new file mode 100644
index 0000000..a409e72
--- /dev/null
+++ b/tests/Factory/createServerRequest_002.phpt
@@ -0,0 +1,84 @@
+--TEST--
+Factory::createServerRequest() with Uri object
+--FILE--
+<?php
+$uri = new HttpMessage\Uri('https://www.example.com');
+
+$factory = new HttpMessage\Factory();
+$request = $factory->createServerRequest('GET', $uri);
+
+var_dump($request);
+var_dump($request->getUri() === $uri);
+
+?>
+--EXPECTF--
+object(HttpMessage\ServerRequest)#%d (14) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(0) ""
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+  ["serverParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["cookieParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["queryParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["uploadedFiles":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["parsedBody":"HttpMessage\ServerRequest":private]=>
+  NULL
+  ["attributes":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["method"]=>
+  string(3) "GET"
+  ["uri"]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(5) "https"
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(15) "www.example.com"
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+}
+bool(false)
\ No newline at end of file
diff --git a/tests/Factory/createServerRequest_003.phpt b/tests/Factory/createServerRequest_003.phpt
new file mode 100644
index 0000000..e8a2243
--- /dev/null
+++ b/tests/Factory/createServerRequest_003.phpt
@@ -0,0 +1,150 @@
+--TEST--
+Factory::createServerRequest() with server params
+--FILE--
+<?php
+// Params do not match method and uri.
+$params = [
+    'SERVER_PROTOCOL' => 'HTTP/1.1',
+    'REQUEST_METHOD' => 'POST',
+    'HTTP_HOST' => 'example.com',
+    'REQUEST_URI' => '/foos/?bar=1',
+    'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
+    'CONTENT_LENGTH' => 1324,
+    'HTTP_ACCEPT' => 'text/html',
+    'HTTP_ACCEPT_CHARSET' => 'utf-8',
+    'HTTP_ACCEPT_ENCODING' => 'zip, deflate',
+    'PHP_AUTH_USER' => 'john',
+    'PHP_AUTH_PASS' => 'secret',
+    'SERVER_PORT' => 80,
+];
+
+$factory = new HttpMessage\Factory();
+$request = $factory->createServerRequest('QUERY', 'https://www.example.com/foo/bar/22?q=1', $params);
+
+var_dump($request);
+
+?>
+--EXPECTF--
+object(HttpMessage\ServerRequest)#%d (14) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(6) {
+    ["Host"]=>
+    array(1) {
+      [0]=>
+      string(11) "example.com"
+    }
+    ["Accept"]=>
+    array(1) {
+      [0]=>
+      string(9) "text/html"
+    }
+    ["Accept-Charset"]=>
+    array(1) {
+      [0]=>
+      string(5) "utf-8"
+    }
+    ["Accept-Encoding"]=>
+    array(1) {
+      [0]=>
+      string(12) "zip, deflate"
+    }
+    ["Content-Type"]=>
+    array(1) {
+      [0]=>
+      string(33) "application/x-www-form-urlencoded"
+    }
+    ["Content-Length"]=>
+    array(1) {
+      [0]=>
+      int(1324)
+    }
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(0) ""
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(4) "http"
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(11) "john:secret"
+    ["host":"HttpMessage\Uri":private]=>
+    string(11) "example.com"
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(6) "/foos/"
+    ["query":"HttpMessage\Uri":private]=>
+    string(5) "bar=1"
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+  ["serverParams":"HttpMessage\ServerRequest":private]=>
+  array(12) {
+    ["SERVER_PROTOCOL"]=>
+    string(8) "HTTP/1.1"
+    ["REQUEST_METHOD"]=>
+    string(4) "POST"
+    ["HTTP_HOST"]=>
+    string(11) "example.com"
+    ["REQUEST_URI"]=>
+    string(12) "/foos/?bar=1"
+    ["CONTENT_TYPE"]=>
+    string(33) "application/x-www-form-urlencoded"
+    ["CONTENT_LENGTH"]=>
+    int(1324)
+    ["HTTP_ACCEPT"]=>
+    string(9) "text/html"
+    ["HTTP_ACCEPT_CHARSET"]=>
+    string(5) "utf-8"
+    ["HTTP_ACCEPT_ENCODING"]=>
+    string(12) "zip, deflate"
+    ["PHP_AUTH_USER"]=>
+    string(4) "john"
+    ["PHP_AUTH_PASS"]=>
+    string(6) "secret"
+    ["SERVER_PORT"]=>
+    int(80)
+  }
+  ["cookieParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["queryParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["uploadedFiles":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["parsedBody":"HttpMessage\ServerRequest":private]=>
+  NULL
+  ["attributes":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["method"]=>
+  string(5) "QUERY"
+  ["uri"]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(5) "https"
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(15) "www.example.com"
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(11) "/foo/bar/22"
+    ["query":"HttpMessage\Uri":private]=>
+    string(3) "q=1"
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+}
\ No newline at end of file
diff --git a/tests/Factory/createServerRequest_err01.phpt b/tests/Factory/createServerRequest_err01.phpt
new file mode 100644
index 0000000..004abc4
--- /dev/null
+++ b/tests/Factory/createServerRequest_err01.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Factory::createServerRequest() with invalid arguments
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $request = $factory->createServerRequest();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request = $factory->createServerRequest(42);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request = $factory->createServerRequest("GET", []);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request = $factory->createServerRequest("GET", "http://www.example.com", "hello");
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Factory::createServerRequest() expects at least 2 %s, 0 given
+HttpMessage\Factory::createServerRequest() expects at least 2 %s, 1 given
+HttpMessage\Factory::createServerRequest() expects parameter 1 to be a string or object that implements Psr\Http\Message\UriInterface, array given
+%sttpMessage\Factory::createServerRequest()%sarray, string given
diff --git a/tests/Factory/createStreamFromFile_001.phpt b/tests/Factory/createStreamFromFile_001.phpt
new file mode 100644
index 0000000..e732c28
--- /dev/null
+++ b/tests/Factory/createStreamFromFile_001.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Factory::createStreamFromFile()
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$stream = $factory->createStreamFromFile(__FILE__);
+
+var_dump($stream);
+
+$resource = $stream->detach();
+var_dump(stream_get_meta_data($resource));
+
+fseek($resource, 0);
+var_dump(fread($resource, 16));
+
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+array(9) {
+  ["timed_out"]=>
+  bool(false)
+  ["blocked"]=>
+  bool(true)
+  ["eof"]=>
+  bool(false)
+  ["wrapper_type"]=>
+  string(9) "plainfile"
+  ["stream_type"]=>
+  string(5) "STDIO"
+  ["mode"]=>
+  string(1) "r"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(%d) "%screateStreamFromFile_001.php"
+}
+string(16) "<?php
+$factory ="
\ No newline at end of file
diff --git a/tests/Factory/createStreamFromFile_002.phpt b/tests/Factory/createStreamFromFile_002.phpt
new file mode 100644
index 0000000..9feb45f
--- /dev/null
+++ b/tests/Factory/createStreamFromFile_002.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Factory::createStreamFromFile() with mode
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $stream = $factory->createStreamFromFile([]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream = $factory->createStreamFromFile(__FILE__, []);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+%sttpMessage\Factory::createStreamFromFile()%sstring, array given
+%sttpMessage\Factory::createStreamFromFile()%sstring, array given
diff --git a/tests/Factory/createStreamFromFile_003.phpt b/tests/Factory/createStreamFromFile_003.phpt
new file mode 100644
index 0000000..28a92d1
--- /dev/null
+++ b/tests/Factory/createStreamFromFile_003.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Factory::createStreamFromFile() with PHP stream
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$stream = $factory->createStreamFromFile('php://memory', 'w+');
+
+var_dump($stream);
+
+$resource = $stream->detach();
+var_dump(stream_get_meta_data($resource));
+
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+array(9) {
+  ["timed_out"]=>
+  bool(false)
+  ["blocked"]=>
+  bool(true)
+  ["eof"]=>
+  bool(false)
+  ["wrapper_type"]=>
+  string(3) "PHP"
+  ["stream_type"]=>
+  string(6) "MEMORY"
+  ["mode"]=>
+  string(3) "w+b"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(12) "php://memory"
+}
\ No newline at end of file
diff --git a/tests/Factory/createStreamFromFile_err01.phpt b/tests/Factory/createStreamFromFile_err01.phpt
new file mode 100644
index 0000000..0d8b8f1
--- /dev/null
+++ b/tests/Factory/createStreamFromFile_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Factory::createStreamFromFile() with invalid args
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createStreamFromFile([]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $factory->createStreamFromFile(__FILE__, []);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+%sttpMessage\Factory::createStreamFromFile()%sstring, array given
+%sttpMessage\Factory::createStreamFromFile()%sstring, array given
diff --git a/tests/Factory/createStreamFromFile_err02.phpt b/tests/Factory/createStreamFromFile_err02.phpt
new file mode 100644
index 0000000..ffba733
--- /dev/null
+++ b/tests/Factory/createStreamFromFile_err02.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Factory::createStreamFromFile() with non-existing file
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createStreamFromFile(__DIR__ . '/not/exists');
+} catch (RuntimeException $e) {
+    echo strtr($e->getMessage(), [DIRECTORY_SEPARATOR => '/']), "\n";
+}
+
+?>
+--EXPECTF--
+Failed to open '%s/Factory/not/exists' stream
\ No newline at end of file
diff --git a/tests/Factory/createStreamFromResource_001.phpt b/tests/Factory/createStreamFromResource_001.phpt
new file mode 100644
index 0000000..868e5ec
--- /dev/null
+++ b/tests/Factory/createStreamFromResource_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Factory::createStreamFromResource()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+
+$factory = new HttpMessage\Factory();
+$stream = $factory->createStreamFromResource($resource);
+
+var_dump($stream);
+var_dump($stream->detach() === $resource);
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+bool(true)
\ No newline at end of file
diff --git a/tests/Factory/createStreamFromResource_err01.phpt b/tests/Factory/createStreamFromResource_err01.phpt
new file mode 100644
index 0000000..73ce60c
--- /dev/null
+++ b/tests/Factory/createStreamFromResource_err01.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Factory::createStreamFromResource() with invalid argument
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createStreamFromResource([]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Expected parameter 1 to be a string or resource, array given
\ No newline at end of file
diff --git a/tests/Factory/createStreamFromResource_err02.phpt b/tests/Factory/createStreamFromResource_err02.phpt
new file mode 100644
index 0000000..ce9104f
--- /dev/null
+++ b/tests/Factory/createStreamFromResource_err02.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Factory::createStreamFromResource() with closed stream
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fclose($resource);
+
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createStreamFromResource($resource);
+} catch (InvalidArgumentException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Resource is not a stream
diff --git a/tests/Factory/createStream_001.phpt b/tests/Factory/createStream_001.phpt
new file mode 100644
index 0000000..ae3067e
--- /dev/null
+++ b/tests/Factory/createStream_001.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Factory::createStream()
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$stream = $factory->createStream();
+
+var_dump($stream);
+
+$resource = $stream->detach();
+var_dump(stream_get_meta_data($resource));
+
+fseek($resource, 0);
+var_dump(fread($resource, 1024));
+
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+array(6) {
+  ["wrapper_type"]=>
+  string(3) "PHP"
+  ["stream_type"]=>
+  string(4) "TEMP"
+  ["mode"]=>
+  string(3) "w+b"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(10) "php://temp"
+}
+string(0) ""
\ No newline at end of file
diff --git a/tests/Factory/createStream_002.phpt b/tests/Factory/createStream_002.phpt
new file mode 100644
index 0000000..6a3c160
--- /dev/null
+++ b/tests/Factory/createStream_002.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Factory::createStream() with content
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$stream = $factory->createStream("hello");
+
+var_dump($stream);
+
+$resource = $stream->detach();
+var_dump(stream_get_meta_data($resource));
+
+fseek($resource, 0);
+var_dump(fread($resource, 1024));
+
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+array(6) {
+  ["wrapper_type"]=>
+  string(3) "PHP"
+  ["stream_type"]=>
+  string(4) "TEMP"
+  ["mode"]=>
+  string(3) "w+b"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(10) "php://temp"
+}
+string(5) "hello"
\ No newline at end of file
diff --git a/tests/Factory/createStream_err01.phpt b/tests/Factory/createStream_err01.phpt
new file mode 100644
index 0000000..be23af2
--- /dev/null
+++ b/tests/Factory/createStream_err01.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Factory::createStream() with invalid argument
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createStream([]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+%sttpMessage\Factory::createStream()%sstring, array given
diff --git a/tests/Factory/createUploadedFile_001.phpt b/tests/Factory/createUploadedFile_001.phpt
new file mode 100644
index 0000000..f435dc7
--- /dev/null
+++ b/tests/Factory/createUploadedFile_001.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Factory::createUploadedFile() with a stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream();
+
+$factory = new HttpMessage\Factory();
+$upload = $factory->createUploadedFile($stream);
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+var_dump($upload->getStream() === $stream);
+var_dump($upload->getStream());
+
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["file":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["size":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(0)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+NULL
+int(0)
+NULL
+NULL
+bool(true)
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
\ No newline at end of file
diff --git a/tests/Factory/createUploadedFile_002.phpt b/tests/Factory/createUploadedFile_002.phpt
new file mode 100644
index 0000000..94eb11c
--- /dev/null
+++ b/tests/Factory/createUploadedFile_002.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Factory::createUploadedFile()
+--FILE--
+<?php
+$stream = new HttpMessage\Stream();
+
+$factory = new HttpMessage\Factory();
+$upload = $factory->createUploadedFile($stream, 99, UPLOAD_ERR_OK, 'myfile.txt', 'text/plain');
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+var_dump($upload->getStream() === $stream);
+var_dump($upload->getStream());
+
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["file":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["size":"HttpMessage\UploadedFile":private]=>
+  int(99)
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(0)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  string(10) "myfile.txt"
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  string(10) "text/plain"
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+int(99)
+int(0)
+string(10) "myfile.txt"
+string(10) "text/plain"
+bool(true)
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
\ No newline at end of file
diff --git a/tests/Factory/createUploadedFile_003.phpt b/tests/Factory/createUploadedFile_003.phpt
new file mode 100644
index 0000000..d6ab638
--- /dev/null
+++ b/tests/Factory/createUploadedFile_003.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Factory::createUploadedFile() with a stream and error
+--FILE--
+<?php
+$stream = new HttpMessage\Stream();
+
+$factory = new HttpMessage\Factory();
+$upload = $factory->createUploadedFile($stream, NULL, UPLOAD_ERR_INI_SIZE);
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/uploadedfile');
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["file":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["size":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(1)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+NULL
+int(1)
+NULL
+NULL
\ No newline at end of file
diff --git a/tests/Factory/createUploadedFile_err01.phpt b/tests/Factory/createUploadedFile_err01.phpt
new file mode 100644
index 0000000..0bc72b9
--- /dev/null
+++ b/tests/Factory/createUploadedFile_err01.phpt
@@ -0,0 +1,51 @@
+--TEST--
+Factory::createUploadedFile() with invalid argument
+--FILE--
+<?php
+$stream = new HttpMessage\Stream();
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createUploadedFile('foo');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $factory->createUploadedFile((object)[]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $factory->createUploadedFile($stream, [], UPLOAD_ERR_OK, 'myfile.txt', 'text/plain');
+} catch (TypeError $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    $factory->createUploadedFile($stream, 99, [], 'myfile.txt', 'text/plain');
+} catch (TypeError $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    $factory->createUploadedFile($stream, 99, UPLOAD_ERR_OK, [], 'text/plain');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $factory->createUploadedFile($stream, 99, UPLOAD_ERR_OK, 'myfile.txt', []);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+%sttpMessage\Factory::createUploadedFile()%s Psr\Http\Message\StreamInterface, string given
+%sttpMessage\Factory::createUploadedFile()%s Psr\Http\Message\StreamInterface,%sstdClass given
+%sttpMessage\Factory::createUploadedFile()%s, array given
+%sttpMessage\Factory::createUploadedFile()%sint, array given
+%sttpMessage\Factory::createUploadedFile()%s, array given
+%sttpMessage\Factory::createUploadedFile()%s, array given
diff --git a/tests/Factory/createUploadedFile_err02.phpt b/tests/Factory/createUploadedFile_err02.phpt
new file mode 100644
index 0000000..5065000
--- /dev/null
+++ b/tests/Factory/createUploadedFile_err02.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Factory::createUploadedFile() with non-readable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(sys_get_temp_dir() . '/uploadedfile', 'w');
+$factory = new HttpMessage\Factory();
+
+try {
+    $factory->createUploadedFile($stream);
+} catch (InvalidArgumentException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile')) unlink(sys_get_temp_dir() . '/uploadedfile');
+?>
+--EXPECT--
+Stream provided for uploaded file is not readable
\ No newline at end of file
diff --git a/tests/Factory/createUri_001.phpt b/tests/Factory/createUri_001.phpt
new file mode 100644
index 0000000..2520dd6
--- /dev/null
+++ b/tests/Factory/createUri_001.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Factory::createUri()
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$uri = $factory->createUri();
+
+var_dump($uri);
+
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
+
diff --git a/tests/Factory/createUri_002.phpt b/tests/Factory/createUri_002.phpt
new file mode 100644
index 0000000..4a880e0
--- /dev/null
+++ b/tests/Factory/createUri_002.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Factory::createUri()
+--FILE--
+<?php
+$factory = new HttpMessage\Factory();
+$uri = $factory->createUri("https://example.com/path?foo=1&bar=2#frag");
+
+var_dump($uri);
+
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(5) "https"
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(11) "example.com"
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(5) "/path"
+  ["query":"HttpMessage\Uri":private]=>
+  string(11) "foo=1&bar=2"
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(4) "frag"
+}
+
diff --git a/tests/Message/body_001.phpt b/tests/Message/body_001.phpt
new file mode 100644
index 0000000..8c0d394
--- /dev/null
+++ b/tests/Message/body_001.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Message body
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+$newResponse = $response->withBody($stream);
+
+var_dump(get_class($response->getBody()));
+var_dump($response->getBody()->getMetadata('uri'));
+
+var_dump(get_class($newResponse->getBody()));
+var_dump($newResponse->getBody() === $stream);
+var_dump($newResponse->getBody()->getMetadata('uri'));
+
+?>
+--EXPECT--
+string(18) "HttpMessage\Stream"
+string(10) "php://temp"
+string(18) "HttpMessage\Stream"
+bool(true)
+string(12) "php://memory"
\ No newline at end of file
diff --git a/tests/Message/body_err01.phpt b/tests/Message/body_err01.phpt
new file mode 100644
index 0000000..720ff6a
--- /dev/null
+++ b/tests/Message/body_err01.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Message::withBody() with invalid arguments
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $resource = fopen('php://memory', 'w+');
+    $response->withBody($resource);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withBody();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+%sttpMessage\Message::withBody()%s Psr\Http\Message\StreamInterface, resource given
+HttpMessage\Message::withBody() expects exactly 1 %s, 0 given
diff --git a/tests/Message/headers_001.phpt b/tests/Message/headers_001.phpt
new file mode 100644
index 0000000..67c6a6f
--- /dev/null
+++ b/tests/Message/headers_001.phpt
@@ -0,0 +1,68 @@
+--TEST--
+Message headers
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+$fooResponse = $response->withHeader('Foo', 'bar');
+$moreResponse = $fooResponse->withHeader('More', 'red');
+
+var_dump($response->getHeaders());
+var_dump($response->hasHeader('Foo'));
+var_dump($response->getHeader('Foo'));
+var_dump($response->getHeaderLine('Foo'));
+
+echo "\n";
+var_dump($fooResponse->getHeaders());
+var_dump($fooResponse->hasHeader('Foo'));
+var_dump($fooResponse->getHeader('Foo'));
+var_dump($fooResponse->getHeaderLine('Foo'));
+
+echo "\n";
+var_dump($moreResponse->getHeaders());
+var_dump($moreResponse->hasHeader('More'));
+var_dump($moreResponse->getHeader('More'));
+var_dump($moreResponse->getHeaderLine('More'));
+var_dump($moreResponse->getHeaderLine('Foo'));
+
+?>
+--EXPECT--
+array(0) {
+}
+bool(false)
+array(0) {
+}
+string(0) ""
+
+array(1) {
+  ["Foo"]=>
+  array(1) {
+    [0]=>
+    string(3) "bar"
+  }
+}
+bool(true)
+array(1) {
+  [0]=>
+  string(3) "bar"
+}
+string(3) "bar"
+
+array(2) {
+  ["Foo"]=>
+  array(1) {
+    [0]=>
+    string(3) "bar"
+  }
+  ["More"]=>
+  array(1) {
+    [0]=>
+    string(3) "red"
+  }
+}
+bool(true)
+array(1) {
+  [0]=>
+  string(3) "red"
+}
+string(3) "red"
+string(3) "bar"
\ No newline at end of file
diff --git a/tests/Message/headers_002.phpt b/tests/Message/headers_002.phpt
new file mode 100644
index 0000000..f52ca9d
--- /dev/null
+++ b/tests/Message/headers_002.phpt
@@ -0,0 +1,68 @@
+--TEST--
+Message headers; added header / without header
+--FILE--
+<?php
+
+$foofooResponse = (new HttpMessage\Response())
+    ->withHeader('Foo', 'bar')
+    ->withAddedHeader('Foo', 'baz');
+$fooResponse = $foofooResponse->withHeader('Foo', 'box');
+$response = $foofooResponse->withoutHeader('Foo');
+
+var_dump($foofooResponse->getHeaders());
+var_dump($foofooResponse->hasHeader('Foo'));
+var_dump($foofooResponse->getHeader('Foo'));
+var_dump($foofooResponse->getHeaderLine('Foo'));
+
+echo "\n";
+var_dump($fooResponse->getHeaders());
+var_dump($fooResponse->hasHeader('Foo'));
+var_dump($fooResponse->getHeader('Foo'));
+var_dump($fooResponse->getHeaderLine('Foo'));
+
+echo "\n";
+var_dump($response->getHeaders());
+var_dump($response->hasHeader('Foo'));
+var_dump($response->getHeader('Foo'));
+var_dump($response->getHeaderLine('Foo'));
+
+?>
+--EXPECT--
+array(1) {
+  ["Foo"]=>
+  array(2) {
+    [0]=>
+    string(3) "bar"
+    [1]=>
+    string(3) "baz"
+  }
+}
+bool(true)
+array(2) {
+  [0]=>
+  string(3) "bar"
+  [1]=>
+  string(3) "baz"
+}
+string(8) "bar, baz"
+
+array(1) {
+  ["Foo"]=>
+  array(1) {
+    [0]=>
+    string(3) "box"
+  }
+}
+bool(true)
+array(1) {
+  [0]=>
+  string(3) "box"
+}
+string(3) "box"
+
+array(0) {
+}
+bool(false)
+array(0) {
+}
+string(0) ""
diff --git a/tests/Message/headers_003.phpt b/tests/Message/headers_003.phpt
new file mode 100644
index 0000000..c12d0fe
--- /dev/null
+++ b/tests/Message/headers_003.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Message header with numeric value
+--FILE--
+<?php
+
+$response = (new HttpMessage\Response())
+    ->withHeader('Content-Length', 100);
+
+var_dump($response->getHeaders());
+
+?>
+--EXPECT--
+array(1) {
+  ["Content-Length"]=>
+  array(1) {
+    [0]=>
+    string(3) "100"
+  }
+}
\ No newline at end of file
diff --git a/tests/Message/headers_err01.phpt b/tests/Message/headers_err01.phpt
new file mode 100644
index 0000000..797aff0
--- /dev/null
+++ b/tests/Message/headers_err01.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Message::withHeader() with invalid arguments
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->withHeader('Foo', ['a', 'b']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withHeader(['Foo' => 'bar']);
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withHeader(['Foo' => 'bar'], null);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Message::withHeader()%sstring, array given
+HttpMessage\Message::withHeader() expects exactly 2 %s, 1 given
+HttpMessage\Message::withHeader()%sstring, array given
diff --git a/tests/Message/headers_err02.phpt b/tests/Message/headers_err02.phpt
new file mode 100644
index 0000000..a9e5c9c
--- /dev/null
+++ b/tests/Message/headers_err02.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Message::withAddedHeader() with invalid arguments
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->withAddedHeader('Foo', ['a', 'b']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withAddedHeader(['Foo' => 'bar']);
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withAddedHeader(['Foo' => 'bar'], null);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Message::withAddedHeader()%sstring, array given
+HttpMessage\Message::withAddedHeader() expects exactly 2 %s, 1 given
+HttpMessage\Message::withAddedHeader()%sstring, array given
diff --git a/tests/Message/headers_err03.phpt b/tests/Message/headers_err03.phpt
new file mode 100644
index 0000000..5888ab3
--- /dev/null
+++ b/tests/Message/headers_err03.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Message::withoutHeader() with invalid arguments
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->withoutHeader(['Foo' => 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withoutHeader();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Message::withoutHeader()%sstring, array given
+HttpMessage\Message::withoutHeader() expects exactly 1 %s, 0 given
diff --git a/tests/Message/headers_err04.phpt b/tests/Message/headers_err04.phpt
new file mode 100644
index 0000000..b6a6762
--- /dev/null
+++ b/tests/Message/headers_err04.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Message::getHeader() with invalid arguments
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->getHeader(['Foo' => 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->getHeader();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Message::getHeader()%sstring, array given
+HttpMessage\Message::getHeader() expects exactly 1 %s, 0 given
diff --git a/tests/Message/headers_err05.phpt b/tests/Message/headers_err05.phpt
new file mode 100644
index 0000000..c2f2a58
--- /dev/null
+++ b/tests/Message/headers_err05.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Message::getHeaderLine() with invalid arguments
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->getHeaderLine(['Foo' => 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->getHeaderLine();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Message::getHeaderLine()%sstring, array given
+HttpMessage\Message::getHeaderLine() expects exactly 1 %s, 0 given
diff --git a/tests/Message/protocolVersion_001.phpt b/tests/Message/protocolVersion_001.phpt
new file mode 100644
index 0000000..241c1b1
--- /dev/null
+++ b/tests/Message/protocolVersion_001.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Message::withProtocolVersion()
+--FILE--
+<?php
+$response11 = new HttpMessage\Response();
+$response10 = $response11->withProtocolVersion('1.0');
+$response20 = $response10->withProtocolVersion('2.0');
+
+var_dump($response10->getProtocolVersion());
+var_dump($response11->getProtocolVersion());
+var_dump($response20->getProtocolVersion());
+
+?>
+--EXPECT--
+string(3) "1.0"
+string(3) "1.1"
+string(3) "2.0"
\ No newline at end of file
diff --git a/tests/Message/protocolVersion_002.phpt b/tests/Message/protocolVersion_002.phpt
new file mode 100644
index 0000000..f618671
--- /dev/null
+++ b/tests/Message/protocolVersion_002.phpt
@@ -0,0 +1,11 @@
+--TEST--
+Message::withProtocolVersion() with float
+--FILE--
+<?php
+$response = (new HttpMessage\Response())
+    ->withProtocolVersion(2.0);
+
+var_dump($response->getProtocolVersion());
+?>
+--EXPECT--
+string(1) "2"
\ No newline at end of file
diff --git a/tests/Message/protocolVersion_err01.phpt b/tests/Message/protocolVersion_err01.phpt
new file mode 100644
index 0000000..0f40115
--- /dev/null
+++ b/tests/Message/protocolVersion_err01.phpt
@@ -0,0 +1,21 @@
+--TEST--
+Message::withProtocolVersion() with invalid argument
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->withProtocolVersion();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withProtocolVersion(['foo', 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Message::withProtocolVersion() expects exactly 1 %s, 0 given
+HttpMessage\Message::withProtocolVersion()%sstring, array given
diff --git a/tests/Request/__construct_001.phpt b/tests/Request/__construct_001.phpt
new file mode 100644
index 0000000..e929012
--- /dev/null
+++ b/tests/Request/__construct_001.phpt
@@ -0,0 +1,49 @@
+--TEST--
+Create a Request
+--FILE--
+<?php
+$request = new HttpMessage\Request();
+
+var_dump($request);
+var_dump($request->getRequestTarget());
+var_dump((string)$request->getUri());
+var_dump($request->getBody()->getMetadata('uri'));
+
+?>
+--EXPECTF--
+object(HttpMessage\Request)#%d (6) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(0) ""
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+}
+string(1) "/"
+string(0) ""
+string(10) "php://temp"
\ No newline at end of file
diff --git a/tests/Request/method_001.phpt b/tests/Request/method_001.phpt
new file mode 100644
index 0000000..0cd8c1a
--- /dev/null
+++ b/tests/Request/method_001.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Request::withMethod()
+--FILE--
+<?php
+$request = new HttpMessage\Request();
+$getRequest = $request->withMethod('GET');
+$postRequest = $getRequest->withMethod('POST');
+
+var_dump($request->getMethod());
+var_dump($getRequest->getMethod());
+var_dump($postRequest->getMethod());
+
+?>
+--EXPECT--
+string(0) ""
+string(3) "GET"
+string(4) "POST"
\ No newline at end of file
diff --git a/tests/Request/method_err01.phpt b/tests/Request/method_err01.phpt
new file mode 100644
index 0000000..95c155f
--- /dev/null
+++ b/tests/Request/method_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Request::withMethod() with invalid arguments
+--FILE--
+<?php
+$request = new HttpMessage\Request();
+
+try {
+    $request->withMethod();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request->withMethod(['foo', 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Request::withMethod() expects exactly 1 %s, 0 given
+HttpMessage\Request::withMethod()%sstring, array given
diff --git a/tests/Request/requestTarget_001.phpt b/tests/Request/requestTarget_001.phpt
new file mode 100644
index 0000000..3b50b89
--- /dev/null
+++ b/tests/Request/requestTarget_001.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Request::withRequestTarget()
+--FILE--
+<?php
+$request = new HttpMessage\Request();
+$newRequest = $request->withRequestTarget('/foo');
+
+var_dump($request->getRequestTarget());
+var_dump($newRequest->getRequestTarget());
+
+?>
+--EXPECT--
+string(1) "/"
+string(4) "/foo"
\ No newline at end of file
diff --git a/tests/Request/requestTarget_002.phpt b/tests/Request/requestTarget_002.phpt
new file mode 100644
index 0000000..42fb9aa
--- /dev/null
+++ b/tests/Request/requestTarget_002.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Request::withRequestTarget() using Uri object
+--FILE--
+<?php
+$request = (new HttpMessage\Request())
+    ->withUri(new HttpMessage\Uri('http://www.example.com:80/foo?color=red'));
+
+$fooRequest = $request->withRequestTarget('/bar');
+$defaultRequest = $fooRequest->withRequestTarget(null);
+
+var_dump($request->getRequestTarget());
+var_dump($fooRequest->getRequestTarget());
+var_dump($defaultRequest->getRequestTarget());
+
+?>
+--EXPECT--
+string(14) "/foo?color=red"
+string(4) "/bar"
+string(14) "/foo?color=red"
\ No newline at end of file
diff --git a/tests/Request/requestTarget_err01.phpt b/tests/Request/requestTarget_err01.phpt
new file mode 100644
index 0000000..97f976e
--- /dev/null
+++ b/tests/Request/requestTarget_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Request::withRequestTarget() with invalid arguments
+--FILE--
+<?php
+$request = new HttpMessage\Request();
+
+try {
+    $request->withRequestTarget();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request->withRequestTarget(['foo', 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Request::withRequestTarget() expects exactly 1 %s, 0 given
+HttpMessage\Request::withRequestTarget()%sstring, array given
diff --git a/tests/Request/uri_001.phpt b/tests/Request/uri_001.phpt
new file mode 100644
index 0000000..e44328f
--- /dev/null
+++ b/tests/Request/uri_001.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Request::withUri()
+--FILE--
+<?php
+$uri = new HttpMessage\Uri();
+
+$request = (new HttpMessage\Request());
+$newRequest = $request->withUri($uri);
+
+var_dump($request->getUri() !== $uri);
+var_dump($newRequest->getUri() === $uri);
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
\ No newline at end of file
diff --git a/tests/Request/uri_err01.phpt b/tests/Request/uri_err01.phpt
new file mode 100644
index 0000000..8803d73
--- /dev/null
+++ b/tests/Request/uri_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Request::withUri() with invalid arguments
+--FILE--
+<?php
+$request = new HttpMessage\Request();
+
+try {
+    $request->withUri();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request->withUri('http://www.example.com');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Request::withUri() expects exactly 1 %s, 0 given
+%sttpMessage\Request::withUri()%sPsr\Http\Message\UriInterface, string given
diff --git a/tests/Response/__construct_001.phpt b/tests/Response/__construct_001.phpt
new file mode 100644
index 0000000..a112db0
--- /dev/null
+++ b/tests/Response/__construct_001.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Create a Response
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+var_dump($response);
+var_dump($response->getBody()->getMetadata('uri'));
+?>
+--EXPECTF--
+object(HttpMessage\Response)#%d (5) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["statusCode":"HttpMessage\Response":private]=>
+  int(0)
+  ["reasonPhrase":"HttpMessage\Response":private]=>
+  string(0) ""
+}
+string(10) "php://temp"
\ No newline at end of file
diff --git a/tests/Response/status_001.phpt b/tests/Response/status_001.phpt
new file mode 100644
index 0000000..1ab889a
--- /dev/null
+++ b/tests/Response/status_001.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Response::withStatus()
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+$newResponse = $response->withStatus(200);
+
+var_dump($response->getStatusCode());
+var_dump($newResponse->getStatusCode());
+var_dump($newResponse->getReasonPhrase());
+
+?>
+--EXPECT--
+int(0)
+int(200)
+string(2) "OK"
\ No newline at end of file
diff --git a/tests/Response/status_002.phpt b/tests/Response/status_002.phpt
new file mode 100644
index 0000000..f793d2f
--- /dev/null
+++ b/tests/Response/status_002.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Response::withStatus() with custom reason phrase
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+$newResponse = $response->withStatus(200, "ALL CLEAR");
+var_dump($newResponse->getStatusCode());
+var_dump($newResponse->getReasonPhrase());
+
+?>
+--EXPECT--
+int(200)
+string(9) "ALL CLEAR"
\ No newline at end of file
diff --git a/tests/Response/status_003.phpt b/tests/Response/status_003.phpt
new file mode 100644
index 0000000..f31dfab
--- /dev/null
+++ b/tests/Response/status_003.phpt
@@ -0,0 +1,69 @@
+--TEST--
+Response::withStatus() with all known status codes
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+$codes = [
+    100, 101,
+    200, 201, 202, 203, 204, 205, 206,
+    300, 301, 302, 303, 304, 305, 307, 308,
+    400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 426, 428, 429, 431, 451,
+    500, 501, 502, 503, 504, 505, 506, 511,
+];
+
+foreach ($codes as $code) {
+    $newResponse = $response->withStatus($code);
+    echo $newResponse->getStatusCode(), ' ', $newResponse->getReasonPhrase(), "\n";
+}
+
+?>
+--EXPECT--
+100 Continue
+101 Switching Protocols
+200 OK
+201 Created
+202 Accepted
+203 Non-Authoritative Information
+204 No Content
+205 Reset Content
+206 Partial Content
+300 Multiple Choices
+301 Moved Permanently
+302 Found
+303 See Other
+304 Not Modified
+305 Use Proxy
+307 Temporary Redirect
+308 Permanent Redirect
+400 Bad Request
+401 Unauthorized
+402 Payment Required
+403 Forbidden
+404 Not Found
+405 Method Not Allowed
+406 Not Acceptable
+407 Proxy Authentication Required
+408 Request Timeout
+409 Conflict
+410 Gone
+411 Length Required
+412 Precondition Failed
+413 Request Entity Too Large
+414 Request-URI Too Long
+415 Unsupported Media Type
+416 Requested Range Not Satisfiable
+417 Expectation Failed
+426 Upgrade Required
+428 Precondition Required
+429 Too Many Requests
+431 Request Header Fields Too Large
+451 Unavailable For Legal Reasons
+500 Internal Server Error
+501 Not Implemented
+502 Bad Gateway
+503 Service Unavailable
+504 Gateway Timeout
+505 HTTP Version Not Supported
+506 Variant Also Negotiates
+511 Network Authentication Required
diff --git a/tests/Response/status_004.phpt b/tests/Response/status_004.phpt
new file mode 100644
index 0000000..75e0b0f
--- /dev/null
+++ b/tests/Response/status_004.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Response::withStatus() with an unknown status code
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+$newResponse = $response->withStatus(990);
+var_dump($newResponse->getStatusCode());
+var_dump($newResponse->getReasonPhrase());
+
+?>
+--EXPECT--
+int(990)
+string(0) ""
\ No newline at end of file
diff --git a/tests/Response/status_err01.phpt b/tests/Response/status_err01.phpt
new file mode 100644
index 0000000..75c6a82
--- /dev/null
+++ b/tests/Response/status_err01.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Response::withStatus() with invalid arguments
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->withStatus();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $response->withStatus('ok');
+} catch (TypeError $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    $response->withStatus(200, ['foo', 'bar']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\Response::withStatus() expects at least 1 %s, 0 given
+HttpMessage\Response::withStatus()%sint, string given
+HttpMessage\Response::withStatus()%sstring, array given
diff --git a/tests/Response/status_err02.phpt b/tests/Response/status_err02.phpt
new file mode 100644
index 0000000..f5040de
--- /dev/null
+++ b/tests/Response/status_err02.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Response::withStatus() with invalid status code
+--FILE--
+<?php
+$response = new HttpMessage\Response();
+
+try {
+    $response->withStatus(-1);
+} catch (InvalidArgumentException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Invalid HTTP status code -1
diff --git a/tests/ServerRequest/__construct_001.phpt b/tests/ServerRequest/__construct_001.phpt
new file mode 100644
index 0000000..48a6405
--- /dev/null
+++ b/tests/ServerRequest/__construct_001.phpt
@@ -0,0 +1,60 @@
+--TEST--
+Create a ServerRequest without any arguments
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest();
+
+var_dump($request);
+
+?>
+--EXPECTF--
+object(HttpMessage\ServerRequest)#%d (12) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(0) {
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(0) ""
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+  ["serverParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["cookieParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["queryParams":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["uploadedFiles":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["parsedBody":"HttpMessage\ServerRequest":private]=>
+  NULL
+  ["attributes":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+}
\ No newline at end of file
diff --git a/tests/ServerRequest/__construct_002.phpt b/tests/ServerRequest/__construct_002.phpt
new file mode 100644
index 0000000..e551e9b
--- /dev/null
+++ b/tests/ServerRequest/__construct_002.phpt
@@ -0,0 +1,128 @@
+--TEST--
+Create a ServerRequest with params
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest(
+    [
+        'REQUEST_METHOD' => 'POST',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/',
+    ],
+    ['session' => 999],
+    ['id' => '42', 'persist' => 'yes'],
+    ['id' => 42, 'date' => '2019-01-01', 'categories' => [22, 44]],
+    [
+        'document' => [
+            'tmp_name' => sys_get_temp_dir() . '/uploadedfile',
+            'size' => 2822,
+            'error' => UPLOAD_ERR_OK,
+            'name' => 'document.pdf',
+            'type' => 'application/pdf',
+        ],
+    ]
+);
+
+var_dump($request);
+
+?>
+--EXPECTF--
+object(HttpMessage\ServerRequest)#%d (13) {
+  ["protocolVersion":"HttpMessage\Message":private]=>
+  string(3) "1.1"
+  ["headers":"HttpMessage\Message":private]=>
+  array(1) {
+    ["Host"]=>
+    array(1) {
+      [0]=>
+      string(11) "example.com"
+    }
+  }
+  ["body":"HttpMessage\Message":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["requestTarget":"HttpMessage\Request":private]=>
+  NULL
+  ["method":"HttpMessage\Request":private]=>
+  string(0) ""
+  ["uri":"HttpMessage\Request":private]=>
+  object(HttpMessage\Uri)#%d (7) {
+    ["scheme":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["userInfo":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["host":"HttpMessage\Uri":private]=>
+    string(11) "example.com"
+    ["port":"HttpMessage\Uri":private]=>
+    NULL
+    ["path":"HttpMessage\Uri":private]=>
+    string(6) "/foos/"
+    ["query":"HttpMessage\Uri":private]=>
+    string(0) ""
+    ["fragment":"HttpMessage\Uri":private]=>
+    string(0) ""
+  }
+  ["serverParams":"HttpMessage\ServerRequest":private]=>
+  array(3) {
+    ["REQUEST_METHOD"]=>
+    string(4) "POST"
+    ["HTTP_HOST"]=>
+    string(11) "example.com"
+    ["REQUEST_URI"]=>
+    string(6) "/foos/"
+  }
+  ["cookieParams":"HttpMessage\ServerRequest":private]=>
+  array(1) {
+    ["session"]=>
+    int(999)
+  }
+  ["queryParams":"HttpMessage\ServerRequest":private]=>
+  array(2) {
+    ["id"]=>
+    string(2) "42"
+    ["persist"]=>
+    string(3) "yes"
+  }
+  ["uploadedFiles":"HttpMessage\ServerRequest":private]=>
+  array(1) {
+    ["document"]=>
+    object(HttpMessage\UploadedFile)#%d (8) {
+      ["stream":"HttpMessage\UploadedFile":private]=>
+      NULL
+      ["file":"HttpMessage\UploadedFile":private]=>
+      string(%d) "%suploadedfile"
+      ["size":"HttpMessage\UploadedFile":private]=>
+      int(2822)
+      ["error":"HttpMessage\UploadedFile":private]=>
+      int(0)
+      ["clientFilename":"HttpMessage\UploadedFile":private]=>
+      string(12) "document.pdf"
+      ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+      string(15) "application/pdf"
+      ["moved":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+      ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+    }
+  }
+  ["parsedBody":"HttpMessage\ServerRequest":private]=>
+  array(3) {
+    ["id"]=>
+    int(42)
+    ["date"]=>
+    string(10) "2019-01-01"
+    ["categories"]=>
+    array(2) {
+      [0]=>
+      int(22)
+      [1]=>
+      int(44)
+    }
+  }
+  ["attributes":"HttpMessage\ServerRequest":private]=>
+  array(0) {
+  }
+  ["method"]=>
+  string(4) "POST"
+}
\ No newline at end of file
diff --git a/tests/ServerRequest/__construct_003.phpt b/tests/ServerRequest/__construct_003.phpt
new file mode 100644
index 0000000..e3712b2
--- /dev/null
+++ b/tests/ServerRequest/__construct_003.phpt
@@ -0,0 +1,270 @@
+--TEST--
+Create a ServerRequest with multiple uploaded files
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest(
+    [],
+    [],
+    [],
+    [],
+    [
+        'document' => [
+            'tmp_name' => sys_get_temp_dir() . '/uploadedfile01',
+            'size' => 2822,
+            'error' => UPLOAD_ERR_OK,
+            'name' => 'document.pdf',
+            'type' => 'application/pdf',
+        ],
+        'attachments' => [
+            'tmp_name' => [
+                sys_get_temp_dir() . '/uploadedfile02',
+                sys_get_temp_dir() . '/uploadedfile03',
+            ],
+            'size' => [
+                3724,
+                263,
+            ],
+            'error' => [
+                UPLOAD_ERR_OK,
+                UPLOAD_ERR_OK,
+            ],
+            'name' => [
+                'copy-id.pdf',
+                'proof-of-residence.pdf',
+            ],
+            'type' => [
+                'image/jpg',
+                'application/pdf',
+            ],
+        ],
+        'forms' => [
+            'tmp_name' => [
+                'g201' => sys_get_temp_dir() . '/uploadedfile04',
+                'g202' => [
+                    'a' => sys_get_temp_dir() . '/uploadedfile05',
+                    'c' => sys_get_temp_dir() . '/uploadedfile06',
+                ],
+                'g301' => [
+                    sys_get_temp_dir() . '/uploadedfile07',
+                ],
+            ],
+            'size' => [
+                'g201' => 942,
+                'g202' => [
+                    'a' => 2391,
+                    'c' => 485,
+                ],
+                'g301' => [
+                    4732,
+                    124901432,
+                ],
+            ],
+            'error' => [
+                'g201' => UPLOAD_ERR_OK,
+                'g202' => [
+                    'a' => UPLOAD_ERR_OK,
+                    'b' => UPLOAD_ERR_NO_FILE,
+                    'c' => UPLOAD_ERR_PARTIAL,
+                ],
+                'g301' => [
+                    UPLOAD_ERR_OK,
+                    UPLOAD_ERR_FORM_SIZE,
+                ],
+            ],
+            'name' => [
+            ],
+            'type' => [
+            ],
+        ],
+    ]
+);
+
+var_dump($request->getUploadedFiles());
+
+?>
+--EXPECTF--
+array(3) {
+  ["document"]=>
+  object(HttpMessage\UploadedFile)#%d (8) {
+    ["stream":"HttpMessage\UploadedFile":private]=>
+    NULL
+    ["file":"HttpMessage\UploadedFile":private]=>
+    string(%d) "%suploadedfile01"
+    ["size":"HttpMessage\UploadedFile":private]=>
+    int(2822)
+    ["error":"HttpMessage\UploadedFile":private]=>
+    int(0)
+    ["clientFilename":"HttpMessage\UploadedFile":private]=>
+    string(12) "document.pdf"
+    ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+    string(15) "application/pdf"
+    ["moved":"HttpMessage\UploadedFile":private]=>
+    bool(false)
+    ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+    bool(false)
+  }
+  ["attachments"]=>
+  array(2) {
+    [0]=>
+    object(HttpMessage\UploadedFile)#%d (8) {
+      ["stream":"HttpMessage\UploadedFile":private]=>
+      NULL
+      ["file":"HttpMessage\UploadedFile":private]=>
+      string(%d) "%suploadedfile02"
+      ["size":"HttpMessage\UploadedFile":private]=>
+      int(3724)
+      ["error":"HttpMessage\UploadedFile":private]=>
+      int(0)
+      ["clientFilename":"HttpMessage\UploadedFile":private]=>
+      string(11) "copy-id.pdf"
+      ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+      string(9) "image/jpg"
+      ["moved":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+      ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+    }
+    [1]=>
+    object(HttpMessage\UploadedFile)#%d (8) {
+      ["stream":"HttpMessage\UploadedFile":private]=>
+      NULL
+      ["file":"HttpMessage\UploadedFile":private]=>
+      string(%d) "%suploadedfile03"
+      ["size":"HttpMessage\UploadedFile":private]=>
+      int(263)
+      ["error":"HttpMessage\UploadedFile":private]=>
+      int(0)
+      ["clientFilename":"HttpMessage\UploadedFile":private]=>
+      string(22) "proof-of-residence.pdf"
+      ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+      string(15) "application/pdf"
+      ["moved":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+      ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+    }
+  }
+  ["forms"]=>
+  array(3) {
+    ["g201"]=>
+    object(HttpMessage\UploadedFile)#%d (8) {
+      ["stream":"HttpMessage\UploadedFile":private]=>
+      NULL
+      ["file":"HttpMessage\UploadedFile":private]=>
+      string(%d) "%suploadedfile04"
+      ["size":"HttpMessage\UploadedFile":private]=>
+      int(942)
+      ["error":"HttpMessage\UploadedFile":private]=>
+      int(0)
+      ["clientFilename":"HttpMessage\UploadedFile":private]=>
+      NULL
+      ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+      NULL
+      ["moved":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+      ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+      bool(false)
+    }
+    ["g202"]=>
+    array(3) {
+      ["a"]=>
+      object(HttpMessage\UploadedFile)#%d (8) {
+        ["stream":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["file":"HttpMessage\UploadedFile":private]=>
+        string(%d) "%suploadedfile05"
+        ["size":"HttpMessage\UploadedFile":private]=>
+        int(2391)
+        ["error":"HttpMessage\UploadedFile":private]=>
+        int(0)
+        ["clientFilename":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["moved":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+        ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+      }
+      ["b"]=>
+      object(HttpMessage\UploadedFile)#%d (8) {
+        ["stream":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["file":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["size":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["error":"HttpMessage\UploadedFile":private]=>
+        int(4)
+        ["clientFilename":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["moved":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+        ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+      }
+      ["c"]=>
+      object(HttpMessage\UploadedFile)#%d (8) {
+        ["stream":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["file":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["size":"HttpMessage\UploadedFile":private]=>
+        int(485)
+        ["error":"HttpMessage\UploadedFile":private]=>
+        int(3)
+        ["clientFilename":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["moved":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+        ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+      }
+    }
+    ["g301"]=>
+    array(2) {
+      [0]=>
+      object(HttpMessage\UploadedFile)#%d (8) {
+        ["stream":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["file":"HttpMessage\UploadedFile":private]=>
+        string(%d) "%suploadedfile07"
+        ["size":"HttpMessage\UploadedFile":private]=>
+        int(4732)
+        ["error":"HttpMessage\UploadedFile":private]=>
+        int(0)
+        ["clientFilename":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["moved":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+        ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+      }
+      [1]=>
+      object(HttpMessage\UploadedFile)#%d (8) {
+        ["stream":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["file":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["size":"HttpMessage\UploadedFile":private]=>
+        int(124901432)
+        ["error":"HttpMessage\UploadedFile":private]=>
+        int(2)
+        ["clientFilename":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+        NULL
+        ["moved":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+        ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+        bool(false)
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/tests/ServerRequest/__construct_004.phpt b/tests/ServerRequest/__construct_004.phpt
new file mode 100644
index 0000000..cb2fcaf
--- /dev/null
+++ b/tests/ServerRequest/__construct_004.phpt
@@ -0,0 +1,76 @@
+--TEST--
+Create a ServerRequest with server params
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'REQUEST_METHOD' => 'POST',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
+        'CONTENT_LENGTH' => 1324,
+        'HTTP_ACCEPT' => 'text/html',
+        'HTTP_ACCEPT_CHARSET' => 'utf-8',
+        'HTTP_ACCEPT_ENCODING' => 'zip, deflate',
+        'PHP_AUTH_USER' => 'john',
+        'PHP_AUTH_PASS' => 'secret',
+        'SERVER_PORT' => 80,
+    ]
+);
+
+var_dump($request->getMethod());
+var_dump($request->getUri());
+var_dump($request->getHeaders());
+
+?>
+--EXPECTF--
+string(0) ""
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(4) "http"
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(11) "john:secret"
+  ["host":"HttpMessage\Uri":private]=>
+  string(11) "example.com"
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(6) "/foos/"
+  ["query":"HttpMessage\Uri":private]=>
+  string(5) "bar=1"
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
+array(6) {
+  ["Host"]=>
+  array(1) {
+    [0]=>
+    string(11) "example.com"
+  }
+  ["Accept"]=>
+  array(1) {
+    [0]=>
+    string(9) "text/html"
+  }
+  ["Accept-Charset"]=>
+  array(1) {
+    [0]=>
+    string(5) "utf-8"
+  }
+  ["Accept-Encoding"]=>
+  array(1) {
+    [0]=>
+    string(12) "zip, deflate"
+  }
+  ["Content-Type"]=>
+  array(1) {
+    [0]=>
+    string(33) "application/x-www-form-urlencoded"
+  }
+  ["Content-Length"]=>
+  array(1) {
+    [0]=>
+    int(1324)
+  }
+}
\ No newline at end of file
diff --git a/tests/ServerRequest/__construct_005.phpt b/tests/ServerRequest/__construct_005.phpt
new file mode 100644
index 0000000..bb1d45e
--- /dev/null
+++ b/tests/ServerRequest/__construct_005.phpt
@@ -0,0 +1,152 @@
+--TEST--
+Create a ServerRequest with server params for uri
+--FILE--
+<?php
+// HTTP
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'SERVER_PORT' => 80,
+    ]
+);
+echo $request->getUri(), "\n";
+
+// HTTP (explicit)
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTPS' => 'off',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'SERVER_PORT' => 80,
+    ]
+);
+echo $request->getUri(), "\n";
+
+// HTTPS
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTPS' => 'on',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'SERVER_PORT' => 443,
+    ]
+);
+echo $request->getUri(), "\n";
+
+// Custom HTTP port
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTPS' => 'off',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'SERVER_PORT' => 8080,
+    ]
+);
+echo $request->getUri(), "\n";
+
+// HTTPS on port 80
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTPS' => 'on',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'SERVER_PORT' => 80,
+    ]
+);
+echo $request->getUri(), "\n";
+
+// No protocol, but default HTTP port
+$request = new HttpMessage\ServerRequest(
+    [
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'SERVER_PORT' => 80,
+    ]
+);
+echo $request->getUri(), "\n";
+
+// No protocol, but default HTTPS port
+$request = new HttpMessage\ServerRequest(
+    [
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'SERVER_PORT' => 80,
+    ]
+);
+echo $request->getUri(), "\n";
+
+// No protocol, no port
+$request = new HttpMessage\ServerRequest(
+    [
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+    ]
+);
+echo $request->getUri(), "\n";
+
+// User
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'PHP_AUTH_USER' => 'john'
+    ]
+);
+echo $request->getUri(), "\n";
+
+// User + pass
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'PHP_AUTH_USER' => 'john',
+        'PHP_AUTH_PASS' => 'secret',
+    ]
+);
+echo $request->getUri(), "\n";
+
+// Only pass (ignored)
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'PHP_AUTH_PASS' => 'secret',
+    ]
+);
+echo $request->getUri(), "\n";
+
+// Query string overwrite
+$request = new HttpMessage\ServerRequest(
+    [
+        'SERVER_PROTOCOL' => 'HTTP/1.1',
+        'HTTP_HOST' => 'example.com',
+        'REQUEST_URI' => '/foos/?bar=1',
+        'QUERY_STRING' => 'bar=99',
+    ]
+);
+echo $request->getUri(), "\n";
+
+
+?>
+--EXPECT--
+http://example.com/foos/?bar=1
+http://example.com/foos/?bar=1
+https://example.com/foos/?bar=1
+http://example.com:8080/foos/?bar=1
+https://example.com:80/foos/?bar=1
+http://example.com/foos/?bar=1
+http://example.com/foos/?bar=1
+//example.com/foos/?bar=1
+http://john@example.com/foos/?bar=1
+http://john:secret@example.com/foos/?bar=1
+http://example.com/foos/?bar=1
+http://example.com/foos/?bar=99
\ No newline at end of file
diff --git a/tests/ServerRequest/__construct_err01.phpt b/tests/ServerRequest/__construct_err01.phpt
new file mode 100644
index 0000000..586d505
--- /dev/null
+++ b/tests/ServerRequest/__construct_err01.phpt
@@ -0,0 +1,49 @@
+--TEST--
+Create ServerRequest with invalid arguments
+--FILE--
+<?php
+
+try {
+    new HttpMessage\ServerRequest('');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\ServerRequest([], '');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\ServerRequest([], [], '');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\ServerRequest([], [], '');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\ServerRequest([], [], [], '');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\ServerRequest([], [], [], [], '');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+%sttpMessage\ServerRequest::__construct()%sarray, string given
+%sttpMessage\ServerRequest::__construct()%sarray, string given
+%sttpMessage\ServerRequest::__construct()%sarray, string given
+%sttpMessage\ServerRequest::__construct()%sarray, string given
+%sttpMessage\ServerRequest::__construct()%sarray, string given
+%sttpMessage\ServerRequest::__construct()%sarray, string given
diff --git a/tests/ServerRequest/attributes_001.phpt b/tests/ServerRequest/attributes_001.phpt
new file mode 100644
index 0000000..2ae8c05
--- /dev/null
+++ b/tests/ServerRequest/attributes_001.phpt
@@ -0,0 +1,52 @@
+--TEST--
+ServerRequest::withAttribute() and ServerRequest::withoutAttribute()
+--FILE--
+<?php
+$request = (new HttpMessage\ServerRequest)
+    ->withAttribute('foo', 'bar')
+    ->withAttribute('colors', ['red', 'green'])
+    ->withAttribute('user', ['id' => 22, 'name' => 'John']);
+
+$newRequest = $request
+    ->withoutAttribute('foo')
+    ->withAttribute('user', ['id' => 32, 'name' => 'Jane']);
+
+var_dump($request->getAttributes());
+var_dump($newRequest->getAttributes());
+
+?>
+--EXPECT--
+array(3) {
+  ["foo"]=>
+  string(3) "bar"
+  ["colors"]=>
+  array(2) {
+    [0]=>
+    string(3) "red"
+    [1]=>
+    string(5) "green"
+  }
+  ["user"]=>
+  array(2) {
+    ["id"]=>
+    int(22)
+    ["name"]=>
+    string(4) "John"
+  }
+}
+array(2) {
+  ["colors"]=>
+  array(2) {
+    [0]=>
+    string(3) "red"
+    [1]=>
+    string(5) "green"
+  }
+  ["user"]=>
+  array(2) {
+    ["id"]=>
+    int(32)
+    ["name"]=>
+    string(4) "Jane"
+  }
+}
\ No newline at end of file
diff --git a/tests/ServerRequest/attributes_002.phpt b/tests/ServerRequest/attributes_002.phpt
new file mode 100644
index 0000000..a453a0d
--- /dev/null
+++ b/tests/ServerRequest/attributes_002.phpt
@@ -0,0 +1,40 @@
+--TEST--
+ServerRequest::getAttribute()
+--FILE--
+<?php
+$request = (new HttpMessage\ServerRequest)
+    ->withAttribute('foo', 'bar')
+    ->withAttribute('colors', ['red', 'green'])
+    ->withAttribute('user', ['id' => 22, 'name' => 'John']);
+
+var_dump($request->getAttributes());
+var_dump($request->getAttribute('colors', []));
+var_dump($request->getAttribute('qux', 42));
+
+?>
+--EXPECT--
+array(3) {
+  ["foo"]=>
+  string(3) "bar"
+  ["colors"]=>
+  array(2) {
+    [0]=>
+    string(3) "red"
+    [1]=>
+    string(5) "green"
+  }
+  ["user"]=>
+  array(2) {
+    ["id"]=>
+    int(22)
+    ["name"]=>
+    string(4) "John"
+  }
+}
+array(2) {
+  [0]=>
+  string(3) "red"
+  [1]=>
+  string(5) "green"
+}
+int(42)
\ No newline at end of file
diff --git a/tests/ServerRequest/attributes_err01.phpt b/tests/ServerRequest/attributes_err01.phpt
new file mode 100644
index 0000000..c66373b
--- /dev/null
+++ b/tests/ServerRequest/attributes_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+ServerRequest::withAttribute() with invalid arguments
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest();
+
+try {
+    $request->withAttribute('foo');
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request->withAttribute(['foo' => 'bar'], 22);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\ServerRequest::withAttribute() expects exactly 2 %s, 1 given
+HttpMessage\ServerRequest::withAttribute()%s string, array given
diff --git a/tests/ServerRequest/cookieParams_001.phpt b/tests/ServerRequest/cookieParams_001.phpt
new file mode 100644
index 0000000..e559659
--- /dev/null
+++ b/tests/ServerRequest/cookieParams_001.phpt
@@ -0,0 +1,30 @@
+--TEST--
+ServerRequest::withCookieParams()
+--FILE--
+<?php
+$request = (new HttpMessage\ServerRequest)
+    ->withCookieParams([
+        'foo' => 'bar',
+        'color' => 'green',
+        'user' => 22,
+    ]);
+
+$newRequest = $request->withCookieParams(['user' => 32]);
+
+var_dump($request->getCookieParams());
+var_dump($newRequest->getCookieParams());
+
+?>
+--EXPECT--
+array(3) {
+  ["foo"]=>
+  string(3) "bar"
+  ["color"]=>
+  string(5) "green"
+  ["user"]=>
+  int(22)
+}
+array(1) {
+  ["user"]=>
+  int(32)
+}
\ No newline at end of file
diff --git a/tests/ServerRequest/cookieParams_err01.phpt b/tests/ServerRequest/cookieParams_err01.phpt
new file mode 100644
index 0000000..1dbe3e6
--- /dev/null
+++ b/tests/ServerRequest/cookieParams_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+ServerRequest::withCookieParams() with invalid arguments
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest();
+
+try {
+    $request->withCookieParams();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request->withCookieParams('foo=bar;');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\ServerRequest::withCookieParams() expects exactly 1 %s, 0 given
+%sttpMessage\ServerRequest::withCookieParams()%s array, string given
diff --git a/tests/ServerRequest/parsedBody_001.phpt b/tests/ServerRequest/parsedBody_001.phpt
new file mode 100644
index 0000000..8b37527
--- /dev/null
+++ b/tests/ServerRequest/parsedBody_001.phpt
@@ -0,0 +1,40 @@
+--TEST--
+ServerRequest::withParsedBody()
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest();
+
+$arrRequest = $request->withParsedBody([
+    'foo' => 'bar',
+    'color' => 'green',
+    'user' => 22,
+]);
+
+$objRequest = $request->withParsedBody((object)[
+    'color' => 'red',
+    'user' => 42,
+]);
+
+$nullRequest = $objRequest->withParsedBody(null);
+
+var_dump($arrRequest->getParsedBody());
+var_dump($objRequest->getParsedBody());
+var_dump($nullRequest->getParsedBody());
+
+?>
+--EXPECTF--
+array(3) {
+  ["foo"]=>
+  string(3) "bar"
+  ["color"]=>
+  string(5) "green"
+  ["user"]=>
+  int(22)
+}
+object(stdClass)#%d (2) {
+  ["color"]=>
+  string(3) "red"
+  ["user"]=>
+  int(42)
+}
+NULL
\ No newline at end of file
diff --git a/tests/ServerRequest/parsedBody_err01.phpt b/tests/ServerRequest/parsedBody_err01.phpt
new file mode 100644
index 0000000..9f5b980
--- /dev/null
+++ b/tests/ServerRequest/parsedBody_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+ServerRequest::withParsedBody() with invalid arguments
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest();
+
+try {
+    $request->withQueryParams();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request->withParsedBody('foo=bar;');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\ServerRequest::withQueryParams() expects exactly 1 %s, 0 given
+HttpMessage\ServerRequest::withParsedBody()%sarray, string given
diff --git a/tests/ServerRequest/queryParams_001.phpt b/tests/ServerRequest/queryParams_001.phpt
new file mode 100644
index 0000000..afe06d7
--- /dev/null
+++ b/tests/ServerRequest/queryParams_001.phpt
@@ -0,0 +1,30 @@
+--TEST--
+ServerRequest::withQueryParams()
+--FILE--
+<?php
+$request = (new HttpMessage\ServerRequest)
+    ->withQueryParams([
+        'foo' => 'bar',
+        'color' => 'green',
+        'user' => 22,
+    ]);
+
+$newRequest = $request->withQueryParams(['user' => 32]);
+
+var_dump($request->getQueryParams());
+var_dump($newRequest->getQueryParams());
+
+?>
+--EXPECT--
+array(3) {
+  ["foo"]=>
+  string(3) "bar"
+  ["color"]=>
+  string(5) "green"
+  ["user"]=>
+  int(22)
+}
+array(1) {
+  ["user"]=>
+  int(32)
+}
\ No newline at end of file
diff --git a/tests/ServerRequest/queryParams_err01.phpt b/tests/ServerRequest/queryParams_err01.phpt
new file mode 100644
index 0000000..a7dd760
--- /dev/null
+++ b/tests/ServerRequest/queryParams_err01.phpt
@@ -0,0 +1,22 @@
+--TEST--
+ServerRequest::withQueryParams() with invalid arguments
+--FILE--
+<?php
+$request = new HttpMessage\ServerRequest();
+
+try {
+    $request->withQueryParams();
+} catch (ArgumentCountError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $request->withQueryParams('foo=bar;');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\ServerRequest::withQueryParams() expects exactly 1 %s, 0 given
+%sttpMessage\ServerRequest::withQueryParams()%s array, string given
diff --git a/tests/Stream/__construct_001.phpt b/tests/Stream/__construct_001.phpt
new file mode 100644
index 0000000..4b3cf55
--- /dev/null
+++ b/tests/Stream/__construct_001.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Create Stream without arguments
+--FILE--
+<?php
+$stream = new HttpMessage\Stream();
+var_dump($stream);
+
+$resource = $stream->detach();
+var_dump(stream_get_meta_data($resource));
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+array(6) {
+  ["wrapper_type"]=>
+  string(3) "PHP"
+  ["stream_type"]=>
+  string(4) "TEMP"
+  ["mode"]=>
+  string(3) "w+b"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(10) "php://temp"
+}
\ No newline at end of file
diff --git a/tests/Stream/__construct_002.phpt b/tests/Stream/__construct_002.phpt
new file mode 100644
index 0000000..581334c
--- /dev/null
+++ b/tests/Stream/__construct_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Create Stream with resource
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+
+$stream = new HttpMessage\Stream($resource);
+var_dump($stream);
+var_dump($stream->detach() === $resource);
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+bool(true)
\ No newline at end of file
diff --git a/tests/Stream/__construct_003.phpt b/tests/Stream/__construct_003.phpt
new file mode 100644
index 0000000..8fde2aa
--- /dev/null
+++ b/tests/Stream/__construct_003.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Create Stream with file name
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(__FILE__);
+var_dump($stream);
+
+$resource = $stream->detach();
+var_dump(stream_get_meta_data($resource));
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+array(9) {
+  ["timed_out"]=>
+  bool(false)
+  ["blocked"]=>
+  bool(true)
+  ["eof"]=>
+  bool(false)
+  ["wrapper_type"]=>
+  string(9) "plainfile"
+  ["stream_type"]=>
+  string(5) "STDIO"
+  ["mode"]=>
+  string(1) "r"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(%d) "%s__construct_003.php"
+}
\ No newline at end of file
diff --git a/tests/Stream/__construct_004.phpt b/tests/Stream/__construct_004.phpt
new file mode 100644
index 0000000..2634434
--- /dev/null
+++ b/tests/Stream/__construct_004.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Create Stream with PHP stream name and mode
+--FILE--
+<?php
+$stream = new HttpMessage\Stream('php://memory', 'w+');
+var_dump($stream);
+
+$resource = $stream->detach();
+var_dump(stream_get_meta_data($resource));
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+array(9) {
+  ["timed_out"]=>
+  bool(false)
+  ["blocked"]=>
+  bool(true)
+  ["eof"]=>
+  bool(false)
+  ["wrapper_type"]=>
+  string(3) "PHP"
+  ["stream_type"]=>
+  string(6) "MEMORY"
+  ["mode"]=>
+  string(3) "w+b"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(12) "php://memory"
+}
\ No newline at end of file
diff --git a/tests/Stream/__construct_err01.phpt b/tests/Stream/__construct_err01.phpt
new file mode 100644
index 0000000..ddf21d1
--- /dev/null
+++ b/tests/Stream/__construct_err01.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Create Stream with closed stream
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fclose($resource);
+
+try {
+    new HttpMessage\Stream($resource);
+} catch (InvalidArgumentException $e) {
+    echo $e->getMessage();
+}
+?>
+--EXPECT--
+Resource is not a stream
diff --git a/tests/Stream/__construct_err02.phpt b/tests/Stream/__construct_err02.phpt
new file mode 100644
index 0000000..e75a21d
--- /dev/null
+++ b/tests/Stream/__construct_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Create Stream with invalid argument
+--FILE--
+<?php
+try {
+    new HttpMessage\Stream(['abc']);
+} catch (TypeError $e) {
+    echo $e->getMessage();
+}
+?>
+--EXPECT--
+Expected parameter 1 to be a string or resource, array given
diff --git a/tests/Stream/__construct_err03.phpt b/tests/Stream/__construct_err03.phpt
new file mode 100644
index 0000000..979d8e8
--- /dev/null
+++ b/tests/Stream/__construct_err03.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Create Stream with non-existing file
+--FILE--
+<?php
+try {
+    new HttpMessage\Stream(__DIR__ . '/not/exists', 'r');
+} catch (RuntimeException $e) {
+    echo strtr($e->getMessage(), [DIRECTORY_SEPARATOR => '/']);
+}
+?>
+--EXPECTF--
+Failed to open '%s/Stream/not/exists' stream
diff --git a/tests/Stream/__toString_001.phpt b/tests/Stream/__toString_001.phpt
new file mode 100644
index 0000000..fb036a9
--- /dev/null
+++ b/tests/Stream/__toString_001.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Cast Stream to string
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abcdefghijklmnopqrstuvwxyz");
+
+$stream = new HttpMessage\Stream($resource);
+
+var_dump((string)$stream);
+var_dump((string)$stream);
+
+fread($resource, 10);
+var_dump((string)$stream);
+?>
+--EXPECT--
+string(26) "abcdefghijklmnopqrstuvwxyz"
+string(26) "abcdefghijklmnopqrstuvwxyz"
+string(26) "abcdefghijklmnopqrstuvwxyz"
diff --git a/tests/Stream/__toString_002.phpt b/tests/Stream/__toString_002.phpt
new file mode 100644
index 0000000..1cbcb2e
--- /dev/null
+++ b/tests/Stream/__toString_002.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Cast unreadable Stream to string from non-readable stream
+--FILE--
+<?php
+$resource = fopen(sys_get_temp_dir() . '/nonreadable.tmp', "a");
+$stream = new HttpMessage\Stream($resource);
+
+var_dump((string)$stream);
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/nonreadable.tmp');
+?>
+--EXPECT--
+string(0) ""
diff --git a/tests/Stream/__toString_003.phpt b/tests/Stream/__toString_003.phpt
new file mode 100644
index 0000000..e4fabb9
--- /dev/null
+++ b/tests/Stream/__toString_003.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Cast unreadable Stream to string from file
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/nonreadable.tmp';
+file_put_contents($path, "foo");
+
+$resource = fopen($path, 'r');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump((string)$stream);
+
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/nonreadable.tmp');
+?>
+--EXPECT--
+string(3) "foo"
\ No newline at end of file
diff --git a/tests/Stream/close_001.phpt b/tests/Stream/close_001.phpt
new file mode 100644
index 0000000..a508ad6
--- /dev/null
+++ b/tests/Stream/close_001.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Stream::close()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+$stream->close();
+var_dump($stream);
+
+var_dump(get_resource_type($resource));
+
+$stream->close(); // No error
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (Unknown)
+}
+string(7) "Unknown"
\ No newline at end of file
diff --git a/tests/Stream/close_002.phpt b/tests/Stream/close_002.phpt
new file mode 100644
index 0000000..e43fc46
--- /dev/null
+++ b/tests/Stream/close_002.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Stream::close() on already closed
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+fclose($resource);
+var_dump($stream);
+
+$stream->close();
+var_dump($stream);
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (Unknown)
+}
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (Unknown)
+}
\ No newline at end of file
diff --git a/tests/Stream/close_003.phpt b/tests/Stream/close_003.phpt
new file mode 100644
index 0000000..ad97612
--- /dev/null
+++ b/tests/Stream/close_003.phpt
@@ -0,0 +1,72 @@
+--TEST--
+Stream::close()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+$stream->close();
+
+try {
+    $stream->tell();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->seek(1);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->rewind();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->read(1);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->getContents();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->write("abc");
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+var_dump((string)$stream);
+var_dump($stream->getSize());
+
+var_dump($stream->eof());
+var_dump($stream->isSeekable());
+var_dump($stream->isWritable());
+var_dump($stream->isReadable());
+
+var_dump($stream->getMetadata());
+var_dump($stream->getMetadata('uri'));
+?>
+--EXPECT--
+Stream is closed
+Stream is closed
+Stream is closed
+Stream is closed
+Stream is closed
+Stream is closed
+string(0) ""
+NULL
+bool(true)
+bool(false)
+bool(false)
+bool(false)
+array(0) {
+}
+NULL
diff --git a/tests/Stream/detach_001.phpt b/tests/Stream/detach_001.phpt
new file mode 100644
index 0000000..934ce1e
--- /dev/null
+++ b/tests/Stream/detach_001.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Stream::detach()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+$ret = $stream->detach();
+var_dump($stream);
+
+var_dump($ret === $resource);
+var_dump(get_resource_type($resource));
+
+var_dump($stream->detach());
+
+$stream->close(); // Calling close() while detached should be ignored
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  NULL
+}
+bool(true)
+string(6) "stream"
+NULL
\ No newline at end of file
diff --git a/tests/Stream/detach_002.phpt b/tests/Stream/detach_002.phpt
new file mode 100644
index 0000000..ae25eea
--- /dev/null
+++ b/tests/Stream/detach_002.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Stream::detach() call all methods
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abc");
+$stream = new HttpMessage\Stream($resource);
+
+$stream->detach();
+
+try {
+    $stream->tell();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->seek(1);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->rewind();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+var_dump($stream->getSize());
+
+var_dump($stream->eof());
+var_dump($stream->isSeekable());
+var_dump($stream->isWritable());
+var_dump($stream->isReadable());
+
+var_dump($stream->getMetadata());
+var_dump($stream->getMetadata('uri'));
+?>
+--EXPECT--
+Stream is detached
+Stream is detached
+Stream is detached
+NULL
+bool(true)
+bool(false)
+bool(false)
+bool(false)
+array(0) {
+}
+NULL
diff --git a/tests/Stream/detach_003.phpt b/tests/Stream/detach_003.phpt
new file mode 100644
index 0000000..81a50a2
--- /dev/null
+++ b/tests/Stream/detach_003.phpt
@@ -0,0 +1,73 @@
+--TEST--
+Stream::detach() call all methods
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abc");
+$stream = new HttpMessage\Stream($resource);
+
+$stream->detach();
+
+try {
+    $stream->tell();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->seek(1);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->rewind();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->read(1);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->getContents();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    $stream->write("abc");
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+var_dump((string)$stream);
+var_dump($stream->getSize());
+
+var_dump($stream->eof());
+var_dump($stream->isSeekable());
+var_dump($stream->isWritable());
+var_dump($stream->isReadable());
+
+var_dump($stream->getMetadata());
+var_dump($stream->getMetadata('uri'));
+?>
+--EXPECT--
+Stream is detached
+Stream is detached
+Stream is detached
+Stream is detached
+Stream is detached
+Stream is detached
+string(0) ""
+NULL
+bool(true)
+bool(false)
+bool(false)
+bool(false)
+array(0) {
+}
+NULL
diff --git a/tests/Stream/eof_001.phpt b/tests/Stream/eof_001.phpt
new file mode 100644
index 0000000..4638a3e
--- /dev/null
+++ b/tests/Stream/eof_001.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Stream::eof()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->eof());
+
+fread($resource, 1);
+var_dump($stream->eof());
+
+fwrite($resource, "abcdef");
+var_dump($stream->eof());
+
+fseek($resource, 0);
+var_dump($stream->eof());
+
+fread($resource, 3);
+var_dump($stream->eof());
+
+fread($resource, 4);
+var_dump($stream->eof());
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
+bool(false)
+bool(false)
+bool(true)
diff --git a/tests/Stream/eof_002.phpt b/tests/Stream/eof_002.phpt
new file mode 100644
index 0000000..b3e099b
--- /dev/null
+++ b/tests/Stream/eof_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Stream::eof() call all methods
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->eof());
+
+fclose($resource);
+var_dump($stream->eof());
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
diff --git a/tests/Stream/eof_003.phpt b/tests/Stream/eof_003.phpt
new file mode 100644
index 0000000..a01a589
--- /dev/null
+++ b/tests/Stream/eof_003.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Stream::eof()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->eof());
+
+$stream->detach();
+var_dump($stream->eof());
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
diff --git a/tests/Stream/getContents_001.phpt b/tests/Stream/getContents_001.phpt
new file mode 100644
index 0000000..37e497d
--- /dev/null
+++ b/tests/Stream/getContents_001.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Stream::getContents()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abcdefghijklmnopqrstuvwxyz");
+fseek($resource, 0);
+
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->getContents());
+var_dump($stream->getContents());
+?>
+--EXPECT--
+string(26) "abcdefghijklmnopqrstuvwxyz"
+string(0) ""
diff --git a/tests/Stream/getContents_002.phpt b/tests/Stream/getContents_002.phpt
new file mode 100644
index 0000000..c2f5a13
--- /dev/null
+++ b/tests/Stream/getContents_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Stream::getContents()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abcdefghijklmnopqrstuvwxyz");
+fseek($resource, 10);
+
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->getContents());
+var_dump($stream->getContents());
+?>
+--EXPECT--
+string(16) "klmnopqrstuvwxyz"
+string(0) ""
diff --git a/tests/Stream/getContents_err01.phpt b/tests/Stream/getContents_err01.phpt
new file mode 100644
index 0000000..50df873
--- /dev/null
+++ b/tests/Stream/getContents_err01.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Stream::getContents() with non-readable stream
+--FILE--
+<?php
+$resource = fopen(sys_get_temp_dir() . '/nonreadable.tmp', "a");
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->getContents();
+} catch (RuntimeException $e) {
+    echo $e->getMessage();
+}
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/nonreadable.tmp');
+?>
+--EXPECT--
+Stream not readable
diff --git a/tests/Stream/getMetadata_001.phpt b/tests/Stream/getMetadata_001.phpt
new file mode 100644
index 0000000..b3bc0bf
--- /dev/null
+++ b/tests/Stream/getMetadata_001.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Stream::getMetadata()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->getMetadata());
+
+?>
+--EXPECT--
+array(9) {
+  ["timed_out"]=>
+  bool(false)
+  ["blocked"]=>
+  bool(true)
+  ["eof"]=>
+  bool(false)
+  ["wrapper_type"]=>
+  string(3) "PHP"
+  ["stream_type"]=>
+  string(6) "MEMORY"
+  ["mode"]=>
+  string(3) "w+b"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(12) "php://memory"
+}
diff --git a/tests/Stream/getMetadata_002.phpt b/tests/Stream/getMetadata_002.phpt
new file mode 100644
index 0000000..18f5e2d
--- /dev/null
+++ b/tests/Stream/getMetadata_002.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Stream::getMetadata() with key
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->getMetadata('uri'));
+var_dump($stream->getMetadata('blocked'));
+
+var_dump($stream->getMetadata('not_exist'));
+
+?>
+--EXPECT--
+string(12) "php://memory"
+bool(true)
+NULL
diff --git a/tests/Stream/getMetadata_003.phpt b/tests/Stream/getMetadata_003.phpt
new file mode 100644
index 0000000..2a33d28
--- /dev/null
+++ b/tests/Stream/getMetadata_003.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Stream::getMetadata()
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(__FILE__, 'r');
+
+var_dump($stream->getMetadata());
+
+?>
+--EXPECTF--
+array(9) {
+  ["timed_out"]=>
+  bool(false)
+  ["blocked"]=>
+  bool(true)
+  ["eof"]=>
+  bool(false)
+  ["wrapper_type"]=>
+  string(9) "plainfile"
+  ["stream_type"]=>
+  string(5) "STDIO"
+  ["mode"]=>
+  string(1) "r"
+  ["unread_bytes"]=>
+  int(0)
+  ["seekable"]=>
+  bool(true)
+  ["uri"]=>
+  string(%d) "%sgetMetadata_003.php"
+}
diff --git a/tests/Stream/getSize_001.phpt b/tests/Stream/getSize_001.phpt
new file mode 100644
index 0000000..9f9e7eb
--- /dev/null
+++ b/tests/Stream/getSize_001.phpt
@@ -0,0 +1,31 @@
+--TEST--
+Stream::getSize()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->getSize());
+
+fwrite($resource, "abcdef");
+var_dump($stream->getSize());
+
+fwrite($resource, "ghi");
+var_dump($stream->getSize());
+
+fseek($resource, 0);
+var_dump($stream->getSize());
+
+fwrite($resource, "1234567890abcdef");
+var_dump($stream->getSize());
+
+fclose($resource);
+var_dump($stream->getSize());
+?>
+--EXPECT--
+int(0)
+int(6)
+int(9)
+int(9)
+int(16)
+NULL
diff --git a/tests/Stream/isReadable_001.phpt b/tests/Stream/isReadable_001.phpt
new file mode 100644
index 0000000..8208aa9
--- /dev/null
+++ b/tests/Stream/isReadable_001.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Stream::isReadable() with readable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(fopen(__FILE__, 'r'));
+var_dump($stream->isReadable());
+?>
+--EXPECT--
+bool(true)
diff --git a/tests/Stream/isReadable_002.phpt b/tests/Stream/isReadable_002.phpt
new file mode 100644
index 0000000..62f21fe
--- /dev/null
+++ b/tests/Stream/isReadable_002.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Stream::isReadable() with non-readable stream
+--FILE--
+<?php
+$resource = fopen(sys_get_temp_dir() . '/isreadable.tmp', "a");
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->isReadable());
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/isreadable.tmp');
+?>
+--EXPECT--
+bool(false)
diff --git a/tests/Stream/isSeekable_001.phpt b/tests/Stream/isSeekable_001.phpt
new file mode 100644
index 0000000..30eb9ac
--- /dev/null
+++ b/tests/Stream/isSeekable_001.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Stream::isSeekable() with seekable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(fopen('php://memory', 'r'));
+var_dump($stream->isSeekable());
+?>
+--EXPECT--
+bool(true)
diff --git a/tests/Stream/isSeekable_002.phpt b/tests/Stream/isSeekable_002.phpt
new file mode 100644
index 0000000..9b477fa
--- /dev/null
+++ b/tests/Stream/isSeekable_002.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Stream::isSeekable() with unseekable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(fopen('php://stdin', 'r'));
+var_dump($stream->isSeekable());
+?>
+--EXPECT--
+bool(false)
diff --git a/tests/Stream/isWritable_001.phpt b/tests/Stream/isWritable_001.phpt
new file mode 100644
index 0000000..4fdb095
--- /dev/null
+++ b/tests/Stream/isWritable_001.phpt
@@ -0,0 +1,11 @@
+--TEST--
+Stream::isWritable() with writable stream
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->isWritable());
+?>
+--EXPECT--
+bool(true)
diff --git a/tests/Stream/isWritable_002.phpt b/tests/Stream/isWritable_002.phpt
new file mode 100644
index 0000000..a6b38c0
--- /dev/null
+++ b/tests/Stream/isWritable_002.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Stream::isWritable() with non-writable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(fopen(__FILE__, 'r'));
+var_dump($stream->isWritable());
+?>
+--EXPECT--
+bool(false)
diff --git a/tests/Stream/read_001.phpt b/tests/Stream/read_001.phpt
new file mode 100644
index 0000000..368fff4
--- /dev/null
+++ b/tests/Stream/read_001.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Stream::read()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abcdefghijklmnopqrstuvwxyz");
+fseek($resource, 0);
+
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->read(10));
+var_dump($stream->read(10));
+var_dump($stream->read(10));
+var_dump($stream->read(10));
+?>
+--EXPECT--
+string(10) "abcdefghij"
+string(10) "klmnopqrst"
+string(6) "uvwxyz"
+string(0) ""
diff --git a/tests/Stream/read_err01.phpt b/tests/Stream/read_err01.phpt
new file mode 100644
index 0000000..d2076ff
--- /dev/null
+++ b/tests/Stream/read_err01.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Stream::read() error: no arguments
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->read();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Stream::read() expects exactly 1 %s, 0 given
diff --git a/tests/Stream/read_err02.phpt b/tests/Stream/read_err02.phpt
new file mode 100644
index 0000000..3a32dee
--- /dev/null
+++ b/tests/Stream/read_err02.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Stream::read() err: invalid arguments
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->read('hello');
+} catch (Error $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    $stream->read(-1);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Stream::read()%s int, string given
+Length parameter must be equal or greater than 0
diff --git a/tests/Stream/read_err03.phpt b/tests/Stream/read_err03.phpt
new file mode 100644
index 0000000..4d75943
--- /dev/null
+++ b/tests/Stream/read_err03.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Stream::read() with non-readable stream
+--FILE--
+<?php
+$resource = fopen(sys_get_temp_dir() . '/nonreadable.tmp', "a");
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->read(1);
+} catch (RuntimeException $e) {
+    echo $e->getMessage();
+}
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/nonreadable.tmp');
+?>
+--EXPECT--
+Stream not readable
diff --git a/tests/Stream/rewind_001.phpt b/tests/Stream/rewind_001.phpt
new file mode 100644
index 0000000..66e9a87
--- /dev/null
+++ b/tests/Stream/rewind_001.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Stream::rewind()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abc");
+
+$stream = new HttpMessage\Stream($resource);
+
+var_dump(fread($resource, 4));
+
+$stream->rewind();
+var_dump(fread($resource, 4));
+?>
+--EXPECT--
+string(0) ""
+string(3) "abc"
diff --git a/tests/Stream/seek_001.phpt b/tests/Stream/seek_001.phpt
new file mode 100644
index 0000000..1785f3a
--- /dev/null
+++ b/tests/Stream/seek_001.phpt
@@ -0,0 +1,34 @@
+--TEST--
+Stream::seek()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+fwrite($resource, "abcdefghijklmnopqrstuvwxyz");
+
+$stream = new HttpMessage\Stream($resource);
+
+$stream->seek(10);
+var_dump(fread($resource, 64));
+
+$stream->seek(3, SEEK_SET);
+var_dump(fread($resource, 6));
+
+$stream->seek(4, SEEK_CUR);
+var_dump(fread($resource, 6));
+
+$stream->seek(-3, SEEK_CUR);
+var_dump(fread($resource, 6));
+
+$stream->seek(-12, SEEK_END);
+var_dump(fread($resource, 6));
+
+$stream->seek(100);
+var_dump(fread($resource, 6));
+?>
+--EXPECT--
+string(16) "klmnopqrstuvwxyz"
+string(6) "defghi"
+string(6) "nopqrs"
+string(6) "qrstuv"
+string(6) "opqrst"
+string(0) ""
diff --git a/tests/Stream/seek_err01.phpt b/tests/Stream/seek_err01.phpt
new file mode 100644
index 0000000..724eac5
--- /dev/null
+++ b/tests/Stream/seek_err01.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Stream::seek() error: no arguments
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->seek();
+} catch (Error $e) {
+    echo $e->getMessage();
+}
+?>
+--EXPECTF--
+HttpMessage\Stream::seek() expects at least 1 %s, 0 given
diff --git a/tests/Stream/seek_err02.phpt b/tests/Stream/seek_err02.phpt
new file mode 100644
index 0000000..8ab5755
--- /dev/null
+++ b/tests/Stream/seek_err02.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Stream::seek() error: invalid arguments
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->seek('hello');
+} catch (Error $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    $stream->seek(1, 'hello');
+} catch (Error $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Stream::seek()%s int, string given
+HttpMessage\Stream::seek()%s int, string given
diff --git a/tests/Stream/seek_err03.phpt b/tests/Stream/seek_err03.phpt
new file mode 100644
index 0000000..11aeb91
--- /dev/null
+++ b/tests/Stream/seek_err03.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Stream::seek() error: invalid whence
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->seek(1, 999);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Invalid value for whence
\ No newline at end of file
diff --git a/tests/Stream/seek_err04.phpt b/tests/Stream/seek_err04.phpt
new file mode 100644
index 0000000..8502952
--- /dev/null
+++ b/tests/Stream/seek_err04.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Stream::seek() error: unseekable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(fopen('php://stdin', 'r'));
+
+try {
+    $stream->seek(0);
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Stream is not seekable
\ No newline at end of file
diff --git a/tests/Stream/tell_001.phpt b/tests/Stream/tell_001.phpt
new file mode 100644
index 0000000..c0413b6
--- /dev/null
+++ b/tests/Stream/tell_001.phpt
@@ -0,0 +1,27 @@
+--TEST--
+Stream::tell()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+var_dump($stream->tell());
+
+fwrite($resource, "abcdef");
+var_dump($stream->tell());
+
+fwrite($resource, "ghi");
+var_dump($stream->tell());
+
+fseek($resource, 0);
+var_dump($stream->tell());
+
+fwrite($resource, "1234567890abcdef");
+var_dump($stream->tell());
+?>
+--EXPECT--
+int(0)
+int(6)
+int(9)
+int(0)
+int(16)
diff --git a/tests/Stream/write_001.phpt b/tests/Stream/write_001.phpt
new file mode 100644
index 0000000..ec46aab
--- /dev/null
+++ b/tests/Stream/write_001.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Stream::write()
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+
+$stream = new HttpMessage\Stream($resource);
+
+$stream->write('hello');
+$stream->write(' world');
+
+fseek($resource, 0);
+var_dump(fread($resource, 100));
+?>
+--EXPECT--
+string(11) "hello world"
diff --git a/tests/Stream/write_err01.phpt b/tests/Stream/write_err01.phpt
new file mode 100644
index 0000000..4a3a775
--- /dev/null
+++ b/tests/Stream/write_err01.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Stream::write() error: no arguments
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->write();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Stream::write() expects exactly 1 %s, 0 given
diff --git a/tests/Stream/write_err02.phpt b/tests/Stream/write_err02.phpt
new file mode 100644
index 0000000..933b338
--- /dev/null
+++ b/tests/Stream/write_err02.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Stream::read() err: invalid arguments
+--FILE--
+<?php
+$resource = fopen('php://memory', 'w+');
+$stream = new HttpMessage\Stream($resource);
+
+try {
+    $stream->write(['foo' => 'bar']);
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Stream::write()%s string, array given
diff --git a/tests/Stream/write_err03.phpt b/tests/Stream/write_err03.phpt
new file mode 100644
index 0000000..78f70ee
--- /dev/null
+++ b/tests/Stream/write_err03.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Stream::read() with non-readable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(fopen(__FILE__, 'r'));
+
+try {
+    $stream->write('hello');
+} catch (RuntimeException $e) {
+    echo $e->getMessage();
+}
+?>
+--EXPECT--
+Stream not writable
diff --git a/tests/UploadedFile/__construct_001.phpt b/tests/UploadedFile/__construct_001.phpt
new file mode 100644
index 0000000..10bbb10
--- /dev/null
+++ b/tests/UploadedFile/__construct_001.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Create UploadedFile
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path, 99, UPLOAD_ERR_OK, 'myfile.txt', 'text/plain');
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/uploadedfile');
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["file":"HttpMessage\UploadedFile":private]=>
+  string(%d) "%s/uploadedfile"
+  ["size":"HttpMessage\UploadedFile":private]=>
+  int(99)
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(0)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  string(10) "myfile.txt"
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  string(10) "text/plain"
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+int(99)
+int(0)
+string(10) "myfile.txt"
+string(10) "text/plain"
\ No newline at end of file
diff --git a/tests/UploadedFile/__construct_002.phpt b/tests/UploadedFile/__construct_002.phpt
new file mode 100644
index 0000000..8b61383
--- /dev/null
+++ b/tests/UploadedFile/__construct_002.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Create UploadedFile with only a path
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path);
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/uploadedfile');
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["file":"HttpMessage\UploadedFile":private]=>
+  string(%d) "%s/uploadedfile"
+  ["size":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(0)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+NULL
+int(0)
+NULL
+NULL
\ No newline at end of file
diff --git a/tests/UploadedFile/__construct_003.phpt b/tests/UploadedFile/__construct_003.phpt
new file mode 100644
index 0000000..0c28021
--- /dev/null
+++ b/tests/UploadedFile/__construct_003.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Create UploadedFile with path and error
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path, NULL, UPLOAD_ERR_INI_SIZE);
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/uploadedfile');
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["file":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["size":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(1)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+NULL
+int(1)
+NULL
+NULL
\ No newline at end of file
diff --git a/tests/UploadedFile/__construct_004.phpt b/tests/UploadedFile/__construct_004.phpt
new file mode 100644
index 0000000..c995015
--- /dev/null
+++ b/tests/UploadedFile/__construct_004.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Create UploadedFile with only an error
+--FILE--
+<?php
+$upload = new HttpMessage\UploadedFile(null, null, UPLOAD_ERR_NO_FILE, null, null);
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["file":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["size":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(4)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+NULL
+int(4)
+NULL
+NULL
\ No newline at end of file
diff --git a/tests/UploadedFile/__construct_005.phpt b/tests/UploadedFile/__construct_005.phpt
new file mode 100644
index 0000000..af79d5a
--- /dev/null
+++ b/tests/UploadedFile/__construct_005.phpt
@@ -0,0 +1,48 @@
+--TEST--
+Create UploadedFile with a stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream();
+$upload = new HttpMessage\UploadedFile($stream);
+
+var_dump($upload);
+var_dump($upload->getSize());
+var_dump($upload->getError());
+var_dump($upload->getClientFilename());
+var_dump($upload->getClientMediaType());
+
+var_dump($upload->getStream() === $stream);
+var_dump($upload->getStream());
+
+?>
+--EXPECTF--
+object(HttpMessage\UploadedFile)#%d (8) {
+  ["stream":"HttpMessage\UploadedFile":private]=>
+  object(HttpMessage\Stream)#%d (1) {
+    ["stream":"HttpMessage\Stream":private]=>
+    resource(%d) of type (stream)
+  }
+  ["file":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["size":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["error":"HttpMessage\UploadedFile":private]=>
+  int(0)
+  ["clientFilename":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["clientMediaType":"HttpMessage\UploadedFile":private]=>
+  NULL
+  ["moved":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+  ["checkUploaded":"HttpMessage\UploadedFile":private]=>
+  bool(false)
+}
+NULL
+int(0)
+NULL
+NULL
+bool(true)
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
diff --git a/tests/UploadedFile/__construct_err01.phpt b/tests/UploadedFile/__construct_err01.phpt
new file mode 100644
index 0000000..51cf776
--- /dev/null
+++ b/tests/UploadedFile/__construct_err01.phpt
@@ -0,0 +1,50 @@
+--TEST--
+Create UploadedFile with invalid arguments
+--FILE--
+<?php
+$stream = new HttpMessage\Stream();
+
+try {
+    new HttpMessage\UploadedFile([]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\UploadedFile((object)[]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\UploadedFile($stream, [], UPLOAD_ERR_OK, 'myfile.txt', 'text/plain');
+} catch (TypeError $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    new HttpMessage\UploadedFile($stream, 99, [], 'myfile.txt', 'text/plain');
+} catch (TypeError $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+
+try {
+    new HttpMessage\UploadedFile($stream, 99, UPLOAD_ERR_OK, [], 'text/plain');
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    new HttpMessage\UploadedFile($stream, 99, UPLOAD_ERR_OK, 'myfile.txt', []);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECTF--
+HttpMessage\UploadedFile::__construct() expects parameter 1 to be a string or object that implements Psr\Http\Message\StreamInterface, array given
+HttpMessage\UploadedFile::__construct() expects parameter 1 to be a string or object that implements Psr\Http\Message\StreamInterface, %s given
+%sttpMessage\UploadedFile::__construct()%s, array given
+%sttpMessage\UploadedFile::__construct()%s, array given
+%sttpMessage\UploadedFile::__construct()%s, array given
+%sttpMessage\UploadedFile::__construct()%s, array given
diff --git a/tests/UploadedFile/__construct_err02.phpt b/tests/UploadedFile/__construct_err02.phpt
new file mode 100644
index 0000000..94233fc
--- /dev/null
+++ b/tests/UploadedFile/__construct_err02.phpt
@@ -0,0 +1,15 @@
+--TEST--
+Create UploadedFile with a non-readable stream
+--FILE--
+<?php
+$stream = new HttpMessage\Stream(sys_get_temp_dir() . '/uploadedfile', 'w');
+
+try {
+    new HttpMessage\UploadedFile($stream);
+} catch (InvalidArgumentException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+Stream provided for uploaded file is not readable
\ No newline at end of file
diff --git a/tests/UploadedFile/getStream_001.phpt b/tests/UploadedFile/getStream_001.phpt
new file mode 100644
index 0000000..19dbbe6
--- /dev/null
+++ b/tests/UploadedFile/getStream_001.phpt
@@ -0,0 +1,29 @@
+--TEST--
+UploadedFile::getStream()
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path, 99, UPLOAD_ERR_OK, 'myfile.txt', 'text/plain');
+
+$stream = $upload->getStream();
+
+var_dump($stream);
+var_dump($stream->getMetadata('uri'));
+var_dump((string)$stream);
+var_dump($stream == $upload->getStream());
+
+?>
+--CLEAN--
+<?php
+unlink(sys_get_temp_dir() . '/uploadedfile');
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+string(%d) "%s/uploadedfile"
+string(3) "foo"
+bool(true)
\ No newline at end of file
diff --git a/tests/UploadedFile/getStream_002.phpt b/tests/UploadedFile/getStream_002.phpt
new file mode 100644
index 0000000..15d2abc
--- /dev/null
+++ b/tests/UploadedFile/getStream_002.phpt
@@ -0,0 +1,37 @@
+--TEST--
+UploadedFile::getStream() after failed move
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path);
+
+try {
+    @$upload->moveTo(sys_get_temp_dir() . '/nosuchdir/movedfile');
+} catch (RuntimeException $e) {
+    // expected
+}
+
+$stream = $upload->getStream();
+
+var_dump($stream);
+var_dump($stream->getMetadata('uri'));
+var_dump((string)$stream);
+var_dump($stream == $upload->getStream());
+
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile') {
+    unlink(sys_get_temp_dir() . '/uploadedfile');
+}
+?>
+--EXPECTF--
+object(HttpMessage\Stream)#%d (1) {
+  ["stream":"HttpMessage\Stream":private]=>
+  resource(%d) of type (stream)
+}
+string(%d) "%s/uploadedfile"
+string(3) "foo"
+bool(true)
\ No newline at end of file
diff --git a/tests/UploadedFile/getStream_err01.phpt b/tests/UploadedFile/getStream_err01.phpt
new file mode 100644
index 0000000..5364c3e
--- /dev/null
+++ b/tests/UploadedFile/getStream_err01.phpt
@@ -0,0 +1,14 @@
+--TEST--
+UploadedFile::getStream() without a file
+--FILE--
+<?php
+$upload = new HttpMessage\UploadedFile(null, null, UPLOAD_ERR_CANT_WRITE);
+
+try {
+    $upload->getStream();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+No file was uploaded or uploaded file not available
\ No newline at end of file
diff --git a/tests/UploadedFile/getStream_err02.phpt b/tests/UploadedFile/getStream_err02.phpt
new file mode 100644
index 0000000..963655c
--- /dev/null
+++ b/tests/UploadedFile/getStream_err02.phpt
@@ -0,0 +1,27 @@
+--TEST--
+UploadedFile::getStream() after move
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path);
+$upload->moveTo(sys_get_temp_dir() . '/movedfile');
+
+try {
+    $upload->getStream();
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile') {
+    unlink(sys_get_temp_dir() . '/uploadedfile');
+}
+if (file_exists(sys_get_temp_dir() . '/movedfile') {
+    unlink(sys_get_temp_dir() . '/movedfile');
+}
+?>
+--EXPECTF--
+Uploaded file '%s/uploadedfile' has already been moved
diff --git a/tests/UploadedFile/getStream_err03.phpt b/tests/UploadedFile/getStream_err03.phpt
new file mode 100644
index 0000000..1658cd1
--- /dev/null
+++ b/tests/UploadedFile/getStream_err03.phpt
@@ -0,0 +1,14 @@
+--TEST--
+UploadedFile::getStream() with non-existing file
+--FILE--
+<?php
+$upload = new HttpMessage\UploadedFile(__DIR__ . '/not/exist');
+
+try {
+    $upload->getStream();
+} catch (RuntimeException $e) {
+    echo strtr($e->getMessage(), [DIRECTORY_SEPARATOR => '/']), "\n";
+}
+?>
+--EXPECTF--
+Failed to open '%s/UploadedFile/not/exist' stream
\ No newline at end of file
diff --git a/tests/UploadedFile/moveTo_001.phpt b/tests/UploadedFile/moveTo_001.phpt
new file mode 100644
index 0000000..1d62c16
--- /dev/null
+++ b/tests/UploadedFile/moveTo_001.phpt
@@ -0,0 +1,22 @@
+--TEST--
+UploadedFile::getStream() after move
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path);
+
+$target = sys_get_temp_dir() . '/movedfile';
+$upload->moveTo($target);
+
+var_dump(file_get_contents($target));
+
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile') unlink(sys_get_temp_dir() . '/uploadedfile');
+if (file_exists(sys_get_temp_dir() . '/movedfile') unlink(sys_get_temp_dir() . '/movedfile');
+?>
+--EXPECT--
+string(3) "foo"
\ No newline at end of file
diff --git a/tests/UploadedFile/moveTo_002.phpt b/tests/UploadedFile/moveTo_002.phpt
new file mode 100644
index 0000000..7413256
--- /dev/null
+++ b/tests/UploadedFile/moveTo_002.phpt
@@ -0,0 +1,28 @@
+--TEST--
+UploadedFile::moveTo() after failed move
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path);
+
+try {
+    @$upload->moveTo(sys_get_temp_dir() . '/nosuchdir/movedfile');
+} catch (RuntimeException $e) {
+    // expected
+}
+
+$target = sys_get_temp_dir() . '/movedfile';
+$upload->moveTo($target);
+
+var_dump(file_get_contents($target));
+
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile') unlink(sys_get_temp_dir() . '/uploadedfile');
+if (file_exists(sys_get_temp_dir() . '/movedfile') unlink(sys_get_temp_dir() . '/movedfile');
+?>
+--EXPECT--
+string(3) "foo"
\ No newline at end of file
diff --git a/tests/UploadedFile/moveTo_003.phpt b/tests/UploadedFile/moveTo_003.phpt
new file mode 100644
index 0000000..b2022e7
--- /dev/null
+++ b/tests/UploadedFile/moveTo_003.phpt
@@ -0,0 +1,22 @@
+--TEST--
+UploadedFile::moveTo() with stream
+--FILE--
+<?php
+$resource = fopen('php://temp', 'r+');
+fwrite($resource, 'foo');
+
+$stream = new HttpMessage\Stream($resource);
+$upload = new HttpMessage\UploadedFile($stream);
+
+$target = sys_get_temp_dir() . '/movedfile';
+$upload->moveTo($target);
+
+var_dump(file_get_contents($target));
+
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/movedfile') unlink(sys_get_temp_dir() . '/movedfile');
+?>
+--EXPECT--
+string(3) "foo"
\ No newline at end of file
diff --git a/tests/UploadedFile/moveTo_err01.phpt b/tests/UploadedFile/moveTo_err01.phpt
new file mode 100644
index 0000000..955da15
--- /dev/null
+++ b/tests/UploadedFile/moveTo_err01.phpt
@@ -0,0 +1,14 @@
+--TEST--
+UploadedFile::moveTo() without a file
+--FILE--
+<?php
+$upload = new HttpMessage\UploadedFile(null, null, UPLOAD_ERR_CANT_WRITE);
+
+try {
+    $upload->moveTo(sys_get_temp_dir() . '/movedfile');
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+No file was uploaded or uploaded file not available
\ No newline at end of file
diff --git a/tests/UploadedFile/moveTo_err02.phpt b/tests/UploadedFile/moveTo_err02.phpt
new file mode 100644
index 0000000..a88bc92
--- /dev/null
+++ b/tests/UploadedFile/moveTo_err02.phpt
@@ -0,0 +1,30 @@
+--TEST--
+UploadedFile::moveTo() after move
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path);
+$upload->moveTo(sys_get_temp_dir() . '/movedfile');
+
+try {
+    $upload->moveTo(sys_get_temp_dir() . '/other');
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile') {
+    unlink(sys_get_temp_dir() . '/uploadedfile');
+}
+if (file_exists(sys_get_temp_dir() . '/movedfile') {
+    unlink(sys_get_temp_dir() . '/movedfile');
+}
+if (file_exists(sys_get_temp_dir() . '/other') {
+    unlink(sys_get_temp_dir() . '/other');
+}
+?>
+--EXPECTF--
+Uploaded file '%s/uploadedfile' has already been moved
diff --git a/tests/UploadedFile/moveTo_err03.phpt b/tests/UploadedFile/moveTo_err03.phpt
new file mode 100644
index 0000000..46d1ab7
--- /dev/null
+++ b/tests/UploadedFile/moveTo_err03.phpt
@@ -0,0 +1,24 @@
+--TEST--
+UploadedFile::moveTo() invalid target
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path);
+
+try {
+    $upload->moveTo(sys_get_temp_dir() . '/nosuchdir/movedfile');
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile') {
+    unlink(sys_get_temp_dir() . '/uploadedfile');
+}
+?>
+--EXPECTF--
+Warning: HttpMessage\UploadedFile::moveTo(%s/nosuchdir/movedfile): %sailed to open stream: No such file or directory in %smoveTo_err03.php on line %d
+Failed to move uploaded file '%s/uploadedfile' to '%s/nosuchdir/movedfile'
diff --git a/tests/UploadedFile/moveTo_err04.phpt b/tests/UploadedFile/moveTo_err04.phpt
new file mode 100644
index 0000000..e625dc0
--- /dev/null
+++ b/tests/UploadedFile/moveTo_err04.phpt
@@ -0,0 +1,26 @@
+--TEST--
+UploadedFile::moveTo() with is_uploaded check
+--FILE--
+<?php
+$path = sys_get_temp_dir() . '/uploadedfile';
+file_put_contents($path, "foo");
+
+$upload = new HttpMessage\UploadedFile($path, null, UPLOAD_ERR_OK, null, null, true);
+
+try {
+    $upload->moveTo(sys_get_temp_dir() . '/movedfile');
+} catch (RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/uploadedfile') {
+    unlink(sys_get_temp_dir() . '/uploadedfile');
+}
+if (file_exists(sys_get_temp_dir() . '/movedfile') {
+    unlink(sys_get_temp_dir() . '/movedfile');
+}
+?>
+--EXPECTF--
+Won't move '%s/uploadedfile'; not an uploaded file
\ No newline at end of file
diff --git a/tests/UploadedFile/moveTo_err05.phpt b/tests/UploadedFile/moveTo_err05.phpt
new file mode 100644
index 0000000..f4d5807
--- /dev/null
+++ b/tests/UploadedFile/moveTo_err05.phpt
@@ -0,0 +1,22 @@
+--TEST--
+UploadedFile::moveTo() with non-existing file
+--FILE--
+<?php
+$upload = new HttpMessage\UploadedFile(__DIR__ . '/not/exist');
+
+try {
+    $upload->moveTo(sys_get_temp_dir() . '/movedfile');
+} catch (RuntimeException $e) {
+    echo strtr($e->getMessage(), [DIRECTORY_SEPARATOR => '/']), "\n";
+}
+
+?>
+--CLEAN--
+<?php
+if (file_exists(sys_get_temp_dir() . '/movedfile') {
+    unlink(sys_get_temp_dir() . '/movedfile');
+}
+?>
+--EXPECTF--
+Warning: HttpMessage\UploadedFile::moveTo(%s): %sailed to open stream: No such file or directory in %s on line %d
+Failed to move uploaded file '%s/UploadedFile/not/exist' to '%s/movedfile'
diff --git a/tests/Uri/__construct_001.phpt b/tests/Uri/__construct_001.phpt
new file mode 100644
index 0000000..3095ccb
--- /dev/null
+++ b/tests/Uri/__construct_001.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri without arguments
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri());
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_002.phpt b/tests/Uri/__construct_002.phpt
new file mode 100644
index 0000000..b01db3d
--- /dev/null
+++ b/tests/Uri/__construct_002.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri with a basic url
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri('https://www.example.com'));
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(5) "https"
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(15) "www.example.com"
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_003.phpt b/tests/Uri/__construct_003.phpt
new file mode 100644
index 0000000..ebfa8b8
--- /dev/null
+++ b/tests/Uri/__construct_003.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri with a full url
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri('http://acme@www.example.com:8000/foo?answer=42#question'));
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(4) "http"
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(4) "acme"
+  ["host":"HttpMessage\Uri":private]=>
+  string(15) "www.example.com"
+  ["port":"HttpMessage\Uri":private]=>
+  int(8000)
+  ["path":"HttpMessage\Uri":private]=>
+  string(4) "/foo"
+  ["query":"HttpMessage\Uri":private]=>
+  string(9) "answer=42"
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(8) "question"
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_004.phpt b/tests/Uri/__construct_004.phpt
new file mode 100644
index 0000000..b472627
--- /dev/null
+++ b/tests/Uri/__construct_004.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri with username and password
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri('https://acme:secure@www.example.com'));
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(5) "https"
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(11) "acme:secure"
+  ["host":"HttpMessage\Uri":private]=>
+  string(15) "www.example.com"
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_005.phpt b/tests/Uri/__construct_005.phpt
new file mode 100644
index 0000000..3b5af9f
--- /dev/null
+++ b/tests/Uri/__construct_005.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri with a basic url without a path
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri('https://www.example.com'));
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(5) "https"
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(15) "www.example.com"
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_006.phpt b/tests/Uri/__construct_006.phpt
new file mode 100644
index 0000000..17e209b
--- /dev/null
+++ b/tests/Uri/__construct_006.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri with a domain name
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri('www.example.com'));
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(15) "www.example.com"
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_007.phpt b/tests/Uri/__construct_007.phpt
new file mode 100644
index 0000000..7f965c1
--- /dev/null
+++ b/tests/Uri/__construct_007.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri with an absolute path
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri('/foo'));
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(4) "/foo"
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_008.phpt b/tests/Uri/__construct_008.phpt
new file mode 100644
index 0000000..92905a0
--- /dev/null
+++ b/tests/Uri/__construct_008.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Create Uri with a relative path
+--FILE--
+<?php
+var_dump(new HttpMessage\Uri('foo'));
+?>
+--EXPECTF--
+object(HttpMessage\Uri)#%d (7) {
+  ["scheme":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["userInfo":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["host":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["port":"HttpMessage\Uri":private]=>
+  NULL
+  ["path":"HttpMessage\Uri":private]=>
+  string(3) "foo"
+  ["query":"HttpMessage\Uri":private]=>
+  string(0) ""
+  ["fragment":"HttpMessage\Uri":private]=>
+  string(0) ""
+}
\ No newline at end of file
diff --git a/tests/Uri/__construct_err01.phpt b/tests/Uri/__construct_err01.phpt
new file mode 100644
index 0000000..0980baa
--- /dev/null
+++ b/tests/Uri/__construct_err01.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Create Uri error: invalid argument
+--FILE--
+<?php
+try {
+    new HttpMessage\Uri(['scheme' => 'https']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+%sttpMessage\Uri::__construct()%s string, array given
diff --git a/tests/Uri/__construct_err02.phpt b/tests/Uri/__construct_err02.phpt
new file mode 100644
index 0000000..46115ea
--- /dev/null
+++ b/tests/Uri/__construct_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Create Uri error: Invalid uri
+--FILE--
+<?php
+try {
+    new HttpMessage\Uri('https://');
+} catch (UnexpectedValueException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Invalid uri
diff --git a/tests/Uri/__toString_001.phpt b/tests/Uri/__toString_001.phpt
new file mode 100644
index 0000000..88b4d22
--- /dev/null
+++ b/tests/Uri/__toString_001.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Cast Uri to string with full url
+--FILE--
+<?php
+$uri = new HttpMessage\Uri('http://acme:secure@www.example.com:8000/foo?q=42#answer');
+var_dump((string)$uri);
+?>
+--EXPECT--
+string(55) "http://acme:secure@www.example.com:8000/foo?q=42#answer"
diff --git a/tests/Uri/__toString_002.phpt b/tests/Uri/__toString_002.phpt
new file mode 100644
index 0000000..daee820
--- /dev/null
+++ b/tests/Uri/__toString_002.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Cast Uri to string without url
+--FILE--
+<?php
+$uri = new HttpMessage\Uri();
+var_dump((string)$uri);
+?>
+--EXPECT--
+string(0) ""
diff --git a/tests/Uri/__toString_003.phpt b/tests/Uri/__toString_003.phpt
new file mode 100644
index 0000000..0703810
--- /dev/null
+++ b/tests/Uri/__toString_003.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Cast Uri to string with basic url
+--FILE--
+<?php
+$uri = new HttpMessage\Uri('https://www.example.com/');
+var_dump((string)$uri);
+?>
+--EXPECT--
+string(24) "https://www.example.com/"
diff --git a/tests/Uri/__toString_004.phpt b/tests/Uri/__toString_004.phpt
new file mode 100644
index 0000000..f58a217
--- /dev/null
+++ b/tests/Uri/__toString_004.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Cast Uri to string with only user info and port
+--FILE--
+<?php
+$uri = (new HttpMessage\Uri)
+    ->withUserInfo('foo:bar')
+    ->withPort(8000);
+
+var_dump((string)$uri);
+?>
+--EXPECT--
+string(0) ""
diff --git a/tests/Uri/__toString_005.phpt b/tests/Uri/__toString_005.phpt
new file mode 100644
index 0000000..fe06f1d
--- /dev/null
+++ b/tests/Uri/__toString_005.phpt
@@ -0,0 +1,10 @@
+--TEST--
+Cast Uri to string with paths
+--FILE--
+<?php
+var_dump((string)(new HttpMessage\Uri('foo')));
+var_dump((string)(new HttpMessage\Uri('/foo')));
+?>
+--EXPECT--
+string(3) "foo"
+string(4) "/foo"
diff --git a/tests/Uri/__toString_006.phpt b/tests/Uri/__toString_006.phpt
new file mode 100644
index 0000000..2e2b0ff
--- /dev/null
+++ b/tests/Uri/__toString_006.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Cast Uri to string with query or fragment
+--FILE--
+<?php
+var_dump((string)(new HttpMessage\Uri('?q=42')));
+var_dump((string)(new HttpMessage\Uri('#answer')));
+var_dump((string)(new HttpMessage\Uri('?q=42#answer')));
+var_dump((string)(new HttpMessage\Uri('/foo?q=42#answer')));
+?>
+--EXPECT--
+string(5) "?q=42"
+string(7) "#answer"
+string(12) "?q=42#answer"
+string(16) "/foo?q=42#answer"
diff --git a/tests/Uri/__toString_007.phpt b/tests/Uri/__toString_007.phpt
new file mode 100644
index 0000000..107e538
--- /dev/null
+++ b/tests/Uri/__toString_007.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Cast Uri to string with authority but without schema
+--FILE--
+<?php
+$uri = (new HttpMessage\Uri)->withHost('www.example.com');
+
+var_dump((string)$uri);
+var_dump((string)($uri->withUserInfo('user:pass')));
+?>
+--EXPECT--
+string(17) "//www.example.com"
+string(27) "//user:pass@www.example.com"
diff --git a/tests/Uri/__toString_008.phpt b/tests/Uri/__toString_008.phpt
new file mode 100644
index 0000000..d626603
--- /dev/null
+++ b/tests/Uri/__toString_008.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Cast Uri to string with authority but without schema
+--FILE--
+<?php
+var_dump((string)((new HttpMessage\Uri)->withPath('/foo')));
+var_dump((string)((new HttpMessage\Uri)->withPath('//foo')));
+var_dump((string)((new HttpMessage\Uri)->withPath('/////foo')));
+?>
+--EXPECT--
+string(4) "/foo"
+string(4) "/foo"
+string(4) "/foo"
diff --git a/tests/Uri/__toString_009.phpt b/tests/Uri/__toString_009.phpt
new file mode 100644
index 0000000..8d9299b
--- /dev/null
+++ b/tests/Uri/__toString_009.phpt
@@ -0,0 +1,11 @@
+--TEST--
+Cast Uri to string with authority but without schema
+--FILE--
+<?php
+$uri = new HttpMessage\Uri('http://www.example.com');
+var_dump((string)($uri->withPath('foo')));
+var_dump((string)($uri->withPath('//foo')));
+?>
+--EXPECT--
+string(26) "http://www.example.com/foo"
+string(27) "http://www.example.com//foo"
diff --git a/tests/Uri/authority_001.phpt b/tests/Uri/authority_001.phpt
new file mode 100644
index 0000000..59bc9b7
--- /dev/null
+++ b/tests/Uri/authority_001.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Uri::getAuthority()
+--FILE--
+<?php
+$uri = new HttpMessage\Uri('http://acme:secure@www.example.com:8000/foo?q=42#answer');
+var_dump($uri->getAuthority());
+?>
+--EXPECT--
+string(32) "acme:secure@www.example.com:8000"
diff --git a/tests/Uri/authority_002.phpt b/tests/Uri/authority_002.phpt
new file mode 100644
index 0000000..ec9c51e
--- /dev/null
+++ b/tests/Uri/authority_002.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Uri::getAuthority()
+--FILE--
+<?php
+$uri = new HttpMessage\Uri();
+var_dump($uri->getAuthority());
+?>
+--EXPECT--
+string(0) ""
diff --git a/tests/Uri/authority_003.phpt b/tests/Uri/authority_003.phpt
new file mode 100644
index 0000000..05ead0e
--- /dev/null
+++ b/tests/Uri/authority_003.phpt
@@ -0,0 +1,10 @@
+--TEST--
+Uri::getAuthority()
+--FILE--
+<?php
+$uri = new HttpMessage\Uri('https://www.example.com/');
+var_dump($uri->getAuthority());
+?>
+--EXPECT--
+string(15) "www.example.com"
+
diff --git a/tests/Uri/fragment_001.phpt b/tests/Uri/fragment_001.phpt
new file mode 100644
index 0000000..11c0071
--- /dev/null
+++ b/tests/Uri/fragment_001.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Uri::getFragment()
+--FILE--
+<?php
+var_dump((new HttpMessage\Uri())->getFragment());
+var_dump((new HttpMessage\Uri('http://example.com#foo'))->getFragment());
+var_dump((new HttpMessage\Uri('#foo'))->getFragment());
+var_dump((new HttpMessage\Uri('?a=1#foo'))->getFragment());
+?>
+--EXPECT--
+string(0) ""
+string(3) "foo"
+string(3) "foo"
+string(3) "foo"
diff --git a/tests/Uri/fragment_002.phpt b/tests/Uri/fragment_002.phpt
new file mode 100644
index 0000000..892e1b4
--- /dev/null
+++ b/tests/Uri/fragment_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Uri::withFragment()
+--FILE--
+<?php
+$blank = new HttpMessage\Uri();
+$foo = $blank->withFragment("foo");
+$bar = $foo->withFragment("!bar");
+
+var_dump($blank->getFragment());
+var_dump($foo->getFragment());
+var_dump($bar->getFragment());
+?>
+--EXPECT--
+string(0) ""
+string(3) "foo"
+string(4) "!bar"
diff --git a/tests/Uri/fragment_err01.phpt b/tests/Uri/fragment_err01.phpt
new file mode 100644
index 0000000..a088cac
--- /dev/null
+++ b/tests/Uri/fragment_err01.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withFragment() error: invalid argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withFragment(['fragment' => 'foo']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withFragment()%s string, array given
diff --git a/tests/Uri/fragment_err02.phpt b/tests/Uri/fragment_err02.phpt
new file mode 100644
index 0000000..b706a6a
--- /dev/null
+++ b/tests/Uri/fragment_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withFragment() error: missing argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withFragment();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withFragment() expects exactly 1 %s, 0 given
diff --git a/tests/Uri/host_001.phpt b/tests/Uri/host_001.phpt
new file mode 100644
index 0000000..32fd98e
--- /dev/null
+++ b/tests/Uri/host_001.phpt
@@ -0,0 +1,10 @@
+--TEST--
+Uri::getHost()
+--FILE--
+<?php
+var_dump((new HttpMessage\Uri())->getHost());
+var_dump((new HttpMessage\Uri('http://www.example.com'))->getHost());
+?>
+--EXPECT--
+string(0) ""
+string(15) "www.example.com"
diff --git a/tests/Uri/host_002.phpt b/tests/Uri/host_002.phpt
new file mode 100644
index 0000000..af9bae5
--- /dev/null
+++ b/tests/Uri/host_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Uri::withHost()
+--FILE--
+<?php
+$blank = new HttpMessage\Uri();
+$dotCom = $blank->withHost("www.example.com");
+$dotNet = $dotCom->withHost("www.example.net");
+
+var_dump($blank->getHost());
+var_dump($dotCom->getHost());
+var_dump($dotNet->getHost());
+?>
+--EXPECT--
+string(0) ""
+string(15) "www.example.com"
+string(15) "www.example.net"
diff --git a/tests/Uri/host_error.phpt b/tests/Uri/host_error.phpt
new file mode 100644
index 0000000..5f00ee0
--- /dev/null
+++ b/tests/Uri/host_error.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Uri::withHost() errors
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withHost(['host' => 'www.example.com']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+try {
+    (new HttpMessage\Uri)->withHost();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withHost()%s, array given
+HttpMessage\Uri::withHost() expects exactly 1 %s, 0 given
diff --git a/tests/Uri/path_001.phpt b/tests/Uri/path_001.phpt
new file mode 100644
index 0000000..683b6db
--- /dev/null
+++ b/tests/Uri/path_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Uri::getPath()
+--FILE--
+<?php
+var_dump((new HttpMessage\Uri())->getPath());
+var_dump((new HttpMessage\Uri('http://example.com'))->getPath());
+var_dump((new HttpMessage\Uri('http://example.com/'))->getPath());
+var_dump((new HttpMessage\Uri('http://example.com/foo'))->getPath());
+var_dump((new HttpMessage\Uri('/'))->getPath());
+var_dump((new HttpMessage\Uri('/foo'))->getPath());
+?>
+--EXPECT--
+string(0) ""
+string(0) ""
+string(1) "/"
+string(4) "/foo"
+string(1) "/"
+string(4) "/foo"
diff --git a/tests/Uri/path_002.phpt b/tests/Uri/path_002.phpt
new file mode 100644
index 0000000..dc24c83
--- /dev/null
+++ b/tests/Uri/path_002.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Uri::withPath()
+--FILE--
+<?php
+$blank = new HttpMessage\Uri();
+$root = $blank->withPath("/");
+$absolute = $root->withPath("/foo");
+$relative = $absolute->withPath("foo");
+$double = $absolute->withPath("//foo");
+
+var_dump($blank->getPath());
+var_dump($root->getPath());
+var_dump($absolute->getPath());
+var_dump($relative->getPath());
+var_dump($double->getPath());
+?>
+--EXPECT--
+string(0) ""
+string(1) "/"
+string(4) "/foo"
+string(3) "foo"
+string(5) "//foo"
diff --git a/tests/Uri/path_err01.phpt b/tests/Uri/path_err01.phpt
new file mode 100644
index 0000000..b44e75a
--- /dev/null
+++ b/tests/Uri/path_err01.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withPath() error: invalid argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withPath(['path' => '/foo']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withPath()%s string, array given
diff --git a/tests/Uri/path_err02.phpt b/tests/Uri/path_err02.phpt
new file mode 100644
index 0000000..35d6fdc
--- /dev/null
+++ b/tests/Uri/path_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withPath() error: missing argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withPath();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withPath() expects exactly 1 %s, 0 given
diff --git a/tests/Uri/port_001.phpt b/tests/Uri/port_001.phpt
new file mode 100644
index 0000000..3ac26ab
--- /dev/null
+++ b/tests/Uri/port_001.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::getPort()
+--FILE--
+<?php
+var_dump((new HttpMessage\Uri)->getPort());
+var_dump((new HttpMessage\Uri('http://www.example.com'))->getPort());
+var_dump((new HttpMessage\Uri('http://www.example.com:80'))->getPort());
+?>
+--EXPECT--
+NULL
+NULL
+int(80)
diff --git a/tests/Uri/port_002.phpt b/tests/Uri/port_002.phpt
new file mode 100644
index 0000000..b4e40ff
--- /dev/null
+++ b/tests/Uri/port_002.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Uri::withPort()
+--FILE--
+<?php
+$blank = new HttpMessage\Uri('http://example.com');
+$port80 = $blank->withPort(80);
+$port8000 = $port80->withPort(8000);
+$noPort = $port80->withPort(null);
+
+var_dump($blank->getPort());
+var_dump($port80->getPort());
+var_dump($port8000->getPort());
+var_dump($noPort->getPort());
+
+var_dump((string)$port80);
+var_dump((string)$noPort);
+?>
+--EXPECT--
+NULL
+int(80)
+int(8000)
+NULL
+string(21) "http://example.com:80"
+string(18) "http://example.com"
\ No newline at end of file
diff --git a/tests/Uri/port_err01.phpt b/tests/Uri/port_err01.phpt
new file mode 100644
index 0000000..1dd9dc0
--- /dev/null
+++ b/tests/Uri/port_err01.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withPort() error: invalid argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withPort(['port' => 80]);
+} catch (TypeError $e) {
+    echo strtr($e->getMessage(), ['integer' => 'int']), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withPort()%sint, array given
diff --git a/tests/Uri/port_err02.phpt b/tests/Uri/port_err02.phpt
new file mode 100644
index 0000000..a24b02c
--- /dev/null
+++ b/tests/Uri/port_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withPort() error: missing argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withPort();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withPort() expects exactly 1 %s, 0 given
diff --git a/tests/Uri/query_001.phpt b/tests/Uri/query_001.phpt
new file mode 100644
index 0000000..88bfacb
--- /dev/null
+++ b/tests/Uri/query_001.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Uri::getQuery()
+--FILE--
+<?php
+var_dump((new HttpMessage\Uri())->getQuery());
+var_dump((new HttpMessage\Uri('http://example.com?foo=1&bar=2'))->getQuery());
+var_dump((new HttpMessage\Uri('?foo=1&bar=2'))->getQuery());
+var_dump((new HttpMessage\Uri('https://www.example.com'))->getQuery());
+?>
+--EXPECT--
+string(0) ""
+string(11) "foo=1&bar=2"
+string(11) "foo=1&bar=2"
+string(0) ""
diff --git a/tests/Uri/query_002.phpt b/tests/Uri/query_002.phpt
new file mode 100644
index 0000000..2f92b8d
--- /dev/null
+++ b/tests/Uri/query_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Uri::withQuery()
+--FILE--
+<?php
+$blank = new HttpMessage\Uri();
+$foo = $blank->withQuery("foo=1");
+$fooBar = $foo->withQuery("foo=1&bar=2");
+
+var_dump($blank->getQuery());
+var_dump($foo->getQuery());
+var_dump($fooBar->getQuery());
+?>
+--EXPECT--
+string(0) ""
+string(5) "foo=1"
+string(11) "foo=1&bar=2"
diff --git a/tests/Uri/query_err01.phpt b/tests/Uri/query_err01.phpt
new file mode 100644
index 0000000..dc6051e
--- /dev/null
+++ b/tests/Uri/query_err01.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withQuery() error: invalid argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withQuery(['foo' => 1]);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withQuery()%s string, array given
diff --git a/tests/Uri/query_err02.phpt b/tests/Uri/query_err02.phpt
new file mode 100644
index 0000000..220c2bc
--- /dev/null
+++ b/tests/Uri/query_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withQuery() error: missing argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withQuery();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withQuery() expects exactly 1 %s, 0 given
diff --git a/tests/Uri/scheme_001.phpt b/tests/Uri/scheme_001.phpt
new file mode 100644
index 0000000..12485b3
--- /dev/null
+++ b/tests/Uri/scheme_001.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Uri::getScheme()
+--FILE--
+<?php
+var_dump((new HttpMessage\Uri())->getScheme());
+var_dump((new HttpMessage\Uri('http:'))->getScheme());
+var_dump((new HttpMessage\Uri('mailto:john@example.com'))->getScheme());
+var_dump((new HttpMessage\Uri('https://www.example.com'))->getScheme());
+var_dump((new HttpMessage\Uri('ftp://www.example.com'))->getScheme());
+?>
+--EXPECT--
+string(0) ""
+string(4) "http"
+string(6) "mailto"
+string(5) "https"
+string(3) "ftp"
diff --git a/tests/Uri/scheme_002.phpt b/tests/Uri/scheme_002.phpt
new file mode 100644
index 0000000..dddb322
--- /dev/null
+++ b/tests/Uri/scheme_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Uri::withScheme()
+--FILE--
+<?php
+$blank = new HttpMessage\Uri();
+$https = $blank->withScheme("https");
+$mailto = $https->withScheme("mailto");
+
+var_dump($blank->getScheme());
+var_dump($https->getScheme());
+var_dump($mailto->getScheme());
+?>
+--EXPECT--
+string(0) ""
+string(5) "https"
+string(6) "mailto"
diff --git a/tests/Uri/scheme_err01.phpt b/tests/Uri/scheme_err01.phpt
new file mode 100644
index 0000000..2df465d
--- /dev/null
+++ b/tests/Uri/scheme_err01.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withHost() error: invalid argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withScheme(['scheme' => 'http']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withScheme()%s string, array given
diff --git a/tests/Uri/scheme_err02.phpt b/tests/Uri/scheme_err02.phpt
new file mode 100644
index 0000000..82ccc21
--- /dev/null
+++ b/tests/Uri/scheme_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withHost() error: missing argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withScheme();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withScheme() expects exactly 1 %s, 0 given
diff --git a/tests/Uri/userInfo_001.phpt b/tests/Uri/userInfo_001.phpt
new file mode 100644
index 0000000..e361643
--- /dev/null
+++ b/tests/Uri/userInfo_001.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::getUserInfo()
+--FILE--
+<?php
+var_dump((new HttpMessage\Uri)->getUserInfo());
+var_dump((new HttpMessage\Uri('http://user@www.example.com'))->getUserInfo());
+var_dump((new HttpMessage\Uri('http://user:pass@www.example.com'))->getUserInfo());
+?>
+--EXPECT--
+string(0) ""
+string(4) "user"
+string(9) "user:pass"
diff --git a/tests/Uri/userInfo_002.phpt b/tests/Uri/userInfo_002.phpt
new file mode 100644
index 0000000..691ba6c
--- /dev/null
+++ b/tests/Uri/userInfo_002.phpt
@@ -0,0 +1,16 @@
+--TEST--
+Uri::withScheme()
+--FILE--
+<?php
+$blank = new HttpMessage\Uri();
+$user = $blank->withUserInfo("user");
+$userPass = $user->withUserInfo("user:pass");
+
+var_dump($blank->getUserInfo());
+var_dump($user->getUserInfo());
+var_dump($userPass->getUserInfo());
+?>
+--EXPECT--
+string(0) ""
+string(4) "user"
+string(9) "user:pass"
diff --git a/tests/Uri/userInfo_err01.phpt b/tests/Uri/userInfo_err01.phpt
new file mode 100644
index 0000000..6be1486
--- /dev/null
+++ b/tests/Uri/userInfo_err01.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withUserInfo() error: invalid argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withUserInfo(['userInfo' => 'user']);
+} catch (TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withUserInfo()%s string, array given
diff --git a/tests/Uri/userInfo_err02.phpt b/tests/Uri/userInfo_err02.phpt
new file mode 100644
index 0000000..c637e0e
--- /dev/null
+++ b/tests/Uri/userInfo_err02.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Uri::withUserInfo() error: missing argument
+--FILE--
+<?php
+try {
+    (new HttpMessage\Uri)->withUserInfo();
+} catch (Error $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+HttpMessage\Uri::withUserInfo() expects exactly 1 %s, 0 given
diff --git a/tests/skeleton_nop.phpt b/tests/skeleton_nop.phpt
deleted file mode 100644
index 0732b1b..0000000
--- a/tests/skeleton_nop.phpt
+++ /dev/null
@@ -1,12 +0,0 @@
---TEST--
-TODO: Add tests for your functions
---SKIPIF--
-<?php if (!extension_loaded("skeleton")) print "skip"; ?>
---FILE--
-<?php
-var_dump(skeleton_nop("Hello World"));
-
-?>
---EXPECT--
-string(11) "Hello World"
-
diff --git a/tests/smoke.php b/tests/smoke.php
new file mode 100644
index 0000000..76b0a61
--- /dev/null
+++ b/tests/smoke.php
@@ -0,0 +1,16 @@
+<?php
+
+// Simple smoke test.
+
+new HttpMessage\Uri();
+new HttpMessage\Stream();
+new HttpMessage\UploadedFile(__FILE__);
+
+new HttpMessage\Request();
+new HttpMessage\ServerRequest();
+new HttpMessage\Response();
+
+new HttpMessage\Factory();
+new HttpMessage\Emitter();
+
+echo "ok";
diff --git a/uploaded_file.c b/uploaded_file.c
new file mode 100644
index 0000000..0695c63
--- /dev/null
+++ b/uploaded_file.c
@@ -0,0 +1,487 @@
+/*
+  +----------------------------------------------------------------------+
+  | HTTP Message PHP extension - UploadedFile class                      |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 2019 Arnold Daniels                                    |
+  +----------------------------------------------------------------------+
+  | Permission is hereby granted, free of charge, to any person          |
+  | obtaining a copy of this software and associated documentation files |
+  | (the "Software"), to deal in the Software without restriction,       |
+  | including without limitation the rights to use, copy, modify, merge, |
+  | publish, distribute, sublicense, and/or sell copies of the Software, |
+  | and to permit persons to whom the Software is furnished to do so,    |
+  | subject to the following conditions:                                 |
+  |                                                                      |
+  | The above copyright notice and this permission notice shall be       |
+  | included in all copies or substantial portions of the Software.      |
+  |                                                                      |
+  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,      |
+  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF   |
+  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                |
+  | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS  |
+  | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN   |
+  | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN    |
+  | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE     |
+  | SOFTWARE.                                                            |
+  +----------------------------------------------------------------------+
+  | Author: Arnold Daniels <arnold@jasny.net>                            |
+  +----------------------------------------------------------------------+
+*/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "php.h"
+#include "php_http_message.h"
+#include "macros.h"
+#include "zend_exceptions.h"
+#include "zend_interfaces.h"
+#include "SAPI.h"
+#include "ext/standard/file.h"
+#include "ext/psr/psr_http_message.h"
+#include "ext/spl/spl_exceptions.h"
+
+#if HAVE_HTTP_MESSAGE
+
+zend_class_entry *HttpMessage_UploadedFile_ce = NULL;
+
+int assert_file_available(zval *file, zval *stream, zval *moved)
+{
+    zval stream_file, arg1;
+    char *filename;
+
+    if ((file == NULL || ZVAL_IS_NULL(file)) && (stream == NULL || ZVAL_IS_NULL(stream))) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "No file was uploaded or uploaded file not available");
+        return FAILURE;
+    }
+
+    if (Z_TYPE_P(moved) == IS_TRUE) {
+        if (!ZVAL_IS_NULL(file)) {
+            filename = Z_STRVAL_P(file);
+            STR_CLOSE(filename, Z_STRLEN_P(file));
+        } else {
+            // Can be any StreamInterface object, doesn't need to be Stream from this lib.
+            ZVAL_STRINGL(&arg1, "uri", 3);
+            zend_call_method_with_1_params(PROPERTY_ARG(stream), NULL, NULL, "getMetadata", &stream_file, &arg1);
+            filename = Z_STRVAL(stream_file);
+            STR_CLOSE(filename, Z_STRLEN(stream_file));
+        }
+
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Uploaded file '%s' has already been moved", filename);
+        return FAILURE;
+    }
+
+    return SUCCESS;
+}
+
+zend_bool assert_uploaded_file(char *path, size_t path_len)
+{
+    if (SG(rfc1867_uploaded_files) == NULL || !zend_hash_str_exists(SG(rfc1867_uploaded_files), path, path_len)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Won't move '%s'; not an uploaded file", path);
+        return FAILURE;
+    }
+
+    return SUCCESS;
+}
+
+void uploaded_file_chmod(char *new_path)
+{
+    int oldmask; int ret;
+
+    oldmask = umask(077);
+    umask(oldmask);
+
+    ret = VCWD_CHMOD(new_path, 0666 & ~oldmask);
+
+    if (ret == -1) {
+        php_error_docref(NULL, E_WARNING, "%s", strerror(errno));
+    }
+}
+
+int move_uploaded_file(char *path, size_t path_len, char *new_path, size_t new_path_len)
+{
+    HashTable *uploaded_files;
+    zend_bool successful = 0;
+
+    STR_CLOSE(new_path, new_path_len);
+
+    if (php_check_open_basedir_ex(new_path, 1)) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0,
+                 "Unable to move uploaded file '%s' to '%s'; open_basedir restriction in effect", path, new_path);
+        return FAILURE;
+    }
+
+    if (VCWD_RENAME(path, new_path) == 0) {
+        successful = 1;
+#ifndef PHP_WIN32
+        uploaded_file_chmod(new_path);
+#endif
+    } else if (php_copy_file_ex(path, new_path, STREAM_DISABLE_OPEN_BASEDIR) == SUCCESS) {
+        VCWD_UNLINK(path);
+        successful = 1;
+    }
+
+    if (!successful) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Failed to move uploaded file '%s' to '%s'",
+                path, new_path);
+        return FAILURE;
+    }
+
+    uploaded_files = SG(rfc1867_uploaded_files);
+    if (uploaded_files != NULL) {
+        zend_hash_str_del(uploaded_files, path, path_len);
+    }
+
+    return SUCCESS;
+}
+
+int move_uploaded_stream(zval *stream, char *new_path, size_t new_path_len)
+{
+    zval resource;
+    php_stream *source, *target;
+    size_t len;
+    int ret;
+
+    // Can be any StreamInterface object, doesn't need to be Stream from this lib.
+    zend_call_method_with_0_params(PROPERTY_ARG(stream), NULL, NULL, "detach", &resource);
+    source = (php_stream*)zend_fetch_resource2_ex(&resource, "stream", php_file_le_stream(), php_file_le_pstream());
+
+    STR_CLOSE(new_path, new_path_len);
+    target = php_stream_open_wrapper(new_path, "w", 0, NULL);
+
+    if (EXPECTED(source != NULL && target != NULL)) {
+        if (source->ops->seek && (source->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) {
+            php_stream_seek(source, 0, SEEK_SET);
+        };
+
+        ret = php_stream_copy_to_stream_ex(source, target, PHP_STREAM_COPY_ALL, &len);
+    } else {
+        ret = FAILURE;
+    }
+
+    if (ret == FAILURE) {
+        zend_throw_exception_ex(spl_ce_RuntimeException, 0, "Failed to stream uploaded file to '%s'", new_path);
+    }
+
+    return ret;
+}
+
+void construct_uploaded_file(
+    zval* object,
+    zval *stream,
+    zend_string *file,
+    zend_long size,
+    zend_long error,
+    zend_string *clientFilename,
+    zend_string *clientMediaType,
+    char checkUploaded
+) {
+    zval zreadable;
+
+    if (error == 0 && stream != NULL) {
+        // Must be verified it's a StreamInterface object before passing it to this function.
+        // Can be any StreamInterface object, doesn't need to be Stream from this lib.
+        zend_call_method_with_0_params(PROPERTY_ARG(stream), NULL, NULL, "isReadable", &zreadable);
+
+        if (UNEXPECTED(Z_TYPE(zreadable) != IS_TRUE)) {
+            zend_throw_exception(
+                    spl_ce_InvalidArgumentException, "Stream provided for uploaded file is not readable", 0
+            );
+        }
+        zend_update_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(object), ZEND_STRL("stream"), stream);
+    } else if (error == 0 && file != NULL) {
+        zend_update_property_str(HttpMessage_UploadedFile_ce, PROPERTY_ARG(object), ZEND_STRL("file"), file);
+    }
+
+    if (clientFilename != NULL) {
+        zend_update_property_str(HttpMessage_UploadedFile_ce, PROPERTY_ARG(object), ZEND_STRL("clientFilename"), clientFilename);
+    }
+    if (clientMediaType != NULL) {
+        zend_update_property_str(HttpMessage_UploadedFile_ce, PROPERTY_ARG(object), ZEND_STRL("clientMediaType"), clientMediaType);
+    }
+
+    if (size > 0) {
+        zend_update_property_long(HttpMessage_UploadedFile_ce, PROPERTY_ARG(object), ZEND_STRL("size"), size);
+    }
+    if (error < 0 || error > 8) {
+        zend_throw_exception_ex(spl_ce_InvalidArgumentException, 0, "Invalid error code %ld", error);
+    }
+    zend_update_property_long(HttpMessage_UploadedFile_ce, PROPERTY_ARG(object), ZEND_STRL("error"), error);
+
+    if (checkUploaded < 0) {
+        checkUploaded = SG(rfc1867_uploaded_files) != NULL;
+    }
+    zend_update_property_bool(HttpMessage_UploadedFile_ce, PROPERTY_ARG(object), ZEND_STRL("checkUploaded"), checkUploaded);
+}
+
+void create_uploaded_file(zval *uploaded_file, zval *tmp_name, zval *size, zval *error, zval *name, zval *type)
+{
+    zend_object *new_object;
+
+    new_object = zend_objects_new(HttpMessage_UploadedFile_ce);
+    object_properties_init(new_object, HttpMessage_UploadedFile_ce);
+
+    ZVAL_OBJ(uploaded_file, new_object);
+
+    construct_uploaded_file(
+            uploaded_file,
+            NULL,
+            tmp_name != NULL ? Z_STR_P(tmp_name) : NULL,
+            size != NULL ? Z_LVAL_P(size) : -1,
+            Z_LVAL_P(error),
+            name != NULL ? Z_STR_P(name) : NULL,
+            type != NULL ? Z_STR_P(type) : NULL,
+            -1 // auto-detect check is_uploaded
+    );
+}
+
+void restructure_uploaded_files(
+    zval *objects,
+    HashTable *names,
+    HashTable *types,
+    HashTable *tmp_names,
+    HashTable *errors,
+    HashTable *sizes
+) {
+    zval *name, *type, *tmp_name, *error, *size, *element;
+    zend_ulong index;
+    zend_string *key;
+
+    ZEND_HASH_FOREACH_KEY_VAL(errors, index, key, error) {
+        element = ARRAY_ADD(Z_ARR_P(objects), index, key);
+
+        name = ARRAY_GET(names, index, key);
+        type = ARRAY_GET(types, index, key);
+        tmp_name = ARRAY_GET(tmp_names, index, key);
+        size = ARRAY_GET(sizes, index, key);
+
+        if (Z_TYPE_P(error) == IS_LONG) {
+            create_uploaded_file(element, tmp_name, size, error, name, type);
+        } else if (EXPECTED(Z_TYPE_P(error) == IS_ARRAY)) {
+            array_init(element);
+            restructure_uploaded_files(element, Z_ARR_P_NULL(name), Z_ARR_P_NULL(type), Z_ARR_P_NULL(tmp_name),
+                    Z_ARR_P_NULL(error), Z_ARR_P_NULL(size)); // recursion
+        }
+    } ZEND_HASH_FOREACH_END();
+}
+
+void create_uploaded_files(zval *objects, HashTable *files)
+{
+    zval *zentry, *name, *type, *tmp_name, *error, *size, *element;
+    HashTable *entry;
+    zend_ulong index;
+    zend_string *key;
+
+    array_init(objects);
+
+    ZEND_HASH_FOREACH_KEY_VAL(files, index, key, zentry) {
+        if (UNEXPECTED(Z_TYPE_P(zentry) != IS_ARRAY)) continue;
+
+        entry = Z_ARR_P(zentry);
+        error = zend_hash_str_find(entry, ZEND_STRL("error"));
+
+        if (UNEXPECTED(error == NULL)) continue;
+
+        name = zend_hash_str_find(entry, ZEND_STRL("name"));
+        type = zend_hash_str_find(entry, ZEND_STRL("type"));
+        tmp_name = zend_hash_str_find(entry, ZEND_STRL("tmp_name"));
+        size = zend_hash_str_find(entry, ZEND_STRL("size"));
+
+        element = ARRAY_ADD(Z_ARR_P(objects), index, key);
+
+        if (Z_TYPE_P(error) == IS_LONG) {
+            create_uploaded_file(element, tmp_name, size, error, name, type);
+        } else if (EXPECTED(Z_TYPE_P(error) == IS_ARRAY)) {
+            array_init(element);
+            restructure_uploaded_files(element, Z_ARR_P(name), Z_ARR_P(type), Z_ARR_P(tmp_name), Z_ARR_P(error),
+                    Z_ARR_P(size));
+        }
+    } ZEND_HASH_FOREACH_END();
+}
+
+
+/* __construct */
+
+ZEND_BEGIN_ARG_INFO_EX(arginfo_HttpMessageUploadedFile_construct, 0, 0, 1)
+    ZEND_ARG_INFO(0, fileOrStream)
+    ZEND_ARG_TYPE_INFO(0, size, IS_LONG, 1)
+    ZEND_ARG_TYPE_INFO(0, error, IS_LONG, 0)
+    ZEND_ARG_TYPE_INFO(0, clientFilename, IS_STRING, 1)
+    ZEND_ARG_TYPE_INFO(0, clientMediaType, IS_STRING, 1)
+    ZEND_ARG_INFO(0, checkUploaded)
+ZEND_END_ARG_INFO()
+
+PHP_METHOD(UploadedFile, __construct)
+{
+    zval *fileOrStream = NULL, *stream = NULL;
+    zend_string *file = NULL, *clientFilename = NULL, *clientMediaType = NULL;
+    zend_long size = -1, error = 0;
+    zend_bool size_is_null = 1, checkUploaded = 0, checkUploaded_is_null = 1;
+    zend_class_entry *stream_interface;
+
+    ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 6)
+        Z_PARAM_ZVAL(fileOrStream)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_LONG_EX(size, size_is_null, 1, 0)
+        Z_PARAM_LONG(error)
+        Z_PARAM_STR_EX(clientFilename, 1, 0)
+        Z_PARAM_STR_EX(clientMediaType, 1, 0)
+        Z_PARAM_BOOL_EX(checkUploaded, checkUploaded_is_null, 1, 0)
+    ZEND_PARSE_PARAMETERS_END();
+
+    if (Z_TYPE_P(fileOrStream) == IS_STRING) {
+        file = Z_STR_P(fileOrStream);
+    } else if (Z_TYPE_P(fileOrStream) != IS_NULL) {
+        stream_interface = HTTP_MESSAGE_PSR_INTERFACE("stream");
+
+        if (UNEXPECTED(stream_interface == NULL)) {
+            zend_throw_error(NULL, "Psr\\Http\\Message\\StreamInterface not found");
+            return;
+        }
+
+        if (UNEXPECTED(
+            Z_TYPE_P(fileOrStream) != IS_OBJECT ||
+            !instanceof_function(Z_OBJCE_P(fileOrStream), stream_interface)
+        )) {
+            custom_parameter_type_error(1,
+                    "a string or object that implements Psr\\Http\\Message\\StreamInterface", fileOrStream);
+            return;
+        }
+
+        stream = fileOrStream;
+    }
+
+    construct_uploaded_file(getThis(), stream, file, size_is_null ? -1 : size, error, clientFilename, clientMediaType,
+            checkUploaded_is_null ? -1 : checkUploaded);
+}
+
+PHP_METHOD(UploadedFile, getStream)
+{
+    zval rv, *stream, *file, *moved, mode;
+
+    file = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("file"), 0, &rv);
+    stream = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    moved = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("moved"), 0, &rv);
+
+    if (assert_file_available(file, stream, moved) == FAILURE) {
+        return;
+    }
+
+    // Stream isn't set: create new stream for file
+    if (ZVAL_IS_NULL(stream)) {
+        ZVAL_STRINGL(&mode, "r", 1);
+        NEW_OBJECT_CONSTRUCT(stream, HttpMessage_Stream_ce, 2, file, &mode);
+    }
+
+    RETURN_ZVAL(stream, 1, 0);
+}
+
+PHP_METHOD(UploadedFile, moveTo)
+{
+    zval rv, *file, *moved, *stream, *checkUploaded;
+    char *new_path = NULL;
+    size_t new_path_len = 0;
+    int move_ret;
+
+    ZEND_PARSE_PARAMETERS_START(1, 1)
+        Z_PARAM_PATH(new_path, new_path_len)
+    ZEND_PARSE_PARAMETERS_END();
+
+    file = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("file"), 0, &rv);
+    stream = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("stream"), 0, &rv);
+    moved = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("moved"), 0, &rv);
+    checkUploaded = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("checkUploaded"), 0, &rv);
+
+    if (
+        assert_file_available(file, stream, moved) == FAILURE ||
+        (Z_TYPE_P(checkUploaded) == IS_TRUE && assert_uploaded_file(Z_STRVAL_P(file), Z_STRLEN_P(file) == FAILURE))
+    ) {
+        return;
+    }
+
+    if (!ZVAL_IS_NULL(file)) {
+        move_ret = move_uploaded_file(Z_STRVAL_P(file), Z_STRLEN_P(file), new_path, new_path_len);
+    } else {
+        move_ret = move_uploaded_stream(stream, new_path, new_path_len);
+    }
+
+    ZVAL_BOOL(moved, move_ret == SUCCESS);
+}
+
+PHP_METHOD(UploadedFile, getSize)
+{
+    zval rv, *value;
+
+    value = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("size"), 0, &rv);
+
+    RETURN_ZVAL(value, 1, 0);
+}
+
+PHP_METHOD(UploadedFile, getError)
+{
+    zval rv, *value;
+
+    value = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("error"), 0, &rv);
+
+    RETURN_ZVAL(value, 1, 0);
+}
+
+PHP_METHOD(UploadedFile, getClientFilename)
+{
+    zval rv, *value;
+
+    value = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("clientFilename"), 0, &rv);
+
+    RETURN_ZVAL(value, 1, 0);
+}
+
+PHP_METHOD(UploadedFile, getClientMediaType)
+{
+    zval rv, *value;
+
+    value = zend_read_property(HttpMessage_UploadedFile_ce, PROPERTY_ARG(getThis()), ZEND_STRL("clientMediaType"), 0, &rv);
+
+    RETURN_ZVAL(value, 1, 0);
+}
+
+/* Define HttpMessage\UploadedFile class */
+
+static const zend_function_entry methods[] = {
+    PHP_ME(UploadedFile, __construct, arginfo_HttpMessageUploadedFile_construct, ZEND_ACC_PUBLIC)
+    HTTP_MESSAGE_ME(UploadedFile, getStream)
+    HTTP_MESSAGE_ME(UploadedFile, moveTo)
+    HTTP_MESSAGE_ME(UploadedFile, getSize)
+    HTTP_MESSAGE_ME(UploadedFile, getError)
+    HTTP_MESSAGE_ME(UploadedFile, getClientFilename)
+    HTTP_MESSAGE_ME(UploadedFile, getClientMediaType)
+    PHP_FE_END
+};
+
+PHP_MINIT_FUNCTION(http_message_uploadedfile)
+{
+    zend_class_entry ce;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("uploadedfile");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(interface, "UploadedFile");
+
+    INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "UploadedFile", methods);
+
+    HttpMessage_UploadedFile_ce = zend_register_internal_class(&ce);
+    zend_class_implements(HttpMessage_UploadedFile_ce, 1, interface);
+
+    /* Properties */
+    zend_declare_property_null(HttpMessage_UploadedFile_ce, ZEND_STRL("stream"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_UploadedFile_ce, ZEND_STRL("file"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_UploadedFile_ce, ZEND_STRL("size"), ZEND_ACC_PRIVATE);
+    zend_declare_property_long(HttpMessage_UploadedFile_ce, ZEND_STRL("error"), 0, ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_UploadedFile_ce, ZEND_STRL("clientFilename"), ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_UploadedFile_ce, ZEND_STRL("clientMediaType"), ZEND_ACC_PRIVATE);
+
+    zend_declare_property_bool(HttpMessage_UploadedFile_ce, ZEND_STRL("moved"), 0, ZEND_ACC_PRIVATE);
+    zend_declare_property_bool(HttpMessage_UploadedFile_ce, ZEND_STRL("checkUploaded"), 0, ZEND_ACC_PRIVATE);
+
+    return SUCCESS;
+}
+
+#endif
diff --git a/uploaded_file.h b/uploaded_file.h
new file mode 100644
index 0000000..5d76f38
--- /dev/null
+++ b/uploaded_file.h
@@ -0,0 +1,47 @@
+/*
+  +----------------------------------------------------------------------+
+  | HTTP Message PHP extension                                           |
+  +----------------------------------------------------------------------+
+  | Copyright (c) 2019 Arnold Daniels                                    |
+  +----------------------------------------------------------------------+
+  | Permission is hereby granted, free of charge, to any person          |
+  | obtaining a copy of this software and associated documentation files |
+  | (the "Software"), to deal in the Software without restriction,       |
+  | including without limitation the rights to use, copy, modify, merge, |
+  | publish, distribute, sublicense, and/or sell copies of the Software, |
+  | and to permit persons to whom the Software is furnished to do so,    |
+  | subject to the following conditions:                                 |
+  |                                                                      |
+  | The above copyright notice and this permission notice shall be       |
+  | included in all copies or substantial portions of the Software.      |
+  |                                                                      |
+  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,      |
+  | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF   |
+  | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND                |
+  | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS  |
+  | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN   |
+  | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN    |
+  | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE     |
+  | SOFTWARE.                                                            |
+  +----------------------------------------------------------------------+
+  | Author: Arnold Daniels <arnold@jasny.net>                            |
+  +----------------------------------------------------------------------+
+*/
+
+#ifndef HTTP_MESSAGE_UPLOADED_FILE_H
+#define HTTP_MESSAGE_UPLOADED_FILE_H
+
+void construct_uploaded_file(
+    zval* object,
+    zval *stream,
+    zend_string *file,
+    zend_long size,
+    zend_long error,
+    zend_string *clientFilename,
+    zend_string *clientMediaType,
+    char checkUploaded
+);
+void create_uploaded_files(zval *objects, HashTable *files);
+void uri_set_userinfo(zval *uri, char *user, size_t user_len, char *pass, size_t pass_len);
+
+#endif //HTTP_MESSAGE_UPLOADED_FILE_H
diff --git a/uri.c b/uri.c
index 026f217..1f5b4c6 100644
--- a/uri.c
+++ b/uri.c
@@ -33,18 +33,40 @@
 #endif
 
 #include "php.h"
-#include "php_ini.h"
-#include "php_http_message.h"
 #include "macros.h"
 #include "zend_exceptions.h"
-#include "ext/standard/info.h"
+#include "zend_smart_str.h"
 #include "ext/standard/url.h"
 #include "ext/psr/psr_http_message.h"
+#include "ext/spl/spl_exceptions.h"
 
 #if HAVE_HTTP_MESSAGE
 
-zend_class_entry *HttpMessage_Uri_ce;
+zend_class_entry *HttpMessage_Uri_ce = NULL;
 
+void uri_set_userinfo(zval *uri, char *user, size_t user_len, char *pass, size_t pass_len)
+{
+    char *userinfo;
+
+    if (user_len == 0 && pass_len == 0) {
+        return;
+    }
+
+    if (pass_len == 0) {
+        zend_update_property_stringl(HttpMessage_Uri_ce, PROPERTY_ARG(uri), ZEND_STRL("userInfo"), user, user_len);
+    } else {
+        userinfo = emalloc(user_len + pass_len + 2);
+        if (UNEXPECTED(userinfo == NULL)) return; // Memory issue
+
+        user[user_len] = '\0';
+        pass[pass_len] = '\0';
+        sprintf(userinfo, "%s:%s", user, pass);
+
+        zend_update_property_stringl(HttpMessage_Uri_ce, PROPERTY_ARG(uri), ZEND_STRL("userInfo"), userinfo, user_len + pass_len + 1);
+
+        efree(userinfo);
+    }
+}
 
 /* __construct */
 
@@ -55,39 +77,114 @@ ZEND_END_ARG_INFO()
 PHP_METHOD(Uri, __construct)
 {
     php_url *info;
-    char *value;
+    char *value = NULL;
     size_t value_len = 0;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 0, 1)
-            Z_PARAM_OPTIONAL
-            Z_PARAM_STRING(value, value_len)
+        Z_PARAM_OPTIONAL
+        Z_PARAM_STRING(value, value_len)
     ZEND_PARSE_PARAMETERS_END();
 
     if (value_len > 0) {
         info = php_url_parse_ex(value, value_len);
 
-        SET_STRING_PROPERTY(HttpMessage_Uri_ce, "scheme", info->scheme);
-        SET_STRING_PROPERTY(HttpMessage_Uri_ce, "host", info->host);
-        SET_STRING_PROPERTY(HttpMessage_Uri_ce, "userInfo", info->user);
-        SET_STRING_PROPERTY(HttpMessage_Uri_ce, "path", info->path);
-        SET_STRING_PROPERTY(HttpMessage_Uri_ce, "query", info->query);
-        SET_STRING_PROPERTY(HttpMessage_Uri_ce, "fragment", info->fragment);
+        if (info == NULL) {
+            zend_throw_exception(spl_ce_UnexpectedValueException, "Invalid uri", 0);
+            return;
+        }
+
+        SET_URI_PROPERTY(HttpMessage_Uri_ce, "scheme", info->scheme);
+        SET_URI_PROPERTY(HttpMessage_Uri_ce, "host", info->host);
+        SET_URI_PROPERTY(HttpMessage_Uri_ce, "path", info->path);
+        SET_URI_PROPERTY(HttpMessage_Uri_ce, "query", info->query);
+        SET_URI_PROPERTY(HttpMessage_Uri_ce, "fragment", info->fragment);
 
         if (info->port > 0) {
-            zend_update_property_long(HttpMessage_Uri_ce, getThis(), ZEND_STRL("port"), info->port);
+            zend_update_property_long(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("port"), info->port);
         }
+
+#if PHP_VERSION_ID < 70300
+        uri_set_userinfo(getThis(), info->user, STRLEN_NULL(info->user), info->pass, STRLEN_NULL(info->pass));
+#else
+        uri_set_userinfo(getThis(), ZSTR_VAL_LEN(info->user), ZSTR_VAL_LEN(info->pass));
+#endif
     }
 }
 
 
 /* __toString */
 
-
 ZEND_BEGIN_ARG_INFO_EX(arginfo_HttpMessageUri_toString, 0, 0, 0)
 ZEND_END_ARG_INFO()
 
 PHP_METHOD(Uri, __toString)
 {
+    zval rv, *scheme, *userinfo, *host, *port, *path, *query, *fragment;
+    smart_str buf = {0};
+    char *path_ptr;
+    size_t path_len = 0;
+
+    scheme = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("scheme"), 0, &rv);
+    userinfo = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("userInfo"), 0, &rv);
+    host = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("host"), 0, &rv);
+    port = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("port"), 0, &rv);
+    path = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("path"), 0, &rv);
+    query = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("query"), 0, &rv);
+    fragment = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("fragment"), 0, &rv);
+
+    smart_str_alloc(&buf, 0, 0);
+
+    if (Z_STRLEN(*scheme) > 0) {
+        smart_str_appendl(&buf, Z_STRVAL(*scheme), Z_STRLEN(*scheme));
+        smart_str_appends(&buf, ":");
+    }
+
+    if (Z_STRLEN(*host) > 0) {
+        smart_str_appends(&buf, "//");
+
+        if (Z_STRLEN(*userinfo) > 0) {
+            smart_str_appendl(&buf, Z_STRVAL(*userinfo), Z_STRLEN(*userinfo));
+            smart_str_appends(&buf, "@");
+        }
+
+        smart_str_appendl(&buf, Z_STRVAL(*host), Z_STRLEN(*host));
+
+        if (Z_TYPE(*port) == IS_LONG) {
+            smart_str_appends(&buf, ":");
+            smart_str_append_long(&buf, Z_LVAL(*port));
+        }
+    }
+
+    if (Z_STRLEN(*path) > 0) {
+        if (Z_STRLEN(*host) > 0 && *Z_STRVAL(*path) != '/') {
+            smart_str_appends(&buf, "/");
+        }
+
+        path_ptr = Z_STRVAL(*path);
+        path_len = Z_STRLEN(*path);
+
+        if (Z_STRLEN(*host) == 0) {
+            while (path_len > 1 && *path_ptr == '/' && *(path_ptr + 1) == '/') {
+                path_ptr++;
+                path_len--;
+            }
+        }
+
+        smart_str_appendl(&buf, path_ptr, path_len);
+    }
+
+    if (Z_STRLEN(*query) > 0) {
+        smart_str_appends(&buf, "?");
+        smart_str_appendl(&buf, Z_STRVAL(*query), Z_STRLEN(*query));
+    }
+
+    if (Z_STRLEN(*fragment) > 0) {
+        smart_str_appends(&buf, "#");
+        smart_str_appendl(&buf, Z_STRVAL(*fragment), Z_STRLEN(*fragment));
+    }
+
+    RETVAL_STR_COPY(buf.s);
+    zend_string_release(buf.s);
 }
 
 
@@ -97,23 +194,22 @@ PHP_METHOD(Uri, getScheme)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("schema"), 0, &rv);
+    value = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("scheme"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Uri, withScheme)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(HttpMessage_Uri_ce, return_value, ZEND_STRL("schema"), value, value_len);
+    zend_update_property_str(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("scheme"), value);
 }
 
 
@@ -121,11 +217,31 @@ PHP_METHOD(Uri, withScheme)
 
 PHP_METHOD(Uri, getAuthority)
 {
-    zval rv, *value;
+    zval rv, *userinfo, *host, *port;
+    smart_str buf = {0};
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("userInfo"), 0, &rv);
+    userinfo = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("userInfo"), 0, &rv);
+    host = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("host"), 0, &rv);
+    port = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("port"), 0, &rv);
 
-    RETURN_ZVAL(value, 1, 0);
+    if (Z_STRLEN_P(host) == 0) {
+        RETURN_EMPTY_STRING();
+    }
+
+    if (Z_STRLEN_P(userinfo) > 0) {
+        smart_str_appendl(&buf, Z_STRVAL_P(userinfo), Z_STRLEN_P(userinfo));
+        smart_str_appends(&buf, "@");
+    }
+
+    smart_str_appendl(&buf, Z_STRVAL_P(host), Z_STRLEN_P(host));
+
+    if (Z_TYPE_P(port) == IS_LONG) {
+        smart_str_appends(&buf, ":");
+        smart_str_append_long(&buf, Z_LVAL_P(port));
+    }
+
+    RETVAL_STR_COPY(buf.s);
+    zend_string_release(buf.s);
 }
 
 
@@ -135,23 +251,22 @@ PHP_METHOD(Uri, getUserInfo)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("userInfo"), 0, &rv);
+    value = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("userInfo"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Uri, withUserInfo)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(HttpMessage_Uri_ce, return_value, ZEND_STRL("userInfo"), value, value_len);
+    zend_update_property_str(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("userInfo"), value);
 }
 
 
@@ -161,23 +276,22 @@ PHP_METHOD(Uri, getHost)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("host"), 0, &rv);
+    value = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("host"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Uri, withHost)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(HttpMessage_Uri_ce, return_value, ZEND_STRL("host"), value, value_len);
+    zend_update_property_str(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("host"), value);
 }
 
 
@@ -187,22 +301,27 @@ PHP_METHOD(Uri, getPort)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("port"), 0, &rv);
+    value = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("port"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Uri, withPort)
 {
-    long value;
+    zend_long value = 0;
+    zend_bool value_is_null = 1;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_LONG(value)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_LONG_EX(value, value_is_null,1, 0)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_long(HttpMessage_Uri_ce, return_value, ZEND_STRL("port"), value);
+    if (!value_is_null) {
+        zend_update_property_long(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("port"), value);
+    } else {
+        zend_update_property_null(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("port"));
+    }
 }
 
 
@@ -212,23 +331,22 @@ PHP_METHOD(Uri, getPath)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("path"), 0, &rv);
+    value = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("path"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Uri, withPath)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(HttpMessage_Uri_ce, return_value, ZEND_STRL("path"), value, value_len);
+    zend_update_property_str(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("path"), value);
 }
 
 
@@ -238,49 +356,47 @@ PHP_METHOD(Uri, getQuery)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("query"), 0, &rv);
+    value = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("query"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Uri, withQuery)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(HttpMessage_Uri_ce, return_value, ZEND_STRL("query"), value, value_len);
+    zend_update_property_str(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("query"), value);
 }
 
 
-/* query */
+/* fragment */
 
 PHP_METHOD(Uri, getFragment)
 {
     zval rv, *value;
 
-    value = zend_read_property(HttpMessage_Uri_ce, getThis(), ZEND_STRL("fragment"), 0, &rv);
+    value = zend_read_property(HttpMessage_Uri_ce, PROPERTY_ARG(getThis()), ZEND_STRL("fragment"), 0, &rv);
 
     RETURN_ZVAL(value, 1, 0);
 }
 
 PHP_METHOD(Uri, withFragment)
 {
-    char *value;
-    size_t value_len;
+    zend_string *value = NULL;
 
     ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
-        Z_PARAM_STRING(value, value_len)
-    ZEND_PARSE_PARAMETERS_END_EX();
+        Z_PARAM_STR(value)
+    ZEND_PARSE_PARAMETERS_END();
 
-    ZVAL_OBJ(return_value, zend_objects_clone_obj(getThis()));
+    ZVAL_OBJ(return_value, zend_objects_clone_obj(PROPERTY_ARG(getThis())));
 
-    zend_update_property_stringl(HttpMessage_Uri_ce, return_value, ZEND_STRL("fragment"), value, value_len);
+    zend_update_property_str(HttpMessage_Uri_ce, PROPERTY_ARG(return_value), ZEND_STRL("fragment"), value);
 }
 
 
@@ -310,19 +426,23 @@ static const zend_function_entry uri_functions[] = {
 PHP_MINIT_FUNCTION(http_message_uri)
 {
     zend_class_entry ce;
+    zend_class_entry *interface = HTTP_MESSAGE_PSR_INTERFACE("uri");
+
+    ASSERT_HTTP_MESSAGE_INTERFACE_FOUND(interface, "Uri");
+
     INIT_NS_CLASS_ENTRY(ce, "HttpMessage", "Uri", uri_functions);
 
     HttpMessage_Uri_ce = zend_register_internal_class(&ce);
-    zend_class_implements(HttpMessage_Uri_ce, 1, PsrHttpMessageUriInterface_ce_ptr);
+    zend_class_implements(HttpMessage_Uri_ce, 1, interface);
 
     /* Properties */
-    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("scheme"), "", ZEND_ACC_PROTECTED);
-    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("userInfo"), "", ZEND_ACC_PROTECTED);
-    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("host"), "", ZEND_ACC_PROTECTED);
-    zend_declare_property_null(HttpMessage_Uri_ce, ZEND_STRL("port"), ZEND_ACC_PROTECTED);
-    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("path"), "", ZEND_ACC_PROTECTED);
-    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("query"), "", ZEND_ACC_PROTECTED);
-    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("fragment"), "", ZEND_ACC_PROTECTED);
+    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("scheme"), "", ZEND_ACC_PRIVATE);
+    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("userInfo"), "", ZEND_ACC_PRIVATE);
+    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("host"), "", ZEND_ACC_PRIVATE);
+    zend_declare_property_null(HttpMessage_Uri_ce, ZEND_STRL("port"), ZEND_ACC_PRIVATE);
+    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("path"), "", ZEND_ACC_PRIVATE);
+    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("query"), "", ZEND_ACC_PRIVATE);
+    zend_declare_property_string(HttpMessage_Uri_ce, ZEND_STRL("fragment"), "", ZEND_ACC_PRIVATE);
 
     return SUCCESS;
 }
