[THRIFT-5757] Unit tests for php lib
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f003db2..0b29ddb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -90,7 +90,7 @@
         uses: shivammathur/setup-php@v2
         with:
           php-version: ${{ matrix.php-version }}
-          extensions: mbstring, intl, xml
+          extensions: mbstring, intl, xml, curl
           ini-values: "error_reporting=E_ALL"
 
       - name: Install Dependencies
diff --git a/composer.json b/composer.json
index 77248a9..900fb28 100644
--- a/composer.json
+++ b/composer.json
@@ -23,8 +23,10 @@
     "require-dev": {
         "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
         "squizlabs/php_codesniffer": "3.*",
+        "php-mock/php-mock-phpunit": "^2.10",
         "ext-json": "*",
-        "ext-xml": "*"
+        "ext-xml": "*",
+        "ext-curl": "*"
     },
     "autoload": {
         "psr-4": {"Thrift\\": "lib/php/lib/"}
diff --git a/lib/php/lib/Transport/TBufferedTransport.php b/lib/php/lib/Transport/TBufferedTransport.php
index 253c5ac..e3a40a4 100644
--- a/lib/php/lib/Transport/TBufferedTransport.php
+++ b/lib/php/lib/Transport/TBufferedTransport.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
diff --git a/lib/php/lib/Transport/TCurlClient.php b/lib/php/lib/Transport/TCurlClient.php
index 2087433..709798e 100644
--- a/lib/php/lib/Transport/TCurlClient.php
+++ b/lib/php/lib/Transport/TCurlClient.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -227,7 +228,6 @@
             register_shutdown_function(array('Thrift\\Transport\\TCurlClient', 'closeCurlHandle'));
             self::$curlHandle = curl_init();
             curl_setopt(self::$curlHandle, CURLOPT_RETURNTRANSFER, true);
-            curl_setopt(self::$curlHandle, CURLOPT_BINARYTRANSFER, true);
             curl_setopt(self::$curlHandle, CURLOPT_USERAGENT, 'PHP/TCurlClient');
             curl_setopt(self::$curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
             curl_setopt(self::$curlHandle, CURLOPT_FOLLOWLOCATION, true);
@@ -238,9 +238,11 @@
         $fullUrl = $this->scheme_ . "://" . $host . $this->uri_;
 
         $headers = array();
-        $defaultHeaders = array('Accept' => 'application/x-thrift',
+        $defaultHeaders = array(
+            'Accept' => 'application/x-thrift',
             'Content-Type' => 'application/x-thrift',
-            'Content-Length' => TStringFuncFactory::create()->strlen($this->request_));
+            'Content-Length' => TStringFuncFactory::create()->strlen($this->request_)
+        );
         foreach (array_merge($defaultHeaders, $this->headers_) as $key => $value) {
             $headers[] = "$key: $value";
         }
@@ -292,10 +294,11 @@
     {
         try {
             if (self::$curlHandle) {
-                curl_close(self::$curlHandle);
+                curl_close(self::$curlHandle); #This function has no effect. Prior to PHP 8.0.0, this function was used to close the resource.
                 self::$curlHandle = null;
             }
         } catch (\Exception $x) {
+            #it's not possible to throw an exception by calling a function that has no effect
             error_log('There was an error closing the curl handle: ' . $x->getMessage());
         }
     }
diff --git a/lib/php/lib/Transport/THttpClient.php b/lib/php/lib/Transport/THttpClient.php
index 4d6be32..0f767f4 100644
--- a/lib/php/lib/Transport/THttpClient.php
+++ b/lib/php/lib/Transport/THttpClient.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -212,11 +213,14 @@
         $host = $this->host_ . ($this->port_ != 80 ? ':' . $this->port_ : '');
 
         $headers = array();
-        $defaultHeaders = array('Host' => $host,
+        $defaultHeaders = array(
+            'Host' => $host,
             'Accept' => 'application/x-thrift',
             'User-Agent' => 'PHP/THttpClient',
             'Content-Type' => 'application/x-thrift',
-            'Content-Length' => TStringFuncFactory::create()->strlen($this->buf_));
+            'Content-Length' => TStringFuncFactory::create()->strlen($this->buf_)
+        );
+
         foreach (array_merge($defaultHeaders, $this->headers_) as $key => $value) {
             $headers[] = "$key: $value";
         }
@@ -225,10 +229,12 @@
 
         $baseHttpOptions = isset($options["http"]) ? $options["http"] : array();
 
-        $httpOptions = $baseHttpOptions + array('method' => 'POST',
+        $httpOptions = $baseHttpOptions + array(
+            'method' => 'POST',
             'header' => implode("\r\n", $headers),
             'max_redirects' => 1,
-            'content' => $this->buf_);
+            'content' => $this->buf_
+        );
         if ($this->timeout_ > 0) {
             $httpOptions['timeout'] = $this->timeout_;
         }
diff --git a/lib/php/lib/Transport/TMemoryBuffer.php b/lib/php/lib/Transport/TMemoryBuffer.php
index fee03a2..e5da9da 100644
--- a/lib/php/lib/Transport/TMemoryBuffer.php
+++ b/lib/php/lib/Transport/TMemoryBuffer.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -35,6 +36,8 @@
  */
 class TMemoryBuffer extends TTransport
 {
+    protected $buf_ = '';
+
     /**
      * Constructor. Optionally pass an initial value
      * for the buffer.
@@ -44,8 +47,6 @@
         $this->buf_ = $buf;
     }
 
-    protected $buf_ = '';
-
     public function isOpen()
     {
         return true;
diff --git a/lib/php/lib/Transport/TPhpStream.php b/lib/php/lib/Transport/TPhpStream.php
index 42823ff..2350b96 100644
--- a/lib/php/lib/Transport/TPhpStream.php
+++ b/lib/php/lib/Transport/TPhpStream.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -53,7 +54,7 @@
     public function open()
     {
         if ($this->read_) {
-            $this->inStream_ = @fopen(self::inStreamName(), 'r');
+            $this->inStream_ = @fopen($this->inStreamName(), 'r');
             if (!is_resource($this->inStream_)) {
                 throw new TException('TPhpStream: Could not open php://input');
             }
@@ -113,7 +114,7 @@
         @fflush($this->outStream_);
     }
 
-    private static function inStreamName()
+    private function inStreamName()
     {
         if (php_sapi_name() == 'cli') {
             return 'php://stdin';
diff --git a/lib/php/lib/Transport/TSSLSocket.php b/lib/php/lib/Transport/TSSLSocket.php
index b4a0adb..16956e7 100644
--- a/lib/php/lib/Transport/TSSLSocket.php
+++ b/lib/php/lib/Transport/TSSLSocket.php
@@ -36,7 +36,7 @@
     /**
      * Remote port
      *
-     * @var resource
+     * @var null|resource
      */
     protected $context_ = null;
 
@@ -57,6 +57,10 @@
     ) {
         $this->host_ = $this->getSSLHost($host);
         $this->port_ = $port;
+        // Initialize a stream context if not provided
+        if ($context === null) {
+            $context = stream_context_create();
+        }
         $this->context_ = $context;
         $this->debugHandler_ = $debugHandler ? $debugHandler : 'error_log';
     }
@@ -87,7 +91,8 @@
             throw new TTransportException('Socket already connected', TTransportException::ALREADY_OPEN);
         }
 
-        if (empty($this->host_)) {
+        $host = parse_url($this->host_, PHP_URL_HOST);
+        if (empty($host)) {
             throw new TTransportException('Cannot open null host', TTransportException::NOT_OPEN);
         }
 
diff --git a/lib/php/lib/Transport/TSocket.php b/lib/php/lib/Transport/TSocket.php
index 8fe60fd..fb74fdb 100644
--- a/lib/php/lib/Transport/TSocket.php
+++ b/lib/php/lib/Transport/TSocket.php
@@ -252,8 +252,10 @@
 
         if (function_exists('socket_import_stream') && function_exists('socket_set_option')) {
             // warnings silenced due to bug https://bugs.php.net/bug.php?id=70939
-            $socket = @socket_import_stream($this->handle_);
-            @socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);
+            $socket = socket_import_stream($this->handle_);
+            if ($socket !== false) {
+                @socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);
+            }
         }
     }
 
diff --git a/lib/php/lib/Transport/TSocketPool.php b/lib/php/lib/Transport/TSocketPool.php
index 307885f..312e023 100644
--- a/lib/php/lib/Transport/TSocketPool.php
+++ b/lib/php/lib/Transport/TSocketPool.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -25,24 +26,6 @@
 use Thrift\Exception\TException;
 
 /**
- * This library makes use of APCu cache to make hosts as down in a web
- * environment. If you are running from the CLI or on a system without APCu
- * installed, then these null functions will step in and act like cache
- * misses.
- */
-if (!function_exists('apcu_fetch')) {
-    function apcu_fetch($key)
-    {
-        return false;
-    }
-
-    function apcu_store($key, $var, $ttl = 0)
-    {
-        return false;
-    }
-}
-
-/**
  * Sockets implementation of the TTransport interface that allows connection
  * to a pool of servers.
  *
@@ -92,6 +75,12 @@
     private $alwaysTryLast_ = true;
 
     /**
+     * Use apcu cache
+     * @var bool
+     */
+    private $useApcuCache;
+
+    /**
      * Socket pool constructor
      *
      * @param array $hosts List of remote hostnames
@@ -116,9 +105,13 @@
         }
 
         foreach ($hosts as $key => $host) {
-            $this->servers_ [] = array('host' => $host,
-                'port' => $ports[$key]);
+            $this->servers_ [] = array(
+                'host' => $host,
+                'port' => $ports[$key]
+            );
         }
+
+        $this->useApcuCache = function_exists('apcu_fetch');
     }
 
     /**
@@ -206,7 +199,7 @@
             $failtimeKey = 'thrift_failtime:' . $host . ':' . $port . '~';
 
             // Cache miss? Assume it's OK
-            $lastFailtime = apcu_fetch($failtimeKey);
+            $lastFailtime = $this->apcuFetch($failtimeKey);
             if ($lastFailtime === false) {
                 $lastFailtime = 0;
             }
@@ -251,7 +244,7 @@
 
                         // Only clear the failure counts if required to do so
                         if ($lastFailtime > 0) {
-                            apcu_store($failtimeKey, 0);
+                            $this->apcuStore($failtimeKey, 0);
                         }
 
                         // Successful connection, return now
@@ -265,7 +258,7 @@
                 $consecfailsKey = 'thrift_consecfails:' . $host . ':' . $port . '~';
 
                 // Ignore cache misses
-                $consecfails = apcu_fetch($consecfailsKey);
+                $consecfails = $this->apcuFetch($consecfailsKey);
                 if ($consecfails === false) {
                     $consecfails = 0;
                 }
@@ -284,12 +277,12 @@
                         );
                     }
                     // Store the failure time
-                    apcu_store($failtimeKey, time());
+                    $this->apcuStore($failtimeKey, time());
 
                     // Clear the count of consecutive failures
-                    apcu_store($consecfailsKey, 0);
+                    $this->apcuStore($consecfailsKey, 0);
                 } else {
-                    apcu_store($consecfailsKey, $consecfails);
+                    $this->apcuStore($consecfailsKey, $consecfails);
                 }
             }
         }
@@ -307,4 +300,20 @@
         }
         throw new TException($error);
     }
+
+    /**
+     * This library makes use of APCu cache to make hosts as down in a web
+     * environment. If you are running from the CLI or on a system without APCu
+     * installed, then these null functions will step in and act like cache
+     * misses.
+     */
+    private function apcuFetch($key, &$success = null)
+    {
+        return $this->useApcuCache ? apcu_fetch($key, $success) : false;
+    }
+
+    private function apcuStore($key, $var, $ttl = 0)
+    {
+        return $this->useApcuCache ? apcu_store($key, $var, $ttl) : false;
+    }
 }
diff --git a/lib/php/phpunit.xml b/lib/php/phpunit.xml
index 53b3f35..2cbea95 100644
--- a/lib/php/phpunit.xml
+++ b/lib/php/phpunit.xml
@@ -28,12 +28,11 @@
          stopOnFailure="true"
          processIsolation="true"
          xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
-    <coverage includeUncoveredFiles="true">
-        <include>
-            <directory suffix=".php">./src</directory>
+    <filter>
+        <whitelist processUncoveredFilesFromWhitelist="true">
             <directory suffix=".php">./lib</directory>
-        </include>
-    </coverage>
+        </whitelist>
+    </filter>
     <testsuites>
         <testsuite name="Thrift PHP Test Suite">
             <directory>./test/Unit</directory>
diff --git a/lib/php/test/Fixtures/Fixtures.php b/lib/php/test/Fixtures/Fixtures.php
index eb348fc..d48be40 100644
--- a/lib/php/test/Fixtures/Fixtures.php
+++ b/lib/php/test/Fixtures/Fixtures.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.test
  */
 
 namespace Test\Thrift\Fixtures;
diff --git a/lib/php/test/Fixtures/TJSONProtocolFixtures.php b/lib/php/test/Fixtures/TJSONProtocolFixtures.php
index 81ada77..77fb270 100644
--- a/lib/php/test/Fixtures/TJSONProtocolFixtures.php
+++ b/lib/php/test/Fixtures/TJSONProtocolFixtures.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.test
  */
 
 namespace Test\Thrift\Fixtures;
diff --git a/lib/php/test/Fixtures/TSimpleJSONProtocolFixtures.php b/lib/php/test/Fixtures/TSimpleJSONProtocolFixtures.php
index 448eb61..0281a87 100644
--- a/lib/php/test/Fixtures/TSimpleJSONProtocolFixtures.php
+++ b/lib/php/test/Fixtures/TSimpleJSONProtocolFixtures.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.test
  */
 
 namespace Test\Thrift\Fixtures;
diff --git a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/A/TestClass.php b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/A/TestClass.php
index e39f501..3652b25 100644
--- a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/A/TestClass.php
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/A/TestClass.php
@@ -17,11 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * ClassLoader to load Thrift library and definitions
- * Inspired from UniversalClassLoader from Symfony 2
- *
- * @package thrift.classloader
  */
 
 namespace A;
diff --git a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/B/TestClass.php b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/B/TestClass.php
index d2e87e2..1d5a543 100644
--- a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/B/TestClass.php
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/B/TestClass.php
@@ -17,11 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * ClassLoader to load Thrift library and definitions
- * Inspired from UniversalClassLoader from Symfony 2
- *
- * @package thrift.classloader
  */
 
 namespace B;
diff --git a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/C/TestClass.php b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/C/TestClass.php
index 9e4000c..58bae58 100644
--- a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/C/TestClass.php
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/C/TestClass.php
@@ -17,11 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * ClassLoader to load Thrift library and definitions
- * Inspired from UniversalClassLoader from Symfony 2
- *
- * @package thrift.classloader
  */
 
 namespace C;
diff --git a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/D/TestClass.php b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/D/TestClass.php
index c0cda0c..592fe56 100644
--- a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/D/TestClass.php
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/D/TestClass.php
@@ -17,11 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * ClassLoader to load Thrift library and definitions
- * Inspired from UniversalClassLoader from Symfony 2
- *
- * @package thrift.classloader
  */
 
 namespace D;
diff --git a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/E/TestClass.php b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/E/TestClass.php
index b1981b4..56b5679 100644
--- a/lib/php/test/Unit/Lib/ClassLoader/Fixtures/E/TestClass.php
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/E/TestClass.php
@@ -17,11 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * ClassLoader to load Thrift library and definitions
- * Inspired from UniversalClassLoader from Symfony 2
- *
- * @package thrift.classloader
  */
 
 namespace E;
diff --git a/lib/php/test/Unit/Lib/ClassLoader/ThriftClassLoaderTest.php b/lib/php/test/Unit/Lib/ClassLoader/ThriftClassLoaderTest.php
index 1180211..46ed2ec 100644
--- a/lib/php/test/Unit/Lib/ClassLoader/ThriftClassLoaderTest.php
+++ b/lib/php/test/Unit/Lib/ClassLoader/ThriftClassLoaderTest.php
@@ -21,6 +21,7 @@
 
 namespace Test\Thrift\Unit\Lib\ClassLoader;
 
+use phpmock\phpunit\PHPMock;
 use PHPUnit\Framework\TestCase;
 use Thrift\ClassLoader\ThriftClassLoader;
 
@@ -30,7 +31,7 @@
  */
 class ThriftClassLoaderTest extends TestCase
 {
-    const APCU_PREFIX = 'test';
+    use PHPMock;
 
     /**
      * @dataProvider registerNamespaceDataProvider
@@ -42,6 +43,16 @@
         $useApcu = false,
         $apcuPrefix = null
     ) {
+        $this->getFunctionMock('Thrift\ClassLoader', 'apcu_fetch')
+             ->expects($useApcu ? $this->once() : $this->never())
+             ->with($apcuPrefix . $class)
+             ->willReturn(false);
+
+        $this->getFunctionMock('Thrift\ClassLoader', 'apcu_store')
+             ->expects($useApcu ? $this->once() : $this->never())
+             ->with($apcuPrefix . $class, $this->anything())
+             ->willReturn(true);
+
         $loader = new ThriftClassLoader($useApcu, $apcuPrefix);
         foreach ($namespaces as $namespace => $paths) {
             $loader->registerNamespace($namespace, $paths);
@@ -97,7 +108,7 @@
             'class' => '\E\TestClass',
             'isClassExist' => true,
             'useApcu' => true,
-            'apcuPrefix' => self::APCU_PREFIX,
+            'apcuPrefix' => 'APCU_PREFIX',
         ];
     }
 
@@ -111,6 +122,16 @@
         $useApcu = false,
         $apcuPrefix = null
     ) {
+        $this->getFunctionMock('Thrift\ClassLoader', 'apcu_fetch')
+             ->expects($useApcu ? $this->once() : $this->never())
+             ->with($apcuPrefix . $class)
+             ->willReturn(false);
+
+        $this->getFunctionMock('Thrift\ClassLoader', 'apcu_store')
+            ->expects($useApcu ? $this->once() : $this->never())
+             ->with($apcuPrefix . $class, $this->anything())
+             ->willReturn(true);
+
         $loader = new ThriftClassLoader($useApcu, $apcuPrefix);
         foreach ($definitions as $namespace => $paths) {
             $loader->registerDefinition($namespace, $paths);
@@ -191,33 +212,7 @@
             'class' => '\TestValidators\TestServiceClient',
             'checkInterfaceExist' => false,
             'useApcu' => true,
-            'apcuPrefix' => self::APCU_PREFIX,
+            'apcuPrefix' => 'APCU_PREFIX',
         ];
     }
 }
-
-namespace Thrift\ClassLoader;
-
-use Test\Thrift\Unit\Lib\ClassLoader\ThriftClassLoaderTest;
-
-if (!function_exists('apcu_fetch')) {
-    {
-        function apcu_fetch($key, &$success = null)
-        {
-            if (strpos($key, ThriftClassLoaderTest::APCU_PREFIX) === false) {
-                throw new \Exception('apcu_fetch error, invalid key');
-            }
-
-            return false;
-        }
-
-        function apcu_store($key, $var, $ttl = 0)
-        {
-            if (strpos($key, ThriftClassLoaderTest::APCU_PREFIX) === false) {
-                throw new \Exception('apcu_store error, invalid key');
-            }
-
-            return false;
-        }
-    }
-}
diff --git a/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php
index 5f7c2a2..76ff187 100644
--- a/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php
+++ b/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.protocol
  */
 
 namespace Test\Thrift\Unit\Lib\Factory;
diff --git a/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php
index f81e789..1483c6a 100644
--- a/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php
+++ b/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.protocol
  */
 
 namespace Test\Thrift\Unit\Lib\Factory;
diff --git a/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php
index 2cb32d8..3b8b5cc 100644
--- a/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php
+++ b/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.protocol
  */
 
 namespace Test\Thrift\Unit\Lib\Factory;
diff --git a/lib/php/test/Unit/Lib/Factory/TJSONProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TJSONProtocolFactoryTest.php
index 0685af7..9c7055d 100644
--- a/lib/php/test/Unit/Lib/Factory/TJSONProtocolFactoryTest.php
+++ b/lib/php/test/Unit/Lib/Factory/TJSONProtocolFactoryTest.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.protocol
  */
 
 namespace Test\Thrift\Unit\Lib\Factory;
diff --git a/lib/php/test/Unit/Lib/Factory/TStringFuncFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TStringFuncFactoryTest.php
index eb4df44..c6feb2c 100644
--- a/lib/php/test/Unit/Lib/Factory/TStringFuncFactoryTest.php
+++ b/lib/php/test/Unit/Lib/Factory/TStringFuncFactoryTest.php
@@ -17,12 +17,11 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.protocol
  */
 
 namespace Test\Thrift\Unit\Lib\Factory;
 
+use phpmock\phpunit\PHPMock;
 use PHPUnit\Framework\TestCase;
 use Thrift\Factory\TStringFuncFactory;
 use Thrift\StringFunc\Core;
@@ -31,16 +30,21 @@
 
 class TStringFuncFactoryTest extends TestCase
 {
-    /**
-     * @return void
-     */
-    public function testCreate()
-    {
-        $factory = new TStringFuncFactory();
-        $stringFunc = $factory::create();
-        $this->assertInstanceOf(TStringFunc::class, $stringFunc);
-        $this->assertInstanceOf(Mbstring::class, $stringFunc);
+    use PHPMock;
 
+    /**
+     * @dataProvider createDataProvider
+     */
+    public function testCreate(
+        $mbstringFuncOverload,
+        $expectedClass
+    ) {
+        $this->getFunctionMock('Thrift\Factory', 'ini_get')
+             ->expects($this->once())
+             ->with('mbstring.func_overload')
+             ->willReturn($mbstringFuncOverload);
+
+        $factory = new TStringFuncFactory();
         /**
          * it is a hack to nullable the instance of TStringFuncFactory, and get a new instance based on the new ini_get value
          */
@@ -50,25 +54,21 @@
         $refInstance->setValue($factory, null);
 
         $stringFunc = $factory::create();
+
         $this->assertInstanceOf(TStringFunc::class, $stringFunc);
-        $this->assertInstanceOf(Core::class, $stringFunc);
+        $this->assertInstanceOf($expectedClass, $stringFunc);
     }
-}
 
+    public function createDataProvider()
+    {
+        yield 'mbstring' => [
+            'mbstring.func_overload' => 2,
+            'expected' => Mbstring::class
+        ];
 
-namespace Thrift\Factory;
-
-function ini_get($key)
-{
-    static $count = 0;
-    if ($key === 'mbstring.func_overload') {
-        if ($count === 0) {
-            $count++;
-            return 2;
-        } else {
-            return 0;
-        }
-    } else {
-        return \ini_get($key);
+        yield 'string' => [
+            'mbstring.func_overload' => 0,
+            'expected' => Core::class
+        ];
     }
 }
diff --git a/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php
index da91b64..a8a791a 100644
--- a/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php
+++ b/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php
@@ -17,8 +17,6 @@
  * KIND, either express or implied. See the License for the
  * specific language governing permissions and limitations
  * under the License.
- *
- * @package thrift.protocol
  */
 
 namespace Test\Thrift\Unit\Lib\Factory;
diff --git a/lib/php/test/Unit/Lib/Transport/TBufferedTransportTest.php b/lib/php/test/Unit/Lib/Transport/TBufferedTransportTest.php
new file mode 100644
index 0000000..dd6003a
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TBufferedTransportTest.php
@@ -0,0 +1,286 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Transport\TBufferedTransport;
+use Thrift\Transport\TTransport;
+
+class TBufferedTransportTest extends TestCase
+{
+    public function testIsOpen()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport);
+
+        $transport
+            ->expects($this->once())
+            ->method('isOpen')
+            ->willReturn(true);
+
+        $this->assertTrue($bufferedTransport->isOpen());
+    }
+
+    public function testOpen()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport);
+
+        $transport
+            ->expects($this->once())
+            ->method('open')
+            ->willReturn(null);
+
+        $this->assertNull($bufferedTransport->open());
+    }
+
+    public function testClose()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport);
+
+        $transport
+            ->expects($this->once())
+            ->method('close')
+            ->willReturn(null);
+
+        $this->assertNull($bufferedTransport->close());
+    }
+
+    public function testPutBack()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport);
+        $bufferedTransport->putBack('test');
+
+        $ref = new \ReflectionClass($bufferedTransport);
+        $property = $ref->getProperty('rBuf_');
+        $property->setAccessible(true);
+        $this->assertEquals('test', $property->getValue($bufferedTransport));
+
+        $bufferedTransport->putBack('abcde');
+        $this->assertEquals('abcdetest', $property->getValue($bufferedTransport));
+    }
+
+    /**
+     * @dataProvider readAllDataProvider
+     */
+    public function testReadAll(
+        $startBuffer,
+        $readLength,
+        $bufferReadLength,
+        $bufferReadResult,
+        $expectedBufferValue,
+        $expectedRead
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport);
+        $bufferedTransport->putBack($startBuffer);
+
+        $transport
+            ->expects($bufferReadLength > 0 ? $this->once() : $this->never())
+            ->method('readAll')
+            ->with($bufferReadLength)
+            ->willReturn($bufferReadResult);
+
+        $this->assertEquals($expectedRead, $bufferedTransport->readAll($readLength));
+
+        $ref = new \ReflectionClass($bufferedTransport);
+        $property = $ref->getProperty('rBuf_');
+        $property->setAccessible(true);
+        $this->assertEquals($expectedBufferValue, $property->getValue($bufferedTransport));
+    }
+
+    public function readAllDataProvider()
+    {
+        yield 'buffer empty' => [
+            'startBuffer' => '',
+            'readLength' => 5,
+            'bufferReadLength' => 5,
+            'bufferReadResult' => '12345',
+            'expectedBufferValue' => '',
+            'expectedRead' => '12345',
+        ];
+        yield 'buffer have partly loaded data' => [
+            'startBuffer' => '12345',
+            'readLength' => 10,
+            'bufferReadLength' => 5,
+            'bufferReadResult' => '67890',
+            'expectedBufferValue' => '',
+            'expectedRead' => '1234567890',
+        ];
+        yield 'buffer fully read' => [
+            'startBuffer' => '12345',
+            'readLength' => 5,
+            'bufferReadLength' => 0,
+            'bufferReadResult' => '',
+            'expectedBufferValue' => '',
+            'expectedRead' => '12345',
+        ];
+        yield 'request less data that we have in buffer' => [
+            'startBuffer' => '12345',
+            'readLength' => 3,
+            'bufferReadLength' => 0,
+            'bufferReadResult' => '',
+            'expectedBufferValue' => '45',
+            'expectedRead' => '123',
+        ];
+    }
+
+    /**
+     * @dataProvider readDataProvider
+     */
+    public function testRead(
+        $readBufferSize,
+        $startBuffer,
+        $readLength,
+        $bufferReadResult,
+        $expectedBufferValue,
+        $expectedRead
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport, $readBufferSize);
+        $bufferedTransport->putBack($startBuffer);
+
+        $transport
+            ->expects(empty($startBuffer) > 0 ? $this->once() : $this->never())
+            ->method('read')
+            ->with($readBufferSize)
+            ->willReturn($bufferReadResult);
+
+        $this->assertEquals($expectedRead, $bufferedTransport->read($readLength));
+
+        $ref = new \ReflectionClass($bufferedTransport);
+        $property = $ref->getProperty('rBuf_');
+        $property->setAccessible(true);
+        $this->assertEquals($expectedBufferValue, $property->getValue($bufferedTransport));
+    }
+
+    public function readDataProvider()
+    {
+        yield 'buffer empty' => [
+            'readBufferSize' => 10,
+            'startBuffer' => '',
+            'readLength' => 5,
+            'bufferReadResult' => '12345',
+            'expectedBufferValue' => '',
+            'expectedRead' => '12345',
+        ];
+        yield 'buffer read partly' => [
+            'readBufferSize' => 10,
+            'startBuffer' => '',
+            'readLength' => 5,
+            'bufferReadResult' => '1234567890',
+            'expectedBufferValue' => '67890',
+            'expectedRead' => '12345',
+        ];
+        yield 'buffer fully read' => [
+            'readBufferSize' => 10,
+            'startBuffer' => '12345',
+            'readLength' => 5,
+            'bufferReadResult' => '',
+            'expectedBufferValue' => '',
+            'expectedRead' => '12345',
+        ];
+    }
+
+    /**
+     * @dataProvider writeDataProvider
+     */
+    public function testWrite(
+        $writeBufferSize,
+        $writeData,
+        $bufferedTransportCall,
+        $expectedWriteBufferValue
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport, 512, $writeBufferSize);
+
+        $transport
+            ->expects($this->exactly($bufferedTransportCall))
+            ->method('write')
+            ->with($writeData)
+            ->willReturn(null);
+
+        $this->assertNull($bufferedTransport->write($writeData));
+
+        $ref = new \ReflectionClass($bufferedTransport);
+        $property = $ref->getProperty('wBuf_');
+        $property->setAccessible(true);
+        $this->assertEquals($expectedWriteBufferValue, $property->getValue($bufferedTransport));
+    }
+
+    public function writeDataProvider()
+    {
+        yield 'store data in buffer' => [
+            'writeBufferSize' => 10,
+            'writeData' => '12345',
+            'bufferedTransportCall' => 0,
+            'expectedWriteBufferValue' => '12345',
+        ];
+        yield 'send data to buffered transport' => [
+            'writeBufferSize' => 10,
+            'writeData' => '12345678901',
+            'bufferedTransportCall' => 1,
+            'expectedWriteBufferValue' => '',
+        ];
+    }
+
+    /**
+     * @dataProvider flushDataProvider
+     */
+    public function testFlush(
+        $writeBuffer
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $bufferedTransport = new TBufferedTransport($transport, 512, 512);
+        $ref = new \ReflectionClass($bufferedTransport);
+        $property = $ref->getProperty('wBuf_');
+        $property->setAccessible(true);
+        $property->setValue($bufferedTransport, $writeBuffer);
+
+        $transport
+            ->expects(!empty($writeBuffer) ? $this->once() : $this->never())
+            ->method('write')
+            ->with($writeBuffer)
+            ->willReturn(null);
+
+        $transport
+            ->expects($this->once())
+            ->method('flush')
+            ->willReturn(null);
+
+        $this->assertNull($bufferedTransport->flush());
+
+        $this->assertEquals('', $property->getValue($bufferedTransport));
+    }
+
+    public function flushDataProvider()
+    {
+        yield 'empty buffer' => [
+            'writeBuffer' => '',
+        ];
+        yield 'not empty buffer' => [
+            'writeBuffer' => '12345',
+        ];
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TCurlClientTest.php b/lib/php/test/Unit/Lib/Transport/TCurlClientTest.php
new file mode 100644
index 0000000..7cd7446
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TCurlClientTest.php
@@ -0,0 +1,423 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TTransportException;
+use Thrift\Transport\TCurlClient;
+
+class TCurlClientTest extends TestCase
+{
+    use PHPMock;
+
+    public function testSetTimeoutSecs()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+        $transport->setTimeoutSecs(1000);
+
+        $ref = new \ReflectionClass($transport);
+        $prop = $ref->getProperty('timeout_');
+        $prop->setAccessible(true);
+        $this->assertEquals(1000, $prop->getValue($transport));
+    }
+
+    public function testSetConnectionTimeoutSecs()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+        $transport->setConnectionTimeoutSecs(1000);
+
+        $ref = new \ReflectionClass($transport);
+        $prop = $ref->getProperty('connectionTimeout_');
+        $prop->setAccessible(true);
+        $this->assertEquals(1000, $prop->getValue($transport));
+    }
+
+    public function testIsOpen()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+        $this->assertTrue($transport->isOpen());
+    }
+
+    public function testOpen()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+        $this->assertNull($transport->open());
+    }
+
+    public function testClose()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propRequest = $ref->getProperty('request_');
+        $propRequest->setAccessible(true);
+        $propRequest->setValue($transport, 'testRequest');
+        $propResponse = $ref->getProperty('response_');
+        $propResponse->setAccessible(true);
+        $propResponse->setValue($transport, 'testResponse');
+
+        $this->assertNull($transport->close());
+        $this->assertEmpty($propRequest->getValue($transport));
+        $this->assertEmpty($propResponse->getValue($transport));
+    }
+
+    public function testRead()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propResponse = $ref->getProperty('response_');
+        $propResponse->setAccessible(true);
+        $propResponse->setValue($transport, '1234567890');
+
+        $response = $transport->read(5);
+        $this->assertEquals('12345', $response);
+        $this->assertEquals('67890', $propResponse->getValue($transport));
+
+        $response = $transport->read(5);
+        $this->assertEquals('67890', $response);
+        # The response does not cleaned after reading full answer, maybe it should be fixed
+        $this->assertEquals('67890', $propResponse->getValue($transport));
+    }
+
+    public function testReadAll()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propResponse = $ref->getProperty('response_');
+        $propResponse->setAccessible(true);
+        $propResponse->setValue($transport, '1234567890');
+
+        $response = $transport->readAll(5);
+        $this->assertEquals('12345', $response);
+        $this->assertEquals('67890', $propResponse->getValue($transport));
+    }
+
+    public function testReadAll_THRIFT_4656()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propResponse = $ref->getProperty('response_');
+        $propResponse->setAccessible(true);
+        $propResponse->setValue($transport, '');
+
+        $this->expectException(TTransportException::class);
+        $this->expectExceptionMessage('TCurlClient could not read 5 bytes');
+        $this->expectExceptionCode(TTransportException::UNKNOWN);
+
+        $transport->readAll(5);
+    }
+
+    public function testWrite()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propRequest = $ref->getProperty('request_');
+        $propRequest->setAccessible(true);
+        $propRequest->setValue($transport, '1234567890');
+
+        $transport->write('12345');
+        $this->assertEquals('123456789012345', $propRequest->getValue($transport));
+    }
+
+    public function testAddHeaders()
+    {
+        $host = 'localhost';
+        $transport = new TCurlClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propRequest = $ref->getProperty('headers_');
+        $propRequest->setAccessible(true);
+        $propRequest->setValue($transport, ['test' => '1234567890']);
+
+        $transport->addHeaders(['test2' => '12345']);
+        $this->assertEquals(['test' => '1234567890', 'test2' => '12345'], $propRequest->getValue($transport));
+    }
+
+    /**
+     * @dataProvider flushDataProvider
+     */
+    public function testFlush(
+        $host,
+        $port,
+        $uri,
+        $scheme,
+        $headers,
+        $request,
+        $timeout,
+        $connectionTimeout,
+        $curlSetOptCalls,
+        $response,
+        $responseError,
+        $responseCode,
+        $expectedException = null,
+        $expectedMessage = null,
+        $expectedCode = null
+    ) {
+        $this->getFunctionMock('Thrift\\Transport', 'register_shutdown_function')
+             ->expects($this->once())
+             ->with(
+                 $this->callback(
+                     function ($arg) {
+                         return is_array(
+                                 $arg
+                             ) && $arg[0] === 'Thrift\\Transport\\TCurlClient' && $arg[1] === 'closeCurlHandle';
+                     }
+                 )
+             );
+        $this->getFunctionMock('Thrift\\Transport', 'curl_init')
+             ->expects($this->once());
+
+        $this->getFunctionMock('Thrift\\Transport', 'curl_setopt')
+             ->expects($this->any())
+             ->withConsecutive(...$curlSetOptCalls)
+             ->willReturn(true);
+
+        $this->getFunctionMock('Thrift\\Transport', 'curl_exec')
+             ->expects($this->once())
+             ->with($this->anything())
+             ->willReturn($response);
+
+        $this->getFunctionMock('Thrift\\Transport', 'curl_error')
+             ->expects($this->once())
+             ->with($this->anything())
+             ->willReturn($responseError);
+
+        $this->getFunctionMock('Thrift\\Transport', 'curl_getinfo')
+             ->expects($this->once())
+             ->with($this->anything(), CURLINFO_HTTP_CODE)
+             ->willReturn($responseCode);
+
+        if (!is_null($expectedException)) {
+            $this->expectException($expectedException);
+            $this->expectExceptionMessage($expectedMessage);
+            $this->expectExceptionCode($expectedCode);
+
+            $this->getFunctionMock('Thrift\\Transport', 'curl_close')
+                 ->expects($this->once())
+                 ->with($this->anything());
+        }
+
+        $transport = new TCurlClient($host, $port, $uri, $scheme);
+        if (!empty($headers)) {
+            $transport->addHeaders($headers);
+        }
+        $transport->write($request);
+        if (!empty($timeout)) {
+            $transport->setTimeoutSecs($timeout);
+        }
+        if (!empty($connectionTimeout)) {
+            $transport->setConnectionTimeoutSecs($connectionTimeout);
+        }
+
+        $transport->flush();
+    }
+
+    public function flushDataProvider()
+    {
+        $request = 'request';
+
+        $default = [
+            'host' => 'localhost',
+            'port' => 80,
+            'uri' => '',
+            'scheme' => 'http',
+            'headers' => [],
+            'request' => $request,
+            'timeout' => null,
+            'connectionTimeout' => null,
+            'curlSetOptCalls' => [
+                [$this->anything(), CURLOPT_RETURNTRANSFER, true],
+                [$this->anything(), CURLOPT_USERAGENT, 'PHP/TCurlClient'],
+                [$this->anything(), CURLOPT_CUSTOMREQUEST, 'POST'],
+                [$this->anything(), CURLOPT_FOLLOWLOCATION, true],
+                [$this->anything(), CURLOPT_MAXREDIRS, 1],
+                [
+                    $this->anything(),
+                    CURLOPT_HTTPHEADER,
+                    [
+                        'Accept: application/x-thrift',
+                        'Content-Type: application/x-thrift',
+                        'Content-Length: ' . strlen($request),
+                    ],
+                ],
+                [$this->anything(), CURLOPT_POSTFIELDS, $request],
+                [$this->anything(), CURLOPT_URL, 'http://localhost'],
+            ],
+            'response' => 'response',
+            'responseError' => '',
+            'responseCode' => 200,
+        ];
+
+        yield 'default' => $default;
+        yield 'additionalHeaders' => array_merge(
+            $default,
+            [
+                'headers' => ['test' => '1234567890'],
+                'curlSetOptCalls' => [
+                    [$this->anything(), CURLOPT_RETURNTRANSFER, true],
+                    [$this->anything(), CURLOPT_USERAGENT, 'PHP/TCurlClient'],
+                    [$this->anything(), CURLOPT_CUSTOMREQUEST, 'POST'],
+                    [$this->anything(), CURLOPT_FOLLOWLOCATION, true],
+                    [$this->anything(), CURLOPT_MAXREDIRS, 1],
+                    [
+                        $this->anything(),
+                        CURLOPT_HTTPHEADER,
+                        [
+                            'Accept: application/x-thrift',
+                            'Content-Type: application/x-thrift',
+                            'Content-Length: ' . strlen($request),
+                            'test: 1234567890',
+                        ],
+                    ],
+                    [$this->anything(), CURLOPT_POSTFIELDS, $request],
+                    [$this->anything(), CURLOPT_URL, 'http://localhost'],
+                ],
+            ]
+        );
+        yield 'uri' => array_merge(
+            $default,
+            [
+                'uri' => 'test1234567890',
+                'curlSetOptCalls' => [
+                    [$this->anything(), CURLOPT_RETURNTRANSFER, true],
+                    [$this->anything(), CURLOPT_USERAGENT, 'PHP/TCurlClient'],
+                    [$this->anything(), CURLOPT_CUSTOMREQUEST, 'POST'],
+                    [$this->anything(), CURLOPT_FOLLOWLOCATION, true],
+                    [$this->anything(), CURLOPT_MAXREDIRS, 1],
+                    [
+                        $this->anything(),
+                        CURLOPT_HTTPHEADER,
+                        [
+                            'Accept: application/x-thrift',
+                            'Content-Type: application/x-thrift',
+                            'Content-Length: ' . strlen($request),
+                        ],
+                    ],
+                    [$this->anything(), CURLOPT_POSTFIELDS, $request],
+                    [$this->anything(), CURLOPT_URL, 'http://localhost/test1234567890'],
+                ],
+            ]
+        );
+        yield 'timeout' => array_merge(
+            $default,
+            [
+                'timeout' => 10,
+                'connectionTimeout' => 10,
+                'curlSetOptCalls' => [
+                    [$this->anything(), CURLOPT_RETURNTRANSFER, true],
+                    [$this->anything(), CURLOPT_USERAGENT, 'PHP/TCurlClient'],
+                    [$this->anything(), CURLOPT_CUSTOMREQUEST, 'POST'],
+                    [$this->anything(), CURLOPT_FOLLOWLOCATION, true],
+                    [$this->anything(), CURLOPT_MAXREDIRS, 1],
+                    [
+                        $this->anything(),
+                        CURLOPT_HTTPHEADER,
+                        [
+                            'Accept: application/x-thrift',
+                            'Content-Type: application/x-thrift',
+                            'Content-Length: ' . strlen($request),
+                        ],
+                    ],
+                    [$this->anything(), CURLOPT_TIMEOUT, 10],
+                    [$this->anything(), CURLOPT_CONNECTTIMEOUT, 10],
+                    [$this->anything(), CURLOPT_POSTFIELDS, $request],
+                    [$this->anything(), CURLOPT_URL, 'http://localhost'],
+                ],
+            ]
+        );
+        yield 'timeout msec' => array_merge(
+            $default,
+            [
+                'timeout' => 0.1,
+                'connectionTimeout' => 0.1,
+                'curlSetOptCalls' => [
+                    [$this->anything(), CURLOPT_RETURNTRANSFER, true],
+                    [$this->anything(), CURLOPT_USERAGENT, 'PHP/TCurlClient'],
+                    [$this->anything(), CURLOPT_CUSTOMREQUEST, 'POST'],
+                    [$this->anything(), CURLOPT_FOLLOWLOCATION, true],
+                    [$this->anything(), CURLOPT_MAXREDIRS, 1],
+                    [
+                        $this->anything(),
+                        CURLOPT_HTTPHEADER,
+                        [
+                            'Accept: application/x-thrift',
+                            'Content-Type: application/x-thrift',
+                            'Content-Length: ' . strlen($request),
+                        ],
+                    ],
+                    [$this->anything(), CURLOPT_TIMEOUT_MS, 100],
+                    [$this->anything(), CURLOPT_CONNECTTIMEOUT_MS, 100],
+                    [$this->anything(), CURLOPT_POSTFIELDS, $request],
+                    [$this->anything(), CURLOPT_URL, 'http://localhost'],
+                ],
+            ]
+        );
+        yield 'curl_exec return false' => array_merge(
+            $default,
+            [
+                'response' => false,
+                'expectedException' => TTransportException::class,
+                'expectedMessage' => 'TCurlClient: Could not connect to http://localhost',
+                'expectedCode' => TTransportException::UNKNOWN,
+            ]
+        );
+        yield 'curl_exec return response code 403' => array_merge(
+            $default,
+            [
+                'responseError' => 'Access denied',
+                'responseCode' => 403,
+                'expectedException' => TTransportException::class,
+                'expectedMessage' => 'TCurlClient: Could not connect to http://localhost, Access denied, HTTP status code: 403',
+                'expectedCode' => TTransportException::UNKNOWN,
+            ]
+        );
+    }
+
+    public function testCloseCurlHandle()
+    {
+        $this->getFunctionMock('Thrift\\Transport', 'curl_close')
+             ->expects($this->once())
+             ->with('testHandle');
+
+        $transport = new TCurlClient('localhost');
+        $ref = new \ReflectionClass($transport);
+        $prop = $ref->getProperty('curlHandle');
+        $prop->setAccessible(true);
+        $prop->setValue($transport, 'testHandle');
+
+        $transport::closeCurlHandle();
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TFramedTransportTest.php b/lib/php/test/Unit/Lib/Transport/TFramedTransportTest.php
new file mode 100644
index 0000000..2607ddb
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TFramedTransportTest.php
@@ -0,0 +1,240 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Transport\TFramedTransport;
+use Thrift\Transport\TTransport;
+
+class TFramedTransportTest extends TestCase
+{
+    public function testIsOpen()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $framedTransport = new TFramedTransport($transport);
+
+        $transport
+            ->expects($this->once())
+            ->method('isOpen')
+            ->willReturn(true);
+
+        $this->assertTrue($framedTransport->isOpen());
+    }
+
+    public function testOpen()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $framedTransport = new TFramedTransport($transport);
+
+        $transport
+            ->expects($this->once())
+            ->method('open')
+            ->willReturn(null);
+
+        $this->assertNull($framedTransport->open());
+    }
+
+    public function testClose()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $framedTransport = new TFramedTransport($transport);
+
+        $transport
+            ->expects($this->once())
+            ->method('close')
+            ->willReturn(null);
+
+        $this->assertNull($framedTransport->close());
+    }
+
+    public function testPutBack()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $framedTransport = new TFramedTransport($transport);
+        $framedTransport->putBack('test');
+
+        $ref = new \ReflectionClass($framedTransport);
+        $property = $ref->getProperty('rBuf_');
+        $property->setAccessible(true);
+        $this->assertEquals('test', $property->getValue($framedTransport));
+
+        $framedTransport->putBack('abcde');
+        $this->assertEquals('abcdetest', $property->getValue($framedTransport));
+    }
+
+    /**
+     * @dataProvider readDataProvider
+     */
+    public function testRead(
+        $readAllowed,
+        $readBuffer,
+        $lowLevelTransportReadResult,
+        $lowLevelTransportReadAllParams,
+        $lowLevelTransportReadAllResult,
+        $readLength,
+        $expectedReadResult
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $framedTransport = new TFramedTransport($transport, $readAllowed);
+        $framedTransport->putBack($readBuffer);
+
+        $transport
+            ->expects($readAllowed ? $this->never() : $this->once())
+            ->method('read')
+            ->with($readLength)
+            ->willReturn($lowLevelTransportReadResult);
+
+        $transport
+            ->expects($this->exactly(count($lowLevelTransportReadAllParams)))
+            ->method('readAll')
+            ->withConsecutive(...$lowLevelTransportReadAllParams)
+            ->willReturnOnConsecutiveCalls(...$lowLevelTransportReadAllResult);
+
+        $this->assertEquals($expectedReadResult, $framedTransport->read($readLength));
+    }
+
+    public function readDataProvider()
+    {
+        yield 'read not allowed' => [
+            'readAllowed' => false,
+            'readBuffer' => '',
+            'lowLevelTransportReadResult' => '12345',
+            'lowLevelTransportReadAllParams' => [],
+            'lowLevelTransportReadAllResult' => [],
+            'readLength' => 5,
+            'expectedReadResult' => '12345',
+        ];
+        yield 'read fully buffered item' => [
+            'readAllowed' => true,
+            'readBuffer' => '',
+            'lowLevelTransportReadResult' => '',
+            'lowLevelTransportReadAllParams' => [[4], [5]],
+            'lowLevelTransportReadAllResult' => [pack('N', '5'), '12345'],
+            'readLength' => 5,
+            'expectedReadResult' => '12345',
+        ];
+        yield 'read partly buffered item' => [
+            'readAllowed' => true,
+            'readBuffer' => '',
+            'lowLevelTransportReadResult' => '',
+            'lowLevelTransportReadAllParams' => [[4], [10]],
+            'lowLevelTransportReadAllResult' => [pack('N', '10'), '1234567890'],
+            'readLength' => 5,
+            'expectedReadResult' => '12345',
+        ];
+    }
+
+    /**
+     * @dataProvider writeDataProvider
+     */
+    public function testWrite(
+        $writeAllowed,
+        $writeData,
+        $writeLength,
+        $expectedWriteBufferValue
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $framedTransport = new TFramedTransport($transport, true, $writeAllowed);
+
+        $transport
+            ->expects($writeAllowed ? $this->never() : $this->once())
+            ->method('write')
+            ->with('12345', 5)
+            ->willReturn(5);
+
+        $framedTransport->write($writeData, $writeLength);
+
+        $ref = new \ReflectionClass($framedTransport);
+        $property = $ref->getProperty('wBuf_');
+        $property->setAccessible(true);
+        $this->assertEquals($expectedWriteBufferValue, $property->getValue($framedTransport));
+    }
+
+    public function writeDataProvider()
+    {
+        yield 'write not allowed' => [
+            'writeAllowed' => false,
+            'writeData' => '12345',
+            'writeLength' => 5,
+            'expectedWriteBufferValue' => '',
+        ];
+        yield 'write full' => [
+            'writeAllowed' => true,
+            'writeData' => '12345',
+            'writeLength' => 5,
+            'expectedWriteBufferValue' => '12345',
+        ];
+        yield 'write partly' => [
+            'writeAllowed' => true,
+            'writeData' => '1234567890',
+            'writeLength' => 5,
+            'expectedWriteBufferValue' => '12345',
+        ];
+    }
+
+    /**
+     * @dataProvider flushDataProvider
+     */
+    public function testFlush(
+        $writeAllowed,
+        $writeBuffer,
+        $lowLevelTransportWrite
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $framedTransport = new TFramedTransport($transport, true, $writeAllowed);
+        $ref = new \ReflectionClass($framedTransport);
+        $property = $ref->getProperty('wBuf_');
+        $property->setAccessible(true);
+        $property->setValue($framedTransport, $writeBuffer);
+
+        $transport
+            ->expects($this->once())
+            ->method('flush');
+
+        $transport
+            ->expects($writeAllowed && !empty($writeBuffer) ? $this->once() : $this->never())
+            ->method('write')
+            ->with($lowLevelTransportWrite)
+            ->willReturn(null);
+
+        $this->assertNull($framedTransport->flush());
+    }
+
+    public function flushDataProvider()
+    {
+        yield 'write not allowed' => [
+            'writeAllowed' => false,
+            'writeBuffer' => '12345',
+            'lowLevelTransportWrite' => '',
+        ];
+        yield 'empty buffer' => [
+            'writeAllowed' => true,
+            'writeBuffer' => '',
+            'lowLevelTransportWrite' => '',
+        ];
+        yield 'write full' => [
+            'writeAllowed' => true,
+            'writeBuffer' => '12345',
+            'lowLevelTransportWrite' => pack('N', strlen('12345')) . '12345',
+        ];
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/THttpClientTest.php b/lib/php/test/Unit/Lib/Transport/THttpClientTest.php
new file mode 100644
index 0000000..ce6813c
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/THttpClientTest.php
@@ -0,0 +1,332 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TTransportException;
+use Thrift\Transport\THttpClient;
+
+class THttpClientTest extends TestCase
+{
+    use PHPMock;
+
+    public function testSetTimeoutSecs()
+    {
+        $host = 'localhost';
+        $transport = new THttpClient($host);
+        $transport->setTimeoutSecs(1000);
+
+        $ref = new \ReflectionClass($transport);
+        $prop = $ref->getProperty('timeout_');
+        $prop->setAccessible(true);
+        $this->assertEquals(1000, $prop->getValue($transport));
+    }
+
+    public function testIsOpen()
+    {
+        $host = 'localhost';
+        $transport = new THttpClient($host);
+        $this->assertTrue($transport->isOpen());
+    }
+
+    public function testOpen()
+    {
+        $host = 'localhost';
+        $transport = new THttpClient($host);
+        $this->assertNull($transport->open());
+    }
+
+    public function testClose()
+    {
+        $handle = fopen('php://temp', 'r+');
+        $this->getFunctionMock('Thrift\\Transport', 'fclose')
+             ->expects($this->once())
+             ->with($handle)
+             ->willReturn(true);
+
+        $host = 'localhost';
+        $transport = new THttpClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propRequest = $ref->getProperty('handle_');
+        $propRequest->setAccessible(true);
+        $propRequest->setValue($transport, $handle);
+
+        $this->assertNull($transport->close());
+        $this->assertNull($propRequest->getValue($transport));
+    }
+
+    /**
+     * @dataProvider readDataProvider
+     */
+    public function testRead(
+        $readLen,
+        $freadResult,
+        $streamGetMetaDataResult,
+        $expectedResult,
+        $expectedException,
+        $expectedExceptionMessage,
+        $expectedExceptionCode
+    ) {
+        $handle = fopen('php://temp', 'r+');
+        $this->getFunctionMock('Thrift\\Transport', 'fread')
+             ->expects($this->once())
+             ->with($handle, $readLen)
+             ->willReturn($freadResult);
+
+        $this->getFunctionMock('Thrift\\Transport', 'stream_get_meta_data')
+             ->expects(!empty($streamGetMetaDataResult) ? $this->once() : $this->never())
+             ->with($handle)
+             ->willReturn($streamGetMetaDataResult);
+
+        if ($expectedException) {
+            $this->expectException($expectedException);
+            $this->expectExceptionMessage($expectedExceptionMessage);
+            $this->expectExceptionCode($expectedExceptionCode);
+        }
+
+        $host = 'localhost';
+        $transport = new THttpClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propRequest = $ref->getProperty('handle_');
+        $propRequest->setAccessible(true);
+        $propRequest->setValue($transport, $handle);
+
+        $this->assertEquals($expectedResult, $transport->read($readLen));
+    }
+
+    public function readDataProvider()
+    {
+        yield 'read success' => [
+            'readLen' => 10,
+            'freadResult' => '1234567890',
+            'streamGetMetaDataResult' => [],
+            'expectedResult' => '1234567890',
+            'expectedException' => null,
+            'expectedExceptionMessage' => null,
+            'expectedExceptionCode' => null,
+        ];
+        yield 'read failed' => [
+            'readLen' => 10,
+            'freadResult' => false,
+            'streamGetMetaDataResult' => [
+                'timed_out' => false,
+            ],
+            'expectedResult' => '',
+            'expectedException' => TTransportException::class,
+            'expectedExceptionMessage' => 'THttpClient: Could not read 10 bytes from localhost:80',
+            'expectedExceptionCode' => TTransportException::UNKNOWN,
+        ];
+        yield 'read timeout' => [
+            'readLen' => 10,
+            'freadResult' => '',
+            'streamGetMetaDataResult' => [
+                'timed_out' => true,
+            ],
+            'expectedResult' => '',
+            'expectedException' => TTransportException::class,
+            'expectedExceptionMessage' => 'THttpClient: timed out reading 10 bytes from localhost:80',
+            'expectedExceptionCode' => TTransportException::TIMED_OUT,
+        ];
+    }
+
+    public function testWrite()
+    {
+        $host = 'localhost';
+        $transport = new THttpClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $prop = $ref->getProperty('buf_');
+        $prop->setAccessible(true);
+
+        $transport->write('1234567890');
+
+        $this->assertEquals('1234567890', $prop->getValue($transport));
+    }
+
+    /**
+     * @dataProvider flushDataProvider
+     */
+    public function testFlush(
+        $host,
+        $port,
+        $uri,
+        $scheme,
+        $context,
+        $headers,
+        $timeout,
+        $streamContextOptions,
+        $streamContext,
+        $fopenResult,
+        $expectedHost,
+        $expectedUri,
+        $expectedException,
+        $expectedExceptionMessage,
+        $expectedExceptionCode
+    ) {
+        $this->getFunctionMock('Thrift\\Transport', 'stream_context_create')
+             ->expects($this->once())
+             ->with($streamContextOptions)
+             ->willReturn($streamContext);
+
+        $this->getFunctionMock('Thrift\\Transport', 'fopen')
+             ->expects($this->once())
+             ->with(
+                 $scheme . '://' . $expectedHost . $expectedUri,
+                 'r',
+                 false,
+                 $streamContext
+             )->willReturn($fopenResult);
+
+        if ($expectedException) {
+            $this->expectException($expectedException);
+            $this->expectExceptionMessage($expectedExceptionMessage);
+            $this->expectExceptionCode($expectedExceptionCode);
+        }
+
+        $transport = new THttpClient($host, $port, $uri, $scheme, $context);
+        if (!empty($headers)) {
+            $transport->addHeaders($headers);
+        }
+        if (!empty($timeout)) {
+            $transport->setTimeoutSecs($timeout);
+        }
+
+        $this->assertNull($transport->flush());
+    }
+
+    public function flushDataProvider()
+    {
+        $default = [
+            'host' => 'localhost',
+            'port' => '80',
+            'uri' => '',
+            'scheme' => 'http',
+            'context' => [],
+            'headers' => [],
+            'timeout' => null,
+            'streamContextOptions' => [
+                'http' => [
+                    'method' => 'POST',
+                    'header' => "Host: localhost\r\n" .
+                        "Accept: application/x-thrift\r\n" .
+                        "User-Agent: PHP/THttpClient\r\n" .
+                        "Content-Type: application/x-thrift\r\n" .
+                        "Content-Length: 0",
+                    'content' => '',
+                    'max_redirects' => 1,
+                ],
+            ],
+            'streamContext' => fopen('php://temp', 'r+'),
+            'fopenResult' => fopen('php://memory', 'r+'),
+            'expectedHost' => 'localhost',
+            'expectedUri' => '',
+            'expectedException' => '',
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => '',
+        ];
+
+        yield 'success' => $default;
+        yield 'additionalHeaders' => array_merge(
+            $default,
+            [
+                'headers' => [
+                    'X-Test-Header' => 'test',
+                ],
+                'streamContextOptions' => [
+                    'http' => [
+                        'method' => 'POST',
+                        'header' => "Host: localhost\r\n" .
+                            "Accept: application/x-thrift\r\n" .
+                            "User-Agent: PHP/THttpClient\r\n" .
+                            "Content-Type: application/x-thrift\r\n" .
+                            "Content-Length: 0\r\n" .
+                            "X-Test-Header: test",
+                        'content' => '',
+                        'max_redirects' => 1,
+                    ],
+                ],
+            ]
+        );
+        yield 'timeout' => array_merge(
+            $default,
+            [
+                'timeout' => 1000,
+                'streamContextOptions' => [
+                    'http' => [
+                        'method' => 'POST',
+                        'header' => "Host: localhost\r\n" .
+                            "Accept: application/x-thrift\r\n" .
+                            "User-Agent: PHP/THttpClient\r\n" .
+                            "Content-Type: application/x-thrift\r\n" .
+                            "Content-Length: 0",
+                        'content' => '',
+                        'max_redirects' => 1,
+                        'timeout' => 1000,
+                    ],
+                ],
+            ]
+        );
+        yield 'fopenFailed' => array_merge(
+            $default,
+            [
+                'host' => 'localhost',
+                'port' => 8080,
+                'uri' => 'test',
+                'expectedHost' => 'localhost:8080',
+                'expectedUri' => '/test',
+                'streamContextOptions' => [
+                    'http' => [
+                        'method' => 'POST',
+                        'header' => "Host: localhost:8080\r\n" .
+                            "Accept: application/x-thrift\r\n" .
+                            "User-Agent: PHP/THttpClient\r\n" .
+                            "Content-Type: application/x-thrift\r\n" .
+                            "Content-Length: 0",
+                        'content' => '',
+                        'max_redirects' => 1,
+                    ],
+                ],
+                'fopenResult' => false,
+                'expectedException' => TTransportException::class,
+                'expectedExceptionMessage' => 'THttpClient: Could not connect to localhost:8080/test',
+                'expectedExceptionCode' => TTransportException::NOT_OPEN,
+            ]
+        );
+    }
+
+    public function testAddHeaders()
+    {
+        $host = 'localhost';
+        $transport = new THttpClient($host);
+
+        $ref = new \ReflectionClass($transport);
+        $propRequest = $ref->getProperty('headers_');
+        $propRequest->setAccessible(true);
+        $propRequest->setValue($transport, ['test' => '1234567890']);
+
+        $transport->addHeaders(['test2' => '12345']);
+        $this->assertEquals(['test' => '1234567890', 'test2' => '12345'], $propRequest->getValue($transport));
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TMemoryBufferTest.php b/lib/php/test/Unit/Lib/Transport/TMemoryBufferTest.php
new file mode 100644
index 0000000..06f0012
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TMemoryBufferTest.php
@@ -0,0 +1,143 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TTransportException;
+use Thrift\Transport\TMemoryBuffer;
+
+class TMemoryBufferTest extends TestCase
+{
+    public function testIsOpen()
+    {
+        $transport = new TMemoryBuffer();
+        $this->assertTrue($transport->isOpen());
+    }
+
+    public function testOpen()
+    {
+        $transport = new TMemoryBuffer();
+        $this->assertNull($transport->open());
+    }
+
+    public function testClose()
+    {
+        $transport = new TMemoryBuffer();
+        $this->assertNull($transport->close());
+    }
+
+    public function testReadEmptyBuffer()
+    {
+        $transport = new TMemoryBuffer();
+        $this->expectException(\Thrift\Exception\TTransportException::class);
+        $this->expectExceptionMessage("TMemoryBuffer: Could not read 1 bytes from buffer.");
+        $this->expectExceptionCode(TTransportException::UNKNOWN);
+        $transport->read(1);
+    }
+
+    /**
+     * @dataProvider readDataProvider
+     */
+    public function testRead(
+        $startBuffer,
+        $readLength,
+        $expectedRead,
+        $expectedBuffer
+    ) {
+        $transport = new TMemoryBuffer($startBuffer);
+        $this->assertEquals($expectedRead, $transport->read($readLength));
+        $this->assertEquals($expectedBuffer, $transport->getBuffer());
+    }
+
+    public function readDataProvider()
+    {
+        yield 'Read part of buffer' => [
+            'startBuffer' => '1234567890',
+            'readLength' => 5,
+            'expectedRead' => '12345',
+            'expectedBuffer' => '67890',
+        ];
+        yield 'Read part of buffer UTF' => [
+            'startBuffer' => 'Slovenščina',
+            'readLength' => 6,
+            'expectedRead' => 'Sloven',
+            'expectedBuffer' => 'ščina',
+        ];
+        yield 'Read part of buffer UTF 2' => [
+            'startBuffer' => 'Українська',
+            'readLength' => 6,
+            'expectedRead' => 'Укр',
+            'expectedBuffer' => 'аїнська',
+        ];
+        yield 'Read full' => [
+            'startBuffer' => '123456789',
+            'readLength' => 10,
+            'expectedRead' => '123456789',
+            'expectedBuffer' => '',
+        ];
+    }
+
+    /**
+     * @dataProvider writeDataProvider
+     */
+    public function testWrite(
+        $startBuffer,
+        $writeData,
+        $expectedBuffer
+    ) {
+        $transport = new TMemoryBuffer($startBuffer);
+        $transport->write($writeData);
+        $this->assertEquals($expectedBuffer, $transport->getBuffer());
+    }
+
+    public function writeDataProvider()
+    {
+        yield 'empty start buffer' => [
+            'startBuffer' => '',
+            'writeData' => '12345',
+            'expectedBuffer' => '12345',
+        ];
+        yield 'not empty start buffer' => [
+            'startBuffer' => '67890',
+            'writeData' => '12345',
+            'expectedBuffer' => '6789012345',
+        ];
+        yield 'not empty start buffer UTF' => [
+            'startBuffer' => 'Slovenščina',
+            'writeData' => 'Українська',
+            'expectedBuffer' => 'SlovenščinaУкраїнська',
+        ];
+    }
+
+    public function testAvailable()
+    {
+        $transport = new TMemoryBuffer('12345');
+        $this->assertEquals('5', $transport->available());
+    }
+
+    public function testPutBack()
+    {
+        $transport = new TMemoryBuffer('12345');
+        $transport->putBack('67890');
+        $this->assertEquals('6789012345', $transport->getBuffer());
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TNullTransportTest.php b/lib/php/test/Unit/Lib/Transport/TNullTransportTest.php
new file mode 100644
index 0000000..044c703
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TNullTransportTest.php
@@ -0,0 +1,62 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TTransportException;
+use Thrift\Transport\TNullTransport;
+
+class TNullTransportTest extends TestCase
+{
+    public function testIsOpen()
+    {
+        $transport = new TNullTransport();
+        $this->assertTrue($transport->isOpen());
+    }
+
+    public function testOpen()
+    {
+        $transport = new TNullTransport();
+        $this->assertNull($transport->open());
+    }
+
+    public function testClose()
+    {
+        $transport = new TNullTransport();
+        $this->assertNull($transport->close());
+    }
+
+    public function testRead()
+    {
+        $transport = new TNullTransport();
+        $this->expectException(TTransportException::class);
+        $this->expectExceptionMessage("Can't read from TNullTransport.");
+        $this->expectExceptionCode(0);
+        $transport->read(1);
+    }
+
+    public function testWrite()
+    {
+        $transport = new TNullTransport();
+        $this->assertNull($transport->write('test'));
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TPhpStreamTest.php b/lib/php/test/Unit/Lib/Transport/TPhpStreamTest.php
new file mode 100644
index 0000000..c2f950c
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TPhpStreamTest.php
@@ -0,0 +1,296 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TException;
+use Thrift\Transport\TPhpStream;
+
+class TPhpStreamTest extends TestCase
+{
+    use PHPMock;
+
+    /**
+     * @dataProvider fopenDataProvider
+     */
+    public function testOpen(
+        $mode,
+        $sapiName,
+        $fopenParams,
+        $fopenResult,
+        $expectedException,
+        $expectedExceptionMessage,
+        $expectedExceptionCode
+    ) {
+        #due to the running tests in separate process we could not open stream in data provider, so we need to do it here
+        foreach ($fopenResult as $num => $result) {
+            $fopenResult[$num] = $result ? fopen(...$result) : $result;
+        }
+
+        $this->getFunctionMock('Thrift\Transport', 'php_sapi_name')
+             ->expects(!empty($sapiName) ? $this->once() : $this->never())
+             ->willReturn($sapiName);
+
+        $this->getFunctionMock('Thrift\Transport', 'fopen')
+             ->expects($this->exactly(count($fopenResult)))
+             ->withConsecutive(...$fopenParams)
+             ->willReturnOnConsecutiveCalls(...$fopenResult);
+
+        if ($expectedException) {
+            $this->expectException($expectedException);
+            $this->expectExceptionMessage($expectedExceptionMessage);
+            $this->expectExceptionCode($expectedExceptionCode);
+        }
+
+        $transport = new TPhpStream($mode);
+        $transport->open();
+    }
+
+    public function fopenDataProvider()
+    {
+        yield 'readCli' => [
+            'mode' => TPhpStream::MODE_R,
+            'sapiName' => 'cli',
+            'fopenParams' => [['php://stdin', 'r']],
+            'fopenResult' => [['php://temp', 'r']],
+            'expectedException' => null,
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'readNotCli' => [
+            'mode' => TPhpStream::MODE_R,
+            'sapiName' => 'apache',
+            'fopenParams' => [['php://input', 'r']],
+            'fopenResult' => [['php://temp', 'r']],
+            'expectedException' => null,
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'write' => [
+            'mode' => TPhpStream::MODE_W,
+            'sapiName' => '',
+            'fopenParams' => [['php://output', 'w']],
+            'fopenResult' => [['php://temp', 'w']],
+            'expectedException' => null,
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'read and write' => [
+            'mode' => TPhpStream::MODE_R | TPhpStream::MODE_W,
+            'sapiName' => 'cli',
+            'fopenParams' => [['php://stdin', 'r'], ['php://output', 'w']],
+            'fopenResult' => [['php://temp', 'r'], ['php://temp', 'w']],
+            'expectedException' => null,
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'read exception' => [
+            'mode' => TPhpStream::MODE_R,
+            'sapiName' => 'cli',
+            'fopenParams' => [['php://stdin', 'r']],
+            'fopenResult' => [false],
+            'expectedException' => TException::class,
+            'expectedExceptionMessage' => 'TPhpStream: Could not open php://input',
+            #should depend on php_sapi_name result
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'write exception' => [
+            'mode' => TPhpStream::MODE_W,
+            'sapiName' => '',
+            'fopenParams' => [['php://output', 'w']],
+            'fopenResult' => [false],
+            'expectedException' => TException::class,
+            'expectedExceptionMessage' => 'TPhpStream: Could not open php://output',
+            'expectedExceptionCode' => 0,
+        ];
+    }
+
+    /**
+     * @dataProvider closeDataProvider
+     */
+    public function testClose(
+        $mode,
+        $fopenParams,
+        $fopenResult
+    ) {
+        #due to the running tests in separate process we could not open stream in data provider, so we need to do it here
+        foreach ($fopenResult as $num => $result) {
+            $fopenResult[$num] = $result ? fopen(...$result) : $result;
+        }
+
+        $this->getFunctionMock('Thrift\Transport', 'fopen')
+             ->expects($this->exactly(count($fopenParams)))
+             ->withConsecutive(...$fopenParams)
+             ->willReturnOnConsecutiveCalls(...$fopenResult);
+
+        $this->getFunctionMock('Thrift\Transport', 'fclose')
+             ->expects($this->exactly(count($fopenParams)))
+             ->with(
+                 $this->callback(function ($stream) {
+                     return is_resource($stream);
+                 })
+             )
+             ->willReturn(true);
+
+        $transport = new TPhpStream($mode);
+        $transport->open();
+        $this->assertTrue($transport->isOpen());
+
+        $transport->close();
+        $this->assertFalse($transport->isOpen());
+    }
+
+    public function closeDataProvider()
+    {
+        $read = ['php://temp', 'r'];
+        $write = ['php://temp', 'w'];
+        yield 'read' => [
+            'mode' => TPhpStream::MODE_R,
+            'fopenParams' => [['php://stdin', 'r']],
+            'fopenResult' => [$read],
+        ];
+        yield 'write' => [
+            'mode' => TPhpStream::MODE_W,
+            'fopenParams' => [['php://output', 'w']],
+            'fopenResult' => [$write],
+        ];
+        yield 'read and write' => [
+            'mode' => TPhpStream::MODE_R | TPhpStream::MODE_W,
+            'fopenParams' => [['php://stdin', 'r'], ['php://output', 'w']],
+            'fopenResult' => [$read, $write],
+        ];
+    }
+
+    /**
+     * @dataProvider readDataProvider
+     */
+    public function testRead(
+        $freadResult,
+        $expectedResult,
+        $expectedException,
+        $expectedExceptionMessage,
+        $expectedExceptionCode
+    ) {
+        $this->getFunctionMock('Thrift\Transport', 'fread')
+             ->expects($this->once())
+             ->with($this->anything(), 5)
+             ->willReturn($freadResult);
+
+        if ($expectedException) {
+            $this->expectException($expectedException);
+            $this->expectExceptionMessage($expectedExceptionMessage);
+            $this->expectExceptionCode($expectedExceptionCode);
+        }
+
+        $transport = new TPhpStream(TPhpStream::MODE_R);
+        $this->assertEquals($expectedResult, $transport->read(5));
+    }
+
+    public function readDataProvider()
+    {
+        yield 'success' => [
+            'freadResult' => '12345',
+            'expectedResult' => '12345',
+            'expectedException' => null,
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'empty' => [
+            'freadResult' => '',
+            'expectedResult' => '',
+            'expectedException' => TException::class,
+            'expectedExceptionMessage' => 'TPhpStream: Could not read 5 bytes',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'false' => [
+            'freadResult' => false,
+            'expectedResult' => false,
+            'expectedException' => TException::class,
+            'expectedExceptionMessage' => 'TPhpStream: Could not read 5 bytes',
+            'expectedExceptionCode' => 0,
+        ];
+    }
+
+    /**
+     * @dataProvider writeDataProvider
+     */
+    public function testWrite(
+        $buf,
+        $fwriteParams,
+        $fwriteResult,
+        $expectedException,
+        $expectedExceptionMessage,
+        $expectedExceptionCode
+    ) {
+        $this->getFunctionMock('Thrift\Transport', 'fwrite')
+             ->expects($this->exactly(count($fwriteParams)))
+             ->withConsecutive(...$fwriteParams)
+             ->willReturnOnConsecutiveCalls(...$fwriteResult);
+
+        if ($expectedException) {
+            $this->expectException($expectedException);
+            $this->expectExceptionMessage($expectedExceptionMessage);
+            $this->expectExceptionCode($expectedExceptionCode);
+        }
+
+        $transport = new TPhpStream(TPhpStream::MODE_W);
+        $transport->write($buf);
+    }
+
+    public function writeDataProvider()
+    {
+        yield 'success' => [
+            'buf' => '12345',
+            'fwriteParams' => [[$this->anything(), '12345']],
+            'fwriteResult' => [5],
+            'expectedException' => null,
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'several iteration' => [
+            'buf' => '1234567890',
+            'fwriteParams' => [[$this->anything(), '1234567890'], [$this->anything(), '67890']],
+            'fwriteResult' => [5, 5],
+            'expectedException' => null,
+            'expectedExceptionMessage' => '',
+            'expectedExceptionCode' => 0,
+        ];
+        yield 'fail' => [
+            'buf' => '1234567890',
+            'fwriteParams' => [[$this->anything(), '1234567890']],
+            'fwriteResult' => [false],
+            'expectedException' => TException::class,
+            'expectedExceptionMessage' => 'TPhpStream: Could not write 10 bytes',
+            'expectedExceptionCode' => 0,
+        ];
+    }
+
+    public function testFlush()
+    {
+        $this->getFunctionMock('Thrift\Transport', 'fflush')
+             ->expects($this->once());
+
+        $transport = new TPhpStream(TPhpStream::MODE_R);
+        $transport->flush();
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TSSLSocketTest.php b/lib/php/test/Unit/Lib/Transport/TSSLSocketTest.php
new file mode 100644
index 0000000..7177219
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TSSLSocketTest.php
@@ -0,0 +1,247 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TException;
+use Thrift\Exception\TTransportException;
+use Thrift\Transport\TSSLSocket;
+
+class TSSLSocketTest extends TestCase
+{
+    use PHPMock;
+
+    /**
+     * @dataProvider openExceptionDataProvider
+     */
+    public function testOpenException(
+        $host,
+        $port,
+        $context,
+        $debugHandler,
+        $streamSocketClientCallCount,
+        $expectedException,
+        $expectedMessage,
+        $expectedCode
+    ) {
+        $this->expectException($expectedException);
+        $this->expectExceptionMessage($expectedMessage);
+        $this->expectExceptionCode($expectedCode);
+
+        $this->getFunctionMock('Thrift\Transport', 'stream_socket_client')
+             ->expects($this->exactly($streamSocketClientCallCount))
+             ->with(
+                 'ssl://' . $host . ':' . $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                 STREAM_CLIENT_CONNECT,
+                 $this->anything() #$context
+             )
+             ->willReturn(false);
+
+        $socket = new TSSLSocket(
+            $host,
+            $port,
+            $context,
+            $debugHandler
+        );
+        $socket->open();
+    }
+
+    public function openExceptionDataProvider()
+    {
+        yield 'host is empty' => [
+            'host' => '',
+            'port' => 9090,
+            'context' => null,
+            'debugHandler' => null,
+            'streamSocketClientCallCount' => 0,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'Cannot open null host',
+            'expectedCode' => TTransportException::NOT_OPEN,
+        ];
+        yield 'port is not positive' => [
+            'host' => 'localhost',
+            'port' => 0,
+            'context' => null,
+            'debugHandler' => null,
+            'streamSocketClientCallCount' => 0,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'Cannot open without port',
+            'expectedCode' => TTransportException::NOT_OPEN,
+        ];
+        yield 'connection failure' => [
+            'host' => 'nonexistent-host',
+            'port' => 9090,
+            'context' => null,
+            'debugHandler' => null,
+            'streamSocketClientCallCount' => 1,
+            'expectedException' => TException::class,
+            'expectedMessage' => 'TSocket: Could not connect to',
+            'expectedCode' => TTransportException::UNKNOWN,
+        ];
+    }
+
+    public function testDoubleConnect(): void
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $context = null;
+        $debugHandler = null;
+
+        $this->getFunctionMock('Thrift\Transport', 'stream_socket_client')
+             ->expects($this->once())
+             ->with(
+                 'ssl://' . $host . ':' . $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                 STREAM_CLIENT_CONNECT,
+                 $this->anything() #$context
+             )
+             ->willReturn(fopen('php://memory', 'r+'));
+
+        $transport = new TSSLSocket(
+            $host,
+            $port,
+            $context,
+            $debugHandler
+        );
+
+        $transport->open();
+        $this->expectException(TTransportException::class);
+        $this->expectExceptionMessage('Socket already connected');
+        $this->expectExceptionCode(TTransportException::ALREADY_OPEN);
+        $transport->open();
+    }
+
+    public function testDebugHandler()
+    {
+        $host = 'nonexistent-host';
+        $port = 9090;
+        $context = null;
+
+        $debugHandler = function ($error) {
+            $this->assertEquals(
+                'TSocket: Could not connect to ssl://nonexistent-host:9090 (Connection refused [999])',
+                $error
+            );
+        };
+
+        $this->getFunctionMock('Thrift\Transport', 'stream_socket_client')
+             ->expects($this->once())
+             ->with(
+                 'ssl://' . $host . ':' . $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                 STREAM_CLIENT_CONNECT,
+                 $this->anything() #$context
+             )
+             ->willReturnCallback(
+                 function ($host, &$error_code, &$error_message, $timeout, $flags, $context) {
+                     $error_code = 999;
+                     $error_message = 'Connection refused';
+
+                     return false;
+                 }
+             );
+
+        $this->expectException(\Exception::class);
+        $this->expectExceptionMessage('TSocket: Could not connect to');
+        $this->expectExceptionCode(0);
+
+        $transport = new TSSLSocket(
+            $host,
+            $port,
+            $context,
+            $debugHandler
+        );
+        $transport->setDebug(true);
+        $transport->open();
+    }
+
+    public function testOpenWithContext()
+    {
+        $host = 'self-signed-localhost';
+        $port = 9090;
+        $context = stream_context_create(
+            [
+                'ssl' => [
+                    'verify_peer' => true,
+                    'verify_peer_name' => true,
+                    'allow_self_signed' => true,
+                ],
+            ]
+        );
+        $debugHandler = null;
+
+        $this->getFunctionMock('Thrift\Transport', 'stream_socket_client')
+             ->expects($this->once())
+             ->with(
+                 'ssl://' . $host . ':' . $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                 STREAM_CLIENT_CONNECT,
+                 $context #$context
+             )
+             ->willReturn(fopen('php://memory', 'r+'));
+
+        $transport = new TSSLSocket(
+            $host,
+            $port,
+            $context,
+            $debugHandler
+        );
+
+
+        $transport->open();
+        $this->assertTrue($transport->isOpen());
+    }
+
+    /**
+     * @dataProvider hostDataProvider
+     */
+    public function testGetHost($host, $expected)
+    {
+        $port = 9090;
+        $context = null;
+        $debugHandler = null;
+        $transport = new TSSLSocket(
+            $host,
+            $port,
+            $context,
+            $debugHandler
+        );
+        $this->assertEquals($expected, $transport->getHost());
+    }
+
+    public function hostDataProvider()
+    {
+        yield 'localhost' => ['localhost', 'ssl://localhost'];
+        yield 'ssl_localhost' => ['ssl://localhost', 'ssl://localhost'];
+        yield 'http_localhost' => ['http://localhost', 'http://localhost'];
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TSocketPoolTest.php b/lib/php/test/Unit/Lib/Transport/TSocketPoolTest.php
new file mode 100644
index 0000000..01e4532
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TSocketPoolTest.php
@@ -0,0 +1,541 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TException;
+use Thrift\Transport\TSocketPool;
+
+class TSocketPoolTest extends TestCase
+{
+    use PHPMock;
+
+    protected function setUp(): void
+    {
+        #need to be defined before the TSocketPool class definition
+        self::defineFunctionMock('Thrift\Transport', 'function_exists');
+    }
+
+    /**
+     * @dataProvider constructDataProvider
+     */
+    public function testConstruct(
+        $hosts,
+        $ports,
+        $persist,
+        $debugHandler,
+        $expectedServers
+    ) {
+        $socketPool = new TSocketPool($hosts, $ports, $persist, $debugHandler);
+
+        $ref = new \ReflectionObject($socketPool);
+        $serversProp = $ref->getProperty('servers_');
+        $serversProp->setAccessible(true);
+
+        $this->assertEquals($expectedServers, $serversProp->getValue($socketPool));
+    }
+
+
+    public function constructDataProvider()
+    {
+        yield 'one server' => [
+            ['localhost'],
+            [9090],
+            false,
+            null,
+            [
+                ['host' => 'localhost', 'port' => 9090],
+            ],
+        ];
+        yield 'two servers' => [
+            ['localhost1', 'localhost2'],
+            [9090, 9091],
+            false,
+            null,
+            [
+                ['host' => 'localhost1', 'port' => 9090],
+                ['host' => 'localhost2', 'port' => 9091],
+            ],
+        ];
+        yield 'one server with one port' => [
+            ['localhost'],
+            9090,
+            false,
+            null,
+            [
+                ['host' => 'localhost', 'port' => 9090],
+            ],
+        ];
+        yield 'two servers with one port' => [
+            ['localhost1', 'localhost2'],
+            9090,
+            false,
+            null,
+            [
+                ['host' => 'localhost1', 'port' => 9090],
+                ['host' => 'localhost2', 'port' => 9090],
+            ],
+        ];
+    }
+
+    public function testAddServer(): void
+    {
+        $socketPool = new TSocketPool([], []);
+        $socketPool->addServer('localhost', 9090);
+
+        $ref = new \ReflectionObject($socketPool);
+        $servers = $ref->getProperty('servers_');
+        $servers->setAccessible(true);
+
+        $this->assertEquals([['host' => 'localhost', 'port' => 9090]], $servers->getValue($socketPool));
+    }
+
+    public function testSetNumRetries(): void
+    {
+        $socketPool = new TSocketPool([], []);
+        $socketPool->setNumRetries(5);
+
+        $ref = new \ReflectionObject($socketPool);
+        $numRetries = $ref->getProperty('numRetries_');
+        $numRetries->setAccessible(true);
+
+        $this->assertEquals(5, $numRetries->getValue($socketPool));
+    }
+
+    public function testrSetRetryInterval(): void
+    {
+        $socketPool = new TSocketPool([], []);
+        $socketPool->setRetryInterval(5);
+
+        $ref = new \ReflectionObject($socketPool);
+        $retryInterval = $ref->getProperty('retryInterval_');
+        $retryInterval->setAccessible(true);
+
+        $this->assertEquals(5, $retryInterval->getValue($socketPool));
+    }
+
+    public function testrSetMaxConsecutiveFailures(): void
+    {
+        $socketPool = new TSocketPool([], []);
+        $socketPool->setMaxConsecutiveFailures(5);
+
+        $ref = new \ReflectionObject($socketPool);
+        $maxConsecutiveFailures = $ref->getProperty('maxConsecutiveFailures_');
+        $maxConsecutiveFailures->setAccessible(true);
+
+        $this->assertEquals(5, $maxConsecutiveFailures->getValue($socketPool));
+    }
+
+    public function testrSetRandomize(): void
+    {
+        $socketPool = new TSocketPool([], []);
+        $socketPool->setRandomize(false);
+
+        $ref = new \ReflectionObject($socketPool);
+        $randomize = $ref->getProperty('randomize_');
+        $randomize->setAccessible(true);
+
+        $this->assertEquals(false, $randomize->getValue($socketPool));
+    }
+
+    public function testrSetAlwaysTryLast(): void
+    {
+        $socketPool = new TSocketPool([], []);
+        $socketPool->setAlwaysTryLast(false);
+
+        $ref = new \ReflectionObject($socketPool);
+        $alwaysTryLast = $ref->getProperty('alwaysTryLast_');
+        $alwaysTryLast->setAccessible(true);
+
+        $this->assertEquals(false, $alwaysTryLast->getValue($socketPool));
+    }
+
+    /**
+     * @dataProvider openDataProvider
+     */
+    public function testOpen(
+        $hosts,
+        $ports,
+        $persist,
+        $debugHandler,
+        $randomize,
+        $retryInterval,
+        $numRetries,
+        $maxConsecutiveFailures,
+        $debug,
+        $servers,
+        $functionExistCallParams,
+        $functionExistResult,
+        $apcuFetchCallParams,
+        $apcuFetchResult,
+        $timeResult,
+        $debugHandlerCall,
+        $apcuStoreCallParams,
+        $fsockopenCallParams,
+        $fsockopenResult,
+        $expectedException,
+        $expectedExceptionMessage
+    ) {
+        $this->getFunctionMock('Thrift\Transport', 'function_exists')
+             ->expects($this->exactly(count($functionExistCallParams)))
+             ->withConsecutive(...$functionExistCallParams)
+             ->willReturnOnConsecutiveCalls(...$functionExistResult);
+
+        $this->getFunctionMock('Thrift\Transport', 'shuffle')
+             ->expects($randomize ? $this->once() : $this->never())
+             ->with($servers)
+             ->willReturnCallback(function (array &$servers) {
+                 $servers = array_reverse($servers);
+
+                 return true;
+             });
+
+        $this->getFunctionMock('Thrift\Transport', 'apcu_fetch')
+             ->expects($this->exactly(count($apcuFetchCallParams)))
+             ->withConsecutive(...$apcuFetchCallParams)
+             ->willReturnOnConsecutiveCalls(...$apcuFetchResult);
+
+        $this->getFunctionMock('Thrift\Transport', 'call_user_func')
+             ->expects($this->exactly(count($debugHandlerCall)))
+             ->withConsecutive(...$debugHandlerCall)
+             ->willReturn(true);
+
+        $this->getFunctionMock('Thrift\Transport', 'apcu_store')
+             ->expects($this->exactly(count($apcuStoreCallParams)))
+             ->withConsecutive(...$apcuStoreCallParams)
+             ->willReturn(true);
+
+        $this->getFunctionMock('Thrift\Transport', 'time')
+             ->expects($this->exactly(count($timeResult)))
+             ->willReturnOnConsecutiveCalls(...$timeResult);
+
+        #due to the running tests in separate process we could not open stream in data provider, so we need to do it here
+        foreach ($fsockopenResult as $num => $result) {
+            $fsockopenResult[$num] = $result ? fopen(...$result) : $result;
+        }
+
+        $this->getFunctionMock('Thrift\Transport', $persist ? 'pfsockopen' : 'fsockopen')
+             ->expects($this->exactly(count($fsockopenCallParams)))
+             ->withConsecutive(...$fsockopenCallParams)
+             ->willReturnOnConsecutiveCalls(...$fsockopenResult);
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_import_stream')
+             ->expects(is_null($expectedException) ? $this->once() : $this->never())
+             ->with(
+                 $this->callback(function ($stream) {
+                     return is_resource($stream);
+                 })
+             )
+             ->willReturn(true);
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_set_option')
+             ->expects(is_null($expectedException) ? $this->once() : $this->never())
+             ->with(
+                 $this->anything(), #$socket,
+                 SOL_TCP, #$level
+                 TCP_NODELAY, #$option
+                 1 #$value
+             )
+             ->willReturn(true);
+
+        if ($expectedException) {
+            $this->expectException($expectedException);
+            $this->expectExceptionMessage($expectedExceptionMessage);
+        }
+
+        $socketPool = new TSocketPool($hosts, $ports, $persist, $debugHandler);
+        $socketPool->setRandomize($randomize);
+        $socketPool->setRetryInterval($retryInterval);
+        $socketPool->setNumRetries($numRetries);
+        $socketPool->setMaxConsecutiveFailures($maxConsecutiveFailures);
+        $socketPool->setDebug($debug);
+
+        $this->assertNull($socketPool->open());
+    }
+
+    public function openDataProvider()
+    {
+        $default = [
+            'hosts' => ['localhost'],
+            'ports' => [9090],
+            'persist' => false,
+            'debugHandler' => null,
+            'randomize' => true,
+            'retryInterval' => 5,
+            'numRetries' => 1,
+            'maxConsecutiveFailures' => 1,
+            'debug' => false,
+            'servers' => [
+                ['host' => 'localhost', 'port' => 9090],
+            ],
+            'functionExistCallParams' => [
+                ['apcu_fetch'],
+                ['socket_import_stream'],
+                ['socket_set_option'],
+            ],
+            'functionExistResult' => [
+                true,
+                true,
+                true,
+            ],
+            'apcuFetchCallParams' => [
+                ['thrift_failtime:localhost:9090~', $this->anything()],
+            ],
+            'apcuFetchResult' => [
+                false,
+            ],
+            'timeResult' => [],
+            'debugHandlerCall' => [],
+            'apcuStoreCallParams' => [],
+            'fsockopenCallParams' => [
+                [
+                    'localhost',
+                    9090,
+                    $this->anything(), #$errno,
+                    $this->anything(), #$errstr,
+                    $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                ],
+            ],
+            'fsockopenResult' => [
+                ['php://temp', 'r'],
+            ],
+            'expectedException' => null,
+            'expectedExceptionMessage' => null,
+        ];
+
+        yield 'one server ready' => $default;
+        yield 'one server failed' => array_merge(
+            $default,
+            [
+                'functionExistCallParams' => [
+                    ['apcu_fetch'],
+                ],
+                'fsockopenResult' => [
+                    false,
+                ],
+                'apcuFetchCallParams' => [
+                    ['thrift_failtime:localhost:9090~', $this->anything()],
+                    ['thrift_consecfails:localhost:9090~', $this->anything()],
+                ],
+                'apcuStoreCallParams' => [
+                    ['thrift_failtime:localhost:9090~', $this->anything()],
+                    ['thrift_consecfails:localhost:9090~', $this->anything(), 0],
+                ],
+                'timeResult' => [
+                    1,
+                ],
+                'expectedException' => TException::class,
+                'expectedExceptionMessage' => 'TSocketPool: All hosts in pool are down. (localhost:9090)',
+            ]
+        );
+        yield 'connect to one server on second attempt' => array_merge(
+            $default,
+            [
+                'numRetries' => 2,
+                'fsockopenCallParams' => [
+                    [
+                        'localhost',
+                        9090,
+                        $this->anything(), #$errno,
+                        $this->anything(), #$errstr,
+                        $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                    ],
+                    [
+                        'localhost',
+                        9090,
+                        $this->anything(), #$errno,
+                        $this->anything(), #$errstr,
+                        $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                    ],
+                ],
+                'fsockopenResult' => [
+                    false,
+                    ['php://temp', 'r'],
+                ],
+                'apcuStoreCallParams' => [],
+            ]
+        );
+        yield 'last time fail time is not expired' => array_merge(
+            $default,
+            [
+                'retryInterval' => 5,
+                'apcuFetchResult' => [
+                    99,
+                ],
+                'apcuStoreCallParams' => [
+                    ['thrift_failtime:localhost:9090~', $this->anything()],
+                ],
+                'timeResult' => [
+                    100,
+                ],
+            ]
+        );
+        yield 'last time fail time is expired, store info to debug' => array_merge(
+            $default,
+            [
+                'retryInterval' => 5,
+                'apcuFetchResult' => [
+                    90,
+                ],
+                'apcuStoreCallParams' => [
+                    ['thrift_failtime:localhost:9090~', $this->anything()],
+                ],
+                'timeResult' => [
+                    100,
+                ],
+                'debug' => true,
+                'debugHandlerCall' => [
+                    ['error_log', 'TSocketPool: retryInterval (5) has passed for host localhost:9090'],
+                ],
+            ]
+        );
+        yield 'not accessible server, store info to debug' => array_merge(
+            $default,
+            [
+                'retryInterval' => 5,
+                'functionExistCallParams' => [
+                    ['apcu_fetch'],
+                ],
+                'functionExistResult' => [
+                    true,
+                ],
+                'apcuFetchCallParams' => [
+                    ['thrift_failtime:localhost:9090~', $this->anything()],
+                    ['thrift_consecfails:localhost:9090~', $this->anything()],
+                ],
+                'apcuFetchResult' => [
+                    90,
+                ],
+                'apcuStoreCallParams' => [
+                    ['thrift_failtime:localhost:9090~', $this->anything()],
+                    ['thrift_consecfails:localhost:9090~', 0],
+                ],
+                'timeResult' => [
+                    100,
+                    101,
+                ],
+                'fsockopenResult' => [
+                    false,
+                ],
+                'debug' => true,
+                'debugHandlerCall' => [
+                    ['error_log', 'TSocketPool: retryInterval (5) has passed for host localhost:9090'],
+                    ['error_log', 'TSocket: Could not connect to localhost:9090 ( [])'],
+                    ['error_log', 'TSocketPool: marking localhost:9090 as down for 5 secs after 1 failed attempts.'],
+                    ['error_log', 'TSocketPool: All hosts in pool are down. (localhost:9090)'],
+                ],
+                'expectedException' => TException::class,
+                'expectedExceptionMessage' => 'TSocketPool: All hosts in pool are down. (localhost:9090)',
+            ]
+        );
+        yield 'max consecutive failures' => array_merge(
+            $default,
+            [
+                'maxConsecutiveFailures' => 5,
+                'functionExistCallParams' => [
+                    ['apcu_fetch'],
+                ],
+                'functionExistResult' => [
+                    true,
+                ],
+                'apcuFetchCallParams' => [
+                    ['thrift_failtime:localhost:9090~', $this->anything()],
+                    ['thrift_consecfails:localhost:9090~', $this->anything()],
+                ],
+                'apcuStoreCallParams' => [
+                    ['thrift_consecfails:localhost:9090~', 1],
+                ],
+                'timeResult' => [],
+                'fsockopenResult' => [
+                    false,
+                ],
+                'expectedException' => TException::class,
+                'expectedExceptionMessage' => 'TSocketPool: All hosts in pool are down. (localhost:9090)',
+            ]
+        );
+        yield 'apcu disabled' => array_merge(
+            $default,
+            [
+                'functionExistCallParams' => [
+                    ['apcu_fetch'],
+                ],
+                'functionExistResult' => [
+                    false,
+                ],
+                'fsockopenResult' => [
+                    false,
+                ],
+                'timeResult' => [
+                    1,
+                ],
+                'apcuFetchCallParams' => [],
+                'apcuStoreCallParams' => [],
+                'expectedException' => TException::class,
+                'expectedExceptionMessage' => 'TSocketPool: All hosts in pool are down. (localhost:9090)',
+            ]
+        );
+        yield 'second host accessible' => array_merge(
+            $default,
+            [
+                'hosts' => ['host1', 'host2'],
+                'ports' => [9090, 9091],
+                'servers' => [
+                    ['host' => 'host1', 'port' => 9090],
+                    ['host' => 'host2', 'port' => 9091],
+                ],
+                'fsockopenCallParams' => [
+                    [
+                        'host2',
+                        9091,
+                        $this->anything(), #$errno,
+                        $this->anything(), #$errstr,
+                        $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                    ],
+                    [
+                        'host1',
+                        9090,
+                        $this->anything(), #$errno,
+                        $this->anything(), #$errstr,
+                        $this->anything(), #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+                    ],
+                ],
+                'fsockopenResult' => [
+                    false,
+                    ['php://temp', 'r'],
+                ],
+                'apcuFetchCallParams' => [
+                    ['thrift_failtime:host2:9091~', $this->anything()],
+                    ['thrift_consecfails:host2:9091~', $this->anything()],
+                    ['thrift_failtime:host1:9090~', $this->anything()],
+                ],
+                'apcuStoreCallParams' => [
+                    ['thrift_failtime:host2:9091~', $this->anything()],
+                    ['thrift_consecfails:host2:9091~', $this->anything(), 0],
+                ],
+                'timeResult' => [
+                    1,
+                ],
+            ]
+        );
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Transport/TSocketTest.php b/lib/php/test/Unit/Lib/Transport/TSocketTest.php
new file mode 100644
index 0000000..6bab297
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Transport/TSocketTest.php
@@ -0,0 +1,669 @@
+<?php
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+namespace Test\Thrift\Unit\Lib\Transport;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TException;
+use Thrift\Exception\TTransportException;
+use Thrift\Transport\TSocket;
+
+class TSocketTest extends TestCase
+{
+    use PHPMock;
+
+    /**
+     * @dataProvider openExceptionDataProvider
+     */
+    public function testOpenException(
+        $host,
+        $port,
+        $persist,
+        $debugHandler,
+        $fsockopenCallCount,
+        $expectedException,
+        $expectedMessage,
+        $expectedCode
+    ) {
+        $this->expectException($expectedException);
+        $this->expectExceptionMessage($expectedMessage);
+        $this->expectExceptionCode($expectedCode);
+
+        $this->getFunctionMock('Thrift\Transport', 'fsockopen')
+             ->expects($this->exactly($fsockopenCallCount))
+             ->with(
+                 $host,
+                 $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything() #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+             )
+             ->willReturn(false);
+
+        $socket = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $socket->open();
+    }
+
+    public function openExceptionDataProvider()
+    {
+        yield 'host is empty' => [
+            'host' => '',
+            'port' => 9090,
+            'persist' => null,
+            'debugHandler' => false,
+            'fsockopenCallCount' => 0,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'Cannot open null host',
+            'expectedCode' => TTransportException::NOT_OPEN,
+        ];
+        yield 'port is not positive' => [
+            'host' => 'localhost',
+            'port' => 0,
+            'persist' => false,
+            'debugHandler' => null,
+            'fsockopenCallCount' => 0,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'Cannot open without port',
+            'expectedCode' => TTransportException::NOT_OPEN,
+        ];
+        yield 'connection failure' => [
+            'host' => 'nonexistent-host',
+            'port' => 9090,
+            'persist' => false,
+            'debugHandler' => null,
+            'fsockopenCallCount' => 1,
+            'expectedException' => TException::class,
+            'expectedMessage' => 'TSocket: Could not connect to',
+            'expectedCode' => TTransportException::UNKNOWN,
+        ];
+    }
+
+    public function testDoubleConnect(): void
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $handle = fopen('php://memory', 'r+');
+        $this->getFunctionMock('Thrift\Transport', 'fsockopen')
+             ->expects($this->once())
+             ->with(
+                 $host,
+                 $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything() #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+             )
+             ->willReturn($handle);
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_import_stream')
+             ->expects($this->once())
+             ->with($handle)
+             ->willReturn(true);
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_set_option')
+             ->expects($this->once())
+             ->with(
+                 $this->anything(), #$socket,
+                 SOL_TCP, #$level
+                 TCP_NODELAY, #$option
+                 1 #$value
+             )
+             ->willReturn(true);
+
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+
+        $transport->open();
+        $this->expectException(TTransportException::class);
+        $this->expectExceptionMessage('Socket already connected');
+        $this->expectExceptionCode(TTransportException::ALREADY_OPEN);
+        $transport->open();
+    }
+
+    public function testDebugHandler()
+    {
+        $host = 'nonexistent-host';
+        $port = 9090;
+        $false = false;
+
+        $debugHandler = function ($error) {
+            $this->assertEquals(
+                'TSocket: Could not connect to nonexistent-host:9090 (Connection refused [999])',
+                $error
+            );
+        };
+
+        $this->getFunctionMock('Thrift\Transport', 'fsockopen')
+             ->expects($this->once())
+             ->with(
+                 $host,
+                 $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything() #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+             )
+             ->willReturnCallback(
+                 function (
+                     string $hostname,
+                     int $port,
+                     &$error_code,
+                     &$error_message,
+                     ?float $timeout
+                 ) {
+                     $error_code = 999;
+                     $error_message = 'Connection refused';
+
+                     return false;
+                 }
+             );
+
+        $transport = new TSocket(
+            $host,
+            $port,
+            $false,
+            $debugHandler
+        );
+        $transport->setDebug(true);
+
+        $this->expectException(\Exception::class);
+        $this->expectExceptionMessage('TSocket: Could not connect to');
+        $this->expectExceptionCode(0);
+        $transport->open();
+    }
+
+    public function testOpenPersist()
+    {
+        $host = 'persist-localhost';
+        $port = 9090;
+        $persist = true;
+        $debugHandler = null;
+
+        $handle = fopen('php://memory', 'r+');
+
+        $this->getFunctionMock('Thrift\Transport', 'pfsockopen')
+             ->expects($this->once())
+             ->with(
+                 $host,
+                 $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything() #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+             )
+             ->willReturn($handle);
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_import_stream')
+             ->expects($this->once())
+             ->with($handle)
+             ->willReturn(true);
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_set_option')
+             ->expects($this->once())
+             ->with(
+                 $this->anything(), #$socket,
+                 SOL_TCP, #$level
+                 TCP_NODELAY, #$option
+                 1 #$value
+             )
+             ->willReturn(true);
+
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+
+        $transport->open();
+        $this->assertTrue($transport->isOpen());
+    }
+
+    /**
+     * @dataProvider open_THRIFT_5132_DataProvider
+     */
+    public function testOpen_THRIFT_5132(
+        $socketImportResult
+    ) {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+
+        $this->getFunctionMock('Thrift\Transport', 'fsockopen')
+             ->expects($this->once())
+             ->with(
+                 $host,
+                 $port,
+                 $this->anything(), #$errno,
+                 $this->anything(), #$errstr,
+                 $this->anything() #$this->sendTimeoutSec_ + ($this->sendTimeoutUsec_ / 1000000),
+             )
+             ->willReturn(fopen('php://input', 'r+'));
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_import_stream')
+             ->expects($this->once())
+             ->willReturn($socketImportResult);
+
+        $this->getFunctionMock('Thrift\Transport', 'socket_set_option')
+             ->expects($socketImportResult ? $this->once() : $this->never())
+             ->with(
+                 $this->anything(), #$socket,
+                 SOL_TCP, #$level
+                 TCP_NODELAY, #$option
+                 1 #$value
+             )
+             ->willReturn(true);
+
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+
+        $transport->open();
+        $this->assertTrue($transport->isOpen());
+    }
+
+    public function open_THRIFT_5132_DataProvider()
+    {
+        yield 'socket_import_stream success' => [
+            'socketImportResult' => true,
+        ];
+        yield 'socket_import_stream fail' => [
+            'socketImportResult' => false,
+        ];
+    }
+
+    public function testSetHandle()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+
+        $this->assertFalse($transport->isOpen());
+        $transport->setHandle(fopen('php://memory', 'r+'));
+        $this->assertTrue($transport->isOpen());
+    }
+
+    public function testSetSendTimeout()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+
+        $transport->setSendTimeout(9999);
+        $reflector = new \ReflectionClass($transport);
+        $property = $reflector->getProperty('sendTimeoutSec_');
+        $property->setAccessible(true);
+        $this->assertEquals(9.0, $property->getValue($transport));
+        $property = $reflector->getProperty('sendTimeoutUsec_');
+        $property->setAccessible(true);
+        $this->assertEquals(999000, $property->getValue($transport));
+    }
+
+    public function testSetRecvTimeout()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+
+        $transport->setRecvTimeout(9999);
+        $reflector = new \ReflectionClass($transport);
+        $property = $reflector->getProperty('recvTimeoutSec_');
+        $property->setAccessible(true);
+        $this->assertEquals(9.0, $property->getValue($transport));
+        $property = $reflector->getProperty('recvTimeoutUsec_');
+        $property->setAccessible(true);
+        $this->assertEquals(999000, $property->getValue($transport));
+    }
+
+    /**
+     * @dataProvider hostDataProvider
+     */
+    public function testGetHost($host, $expected)
+    {
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $this->assertEquals($expected, $transport->getHost());
+    }
+
+    public function hostDataProvider()
+    {
+        yield 'localhost' => ['localhost', 'localhost'];
+        yield 'ssl_localhost' => ['ssl://localhost', 'ssl://localhost'];
+        yield 'http_localhost' => ['http://localhost', 'http://localhost'];
+    }
+
+    public function testGetPort()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $this->assertEquals($port, $transport->getPort());
+    }
+
+    public function testClose()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $transport->setHandle(fopen('php://memory', 'r+'));
+        $reflector = new \ReflectionClass($transport);
+        $property = $reflector->getProperty('handle_');
+        $property->setAccessible(true);
+        $this->assertNotNull($property->getValue($transport));
+
+        $transport->close();
+        $reflector = new \ReflectionClass($transport);
+        $property = $reflector->getProperty('handle_');
+        $property->setAccessible(true);
+        $this->assertNull($property->getValue($transport));
+    }
+
+    /**
+     * @dataProvider writeFailDataProvider
+     */
+    public function testWriteFail(
+        $streamSelectResult,
+        $fwriteCallCount,
+        $expectedException,
+        $expectedMessage,
+        $expectedCode
+    ) {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $handle = fopen('php://memory', 'r+');
+
+        $this->getFunctionMock('Thrift\Transport', 'stream_select')
+             ->expects($this->once())
+             ->with(
+                 $this->anything(), #$null,
+                 [$handle],
+                 $this->anything(), #$null,
+                 $this->anything(), #$this->sendTimeoutSec_,
+                 $this->anything() #$this->sendTimeoutUsec_
+             )
+             ->willReturn($streamSelectResult);
+
+        $this->getFunctionMock('Thrift\Transport', 'fwrite')
+             ->expects($this->exactly($fwriteCallCount))
+             ->with(
+                 $handle,
+                 'test1234456789132456798'
+             )
+             ->willReturn(false);
+
+        $this->expectException($expectedException);
+        $this->expectExceptionMessage($expectedMessage);
+        $this->expectExceptionCode($expectedCode);
+
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $transport->setHandle($handle);
+
+        $transport->write('test1234456789132456798');
+    }
+
+    public function testWrite()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $fileName = sys_get_temp_dir() . '/' . md5(mt_rand(0, time()) . time());
+        touch($fileName);
+        $handle = fopen($fileName, 'r+');
+        $transport->setHandle($handle);
+        $transport->write('test1234456789132456798');
+        $this->assertEquals('test1234456789132456798', file_get_contents($fileName));
+
+        register_shutdown_function(function () use ($fileName) {
+            is_file($fileName) && unlink($fileName);
+        });
+    }
+
+    public function writeFailDataProvider()
+    {
+        yield 'stream_select timeout' => [
+            'streamSelectResult' => 0,
+            'fwriteCallCount' => 0,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket: timed out writing 23 bytes from localhost:9090',
+            'expectedCode' => 0,
+        ];
+        yield 'stream_select fail write' => [
+            'streamSelectResult' => 1,
+            'fwriteCallCount' => 1,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket: Could not write 23 bytes localhost:9090',
+            'expectedCode' => 0,
+        ];
+        yield 'stream_select fail' => [
+            'streamSelectResult' => false,
+            'fwriteCallCount' => 0,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket: Could not write 23 bytes localhost:9090',
+            'expectedCode' => 0,
+        ];
+    }
+
+    public function testRead()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $fileName = sys_get_temp_dir() . '/' . md5(mt_rand(0, time()) . time());
+        file_put_contents($fileName, '12345678901234567890');
+        $handle = fopen($fileName, 'r+');
+        $transport->setHandle($handle);
+        $this->assertEquals('12345', $transport->read(5));
+
+        register_shutdown_function(function () use ($fileName) {
+            is_file($fileName) && unlink($fileName);
+        });
+    }
+
+    /**
+     * @dataProvider readFailDataProvider
+     */
+    public function testReadFail(
+        $streamSelectResult,
+        $freadResult,
+        $feofResult,
+        $expectedException,
+        $expectedMessage,
+        $expectedCode
+    ) {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $handle = fopen('php://memory', 'r+');
+
+        $this->getFunctionMock('Thrift\Transport', 'stream_select')
+             ->expects($this->once())
+             ->with(
+                 [$handle],
+                 $this->anything(), #$null,
+                 $this->anything(), #$null,
+                 $this->anything(), #$this->recvTimeoutSec_,
+                 $this->anything() #$this->recvTimeoutUsec_
+             )
+             ->willReturn($streamSelectResult);
+
+        $this->getFunctionMock('Thrift\Transport', 'fread')
+             ->expects($this->exactly($streamSelectResult ? 1 : 0))
+             ->with(
+                 $handle,
+                 5
+             )
+             ->willReturn($freadResult);
+        $this->getFunctionMock('Thrift\Transport', 'feof')
+             ->expects($this->exactly($feofResult ? 1 : 0))
+             ->with($handle)
+             ->willReturn($feofResult);
+
+        $this->expectException($expectedException);
+        $this->expectExceptionMessage($expectedMessage);
+        $this->expectExceptionCode($expectedCode);
+
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $transport->setHandle($handle);
+
+        $transport->read(5);
+    }
+
+    public function readFailDataProvider()
+    {
+        yield 'stream_select timeout' => [
+            'streamSelectResult' => 0,
+            'freadResult' => '',
+            'feofResult' => false,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket: timed out reading 5 bytes from localhost:9090',
+            'expectedCode' => 0,
+        ];
+        yield 'stream_select fail read' => [
+            'streamSelectResult' => 1,
+            'freadResult' => '',
+            'feofResult' => true,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket read 0 bytes',
+            'expectedCode' => 0,
+        ];
+        yield 'stream_select fail' => [
+            'streamSelectResult' => false,
+            'freadResult' => '',
+            'feofResult' => false,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket: Could not read 5 bytes from localhost:9090',
+            'expectedCode' => 0,
+        ];
+        yield 'fread false' => [
+            'streamSelectResult' => 1,
+            'freadResult' => false,
+            'feofResult' => false,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket: Could not read 5 bytes from localhost:9090',
+            'expectedCode' => 0,
+        ];
+        yield 'fread empty' => [
+            'streamSelectResult' => 1,
+            'freadResult' => '',
+            'feofResult' => true,
+            'expectedException' => TTransportException::class,
+            'expectedMessage' => 'TSocket read 0 bytes',
+            'expectedCode' => 0,
+        ];
+    }
+
+    public function testFlush()
+    {
+        $host = 'localhost';
+        $port = 9090;
+        $persist = false;
+        $debugHandler = null;
+        $transport = new TSocket(
+            $host,
+            $port,
+            $persist,
+            $debugHandler
+        );
+        $this->assertNUll($transport->flush());
+    }
+}