Merge branch '0.20.0'
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index aeb1614..0b29ddb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -76,6 +76,59 @@
           path: compiler/cpp/thrift
           retention-days: 3
 
+  lib-php:
+    needs: compiler
+    runs-on: ubuntu-20.04
+    strategy:
+      matrix:
+        php-version: [7.1, 7.2, 7.3, 7.4, 8.0, 8.1, 8.2, 8.3]
+      fail-fast: false
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Set up PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php-version }}
+          extensions: mbstring, intl, xml, curl
+          ini-values: "error_reporting=E_ALL"
+
+      - name: Install Dependencies
+        run: composer install
+
+      - name: Run bootstrap
+        run: ./bootstrap.sh
+
+      - name: Run configure
+        run: |
+          ./configure $(echo $CONFIG_ARGS_FOR_LIBS | sed 's/without-php/with-php/' | sed 's/without-php_extension/with-php_extension/' )
+
+      - uses: actions/download-artifact@v3
+        with:
+          name: thrift-compiler
+          path: compiler/cpp
+
+      - name: Run thrift-compiler
+        run: |
+          chmod a+x compiler/cpp/thrift
+          compiler/cpp/thrift -version
+
+      - name: Build Thrift Classes
+        run: |
+          mkdir -p ./lib/php/test/Resources/packages/php
+          mkdir -p ./lib/php/test/Resources/packages/phpv
+          mkdir -p ./lib/php/test/Resources/packages/phpvo
+          mkdir -p ./lib/php/test/Resources/packages/phpjs
+          mkdir -p ./lib/php/test/Resources/packages/phpcm
+          compiler/cpp/thrift --gen php -r --out ./lib/php/test/Resources/packages/php lib/php/test/Resources/ThriftTest.thrift
+          compiler/cpp/thrift --gen php:validate -r --out ./lib/php/test/Resources/packages/phpv lib/php/test/Resources/ThriftTest.thrift
+          compiler/cpp/thrift --gen php:validate,oop -r --out ./lib/php/test/Resources/packages/phpvo lib/php/test/Resources/ThriftTest.thrift
+          compiler/cpp/thrift --gen php:json -r --out ./lib/php/test/Resources/packages/phpjs lib/php/test/Resources/ThriftTest.thrift
+          compiler/cpp/thrift --gen php:classmap,server,rest -r --out ./lib/php/test/Resources/packages/phpcm lib/php/test/Resources/ThriftTest.thrift
+
+      - name: Run Tests
+        run: vendor/bin/phpunit -c lib/php/phpunit.xml
+
   lib-go:
     needs: compiler
     runs-on: ubuntu-20.04
diff --git a/.gitignore b/.gitignore
index cb8029c..0ed4729 100644
--- a/.gitignore
+++ b/.gitignore
@@ -279,9 +279,8 @@
 /lib/php/src/ext/thrift_protocol/run-tests.php
 /lib/php/src/ext/thrift_protocol/thrift_protocol.la
 /lib/php/src/ext/thrift_protocol/tmp-php.ini
-/lib/php/src/packages/
-/lib/php/test/TEST-*.xml
-/lib/php/test/packages/
+/lib/php/tests/Resources/packages/
+/lib/php/test/test-log-junit.xml
 /lib/py/dist/
 /lib/erl/logs/
 /lib/go/pkg
diff --git a/ApacheThrift.nuspec b/ApacheThrift.nuspec
index cd9a29c..a4ca51e 100644
--- a/ApacheThrift.nuspec
+++ b/ApacheThrift.nuspec
@@ -19,14 +19,14 @@
      the "Thrift" project.
   2. nuget setApiKey <your-api-key>
   3. nuget pack ApacheThrift.nuspec -Symbols -SymbolPackageFormat snupkg
-  4. nuget push ApacheThrift.0.20.0.nupkg -Source https://api.nuget.org/v3/index.json
+  4. nuget push ApacheThrift.0.21.0.nupkg -Source https://api.nuget.org/v3/index.json
   -->
 
 <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
   <metadata>
     <id>ApacheThrift</id>
-    <version>0.20.0</version>
-    <title>Apache Thrift 0.20.0</title>
+    <version>0.21.0</version>
+    <title>Apache Thrift 0.21.0</title>
     <authors>Apache Thrift Developers</authors>
     <owners>Apache Software Foundation</owners>
     <license type="expression">Apache-2.0</license>
@@ -36,7 +36,7 @@
     <description>
       Contains runtime libraries from lib/netstd for netstandard2.0 framework development.
     </description>
-    <repository type="GitHub" url="https://github.com/apache/thrift" branch="release/0.20.0" />
+    <repository type="GitHub" url="https://github.com/apache/thrift" branch="release/0.21.0" />
     <tags>Apache Thrift RPC</tags>
   </metadata>
   <files>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a685e4f..78969f2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -28,7 +28,7 @@
 
 # PACKAGE_VERSION is used by cpack scripts currently
 # Both thrift_VERSION and PACKAGE_VERSION should be the same for now
-set(thrift_VERSION "0.20.0")
+set(thrift_VERSION "0.21.0")
 set(PACKAGE_VERSION ${thrift_VERSION})
 
 project("thrift" VERSION ${PACKAGE_VERSION})
diff --git a/Thrift.podspec b/Thrift.podspec
index 8bf70f6..4fde1e5 100644
--- a/Thrift.podspec
+++ b/Thrift.podspec
@@ -1,6 +1,6 @@
 Pod::Spec.new do |s|
   s.name          = 'Thrift'
-  s.version       = '0.20.0'
+  s.version       = '0.21.0'
   s.summary       = "Apache Thrift is a lightweight, language-independent software stack with an associated code generation mechanism for RPC."
   s.description   = <<-DESC
 The Apache Thrift scalable cross-language software framework for networked services development combines a software stack with a code generation engine to build services that work efficiently and seamlessly between many programming languages.
@@ -10,6 +10,6 @@
   s.author        = { 'Apache Thrift Developers' => 'dev@thrift.apache.org' }
   s.ios.deployment_target = '9.0'
   s.osx.deployment_target = '10.10'
-  s.source        = { :git => 'https://github.com/apache/thrift.git', :tag => 'v0.20.0' }
+  s.source        = { :git => 'https://github.com/apache/thrift.git', :tag => 'v0.21.0' }
   s.source_files  = 'lib/swift/Sources/*.swift'
 end
diff --git a/appveyor.yml b/appveyor.yml
index 5847ea9..0e81ba4 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -19,7 +19,7 @@
 
 # build Apache Thrift on AppVeyor - https://ci.appveyor.com
 
-version: '0.20.0.{build}'
+version: '0.21.0.{build}'
 
 shallow_clone: true
 
diff --git a/bower.json b/bower.json
index 84f3a98..f80fe97 100644
--- a/bower.json
+++ b/bower.json
@@ -1,6 +1,6 @@
 {
   "name": "thrift",
-  "version": "0.20.0",
+  "version": "0.21.0",
   "homepage": "https://github.com/apache/thrift.git",
   "authors": [
     "Apache Thrift <dev@thrift.apache.org>"
diff --git a/build/cmake/ThriftConfig.cmake.in b/build/cmake/ThriftConfig.cmake.in
index 2f2003b..f132fe1 100644
--- a/build/cmake/ThriftConfig.cmake.in
+++ b/build/cmake/ThriftConfig.cmake.in
@@ -40,6 +40,13 @@
     set(THRIFT_LIBRARIES thriftz::thriftz)
 endif()
 
+if(@Qt5_FOUND@ AND @WITH_QT5@)
+    if (NOT TARGET thriftqt5::thriftqt5)
+        include("${THRIFT_CMAKE_DIR}/thriftqt5Targets.cmake")
+    endif()
+    set(THRIFT_LIBRARIES thriftqt5::thriftqt5)
+endif()
+
 if ("${THRIFT_LIBRARIES}" STREQUAL "")
     message(FATAL_ERROR "thrift libraries were not found")
 endif()
diff --git a/build/docker/ubuntu-bionic/Dockerfile b/build/docker/ubuntu-bionic/Dockerfile
index 350921a..5ece6e1 100644
--- a/build/docker/ubuntu-bionic/Dockerfile
+++ b/build/docker/ubuntu-bionic/Dockerfile
@@ -216,9 +216,14 @@
       php-dev \
       php-json \
       php-pear \
+      php-mbstring \
+      php-xml \
       re2c \
       composer
 
+RUN pecl install xdebug-3.1.1 && \
+      echo "zend_extension=xdebug.so" > /etc/php/7.2/cli/conf.d/20-xdebug.ini
+
 RUN apt-get install -y --no-install-recommends \
       `# Python dependencies` \
       python-all \
diff --git a/compiler/cpp/src/thrift/generate/t_cpp_generator.cc b/compiler/cpp/src/thrift/generate/t_cpp_generator.cc
index 9724fae..a085ada 100644
--- a/compiler/cpp/src/thrift/generate/t_cpp_generator.cc
+++ b/compiler/cpp/src/thrift/generate/t_cpp_generator.cc
@@ -143,14 +143,17 @@
                                   std::ostream& force_cpp_out,
                                   t_struct* tstruct,
                                   bool setters = true,
-                                  bool is_user_struct = false);
+                                  bool is_user_struct = false,
+                                  bool pointers = false);
   void generate_copy_constructor(std::ostream& out, t_struct* tstruct, bool is_exception);
   void generate_move_constructor(std::ostream& out, t_struct* tstruct, bool is_exception);
+  void generate_default_constructor(std::ostream& out, t_struct* tstruct, bool is_exception);
   void generate_constructor_helper(std::ostream& out,
                                    t_struct* tstruct,
                                    bool is_excpetion,
                                    bool is_move);
   void generate_assignment_operator(std::ostream& out, t_struct* tstruct);
+  void generate_equality_operator(std::ostream& out, t_struct* tstruct);
   void generate_move_assignment_operator(std::ostream& out, t_struct* tstruct);
   void generate_assignment_helper(std::ostream& out, t_struct* tstruct, bool is_move);
   void generate_struct_reader(std::ostream& out, t_struct* tstruct, bool pointers = false);
@@ -300,6 +303,12 @@
    */
   bool is_struct_storage_not_throwing(t_struct* tstruct) const;
 
+  /**
+   * Helper function to determine whether any of the members of our struct
+   * has a default value.
+   */
+  bool has_field_with_default_value(t_struct* tstruct);
+
 private:
   /**
    * Returns the include prefix to use for a file generated by program, or the
@@ -908,12 +917,15 @@
  */
 void t_cpp_generator::generate_cpp_struct(t_struct* tstruct, bool is_exception) {
   generate_struct_declaration(f_types_, tstruct, is_exception, false, true, true, true, true);
-  generate_struct_definition(f_types_impl_, f_types_impl_, tstruct, true, true);
+  generate_struct_definition(f_types_impl_, f_types_impl_, tstruct, true, true, false);
 
   std::ostream& out = (gen_templates_ ? f_types_tcc_ : f_types_impl_);
   generate_struct_reader(out, tstruct);
   generate_struct_writer(out, tstruct);
   generate_struct_swap(f_types_impl_, tstruct);
+  if (!gen_no_default_operators_) {
+    generate_equality_operator(f_types_impl_, tstruct);
+  }
   generate_copy_constructor(f_types_impl_, tstruct, is_exception);
   if (gen_moveable_) {
     generate_move_constructor(f_types_impl_, tstruct, is_exception);
@@ -934,6 +946,130 @@
   has_members_ = true;
 }
 
+void t_cpp_generator::generate_equality_operator(std::ostream& out, t_struct* tstruct) {
+  // Get members
+  vector<t_field*>::const_iterator m_iter;
+  const vector<t_field*>& members = tstruct->get_members();
+
+  out << indent() << "bool " << tstruct->get_name()
+      << "::operator==(const " << tstruct->get_name() << " & "
+      << (members.size() > 0 ? "rhs" : "/* rhs */") << ") const" << endl;
+  scope_up(out);
+  for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
+    // Most existing Thrift code does not use isset or optional/required,
+    // so we treat "default" fields as required.
+    if ((*m_iter)->get_req() != t_field::T_OPTIONAL) {
+      out << indent() << "if (!(" << (*m_iter)->get_name() << " == rhs."
+          << (*m_iter)->get_name() << "))" << endl << indent() << "  return false;" << endl;
+    } else {
+      out << indent() << "if (__isset." << (*m_iter)->get_name() << " != rhs.__isset."
+          << (*m_iter)->get_name() << ")" << endl << indent() << "  return false;" << endl
+          << indent() << "else if (__isset." << (*m_iter)->get_name() << " && !("
+          << (*m_iter)->get_name() << " == rhs." << (*m_iter)->get_name() << "))" << endl
+          << indent() << "  return false;" << endl;
+    }
+  }
+  indent(out) << "return true;" << endl;
+  scope_down(out);
+  out << "\n";
+}
+
+bool t_cpp_generator::has_field_with_default_value(t_struct* tstruct)
+{
+  vector<t_field*>::const_iterator m_iter;
+  const vector<t_field*>& members = tstruct->get_members();
+
+  for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
+    t_type* t = get_true_type((*m_iter)->get_type());
+    if (is_reference(*m_iter) || t->is_string()) {
+      t_const_value* cv = (*m_iter)->get_value();
+      if (cv != nullptr) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+void t_cpp_generator::generate_default_constructor(ostream& out,
+                                                   t_struct* tstruct,
+                                                   bool is_exception) {
+  // Get members
+  vector<t_field*>::const_iterator m_iter;
+  const vector<t_field*>& members = tstruct->get_members();
+
+  bool has_default_value = has_field_with_default_value(tstruct);
+
+  std::string clsname_ctor = tstruct->get_name() + "::" + tstruct->get_name() + "()";
+  indent(out) << clsname_ctor << (has_default_value ? "" : " noexcept");
+
+  //
+  // Start generating initializer list
+  //
+
+  bool init_ctor = false;
+  std::string args_indent("   ");
+
+  // Default-initialize TException, if it is our base type
+  if (is_exception)
+  {
+    out << "\n";
+    indent(out) << " : ";
+    out << "TException()";
+    init_ctor = true;
+  }
+
+  // Default-initialize all members that should be initialized in
+  // the initializer block
+  for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
+    t_type* t = get_true_type((*m_iter)->get_type());
+    if (t->is_base_type() || t->is_enum() || is_reference(*m_iter)) {
+      string dval;
+      t_const_value* cv = (*m_iter)->get_value();
+      if (cv != nullptr) {
+        dval += render_const_value(out, (*m_iter)->get_name(), t, cv);
+      } else if (t->is_enum()) {
+        dval += "static_cast<" + type_name(t) + ">(0)";
+      } else {
+        dval += (t->is_string() || is_reference(*m_iter)) ? "" : "0";
+      }
+      if (!init_ctor) {
+        init_ctor = true;
+        if(has_default_value) {
+          out << " : ";
+        } else {
+          out << '\n' << args_indent << ": ";
+          args_indent.append("  ");
+        }
+      } else {
+        out << ",\n" << args_indent;
+      }
+
+      out << (*m_iter)->get_name() << "(" << dval << ")";
+    }
+  }
+
+  //
+  // Start generating body
+  //
+
+  out << " {" << endl;
+  indent_up();
+  // TODO(dreiss): When everything else in Thrift is perfect,
+  // do more of these in the initializer list.
+  for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
+    t_type* t = get_true_type((*m_iter)->get_type());
+    if (!t->is_base_type() && !t->is_enum() && !is_reference(*m_iter)) {
+      t_const_value* cv = (*m_iter)->get_value();
+      if (cv != nullptr) {
+        print_const_value(out, (*m_iter)->get_name(), t, cv);
+      }
+    }
+  }
+  scope_down(out);
+}
+
 void t_cpp_generator::generate_copy_constructor(ostream& out,
                                                 t_struct* tstruct,
                                                 bool is_exception) {
@@ -1154,66 +1290,11 @@
                   << endl;
     }
 
-    bool has_default_value = false;
-    for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
-      t_type* t = get_true_type((*m_iter)->get_type());
-      if (is_reference(*m_iter) || t->is_string()) {
-        t_const_value* cv = (*m_iter)->get_value();
-        if (cv != nullptr) {
-          has_default_value = true;
-          break;
-        }
-      }
-    }
-
+    bool has_default_value = has_field_with_default_value(tstruct);
+    
     // Default constructor
     std::string clsname_ctor = tstruct->get_name() + "()";
-    indent(out) << clsname_ctor << (has_default_value ? "" : " noexcept");
-
-    bool init_ctor = false;
-    std::string args_indent(
-      indent().size() + clsname_ctor.size() + (has_default_value ? 3 : -1), ' ');
-
-    for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
-      t_type* t = get_true_type((*m_iter)->get_type());
-      if (t->is_base_type() || t->is_enum() || is_reference(*m_iter)) {
-        string dval;
-        t_const_value* cv = (*m_iter)->get_value();
-        if (cv != nullptr) {
-          dval += render_const_value(out, (*m_iter)->get_name(), t, cv);
-        } else if (t->is_enum()) {
-          dval += "static_cast<" + type_name(t) + ">(0)";
-        } else {
-          dval += (t->is_string() || is_reference(*m_iter)) ? "" : "0";
-        }
-        if (!init_ctor) {
-          init_ctor = true;
-          if(has_default_value) {
-            out << " : ";
-          } else {
-            out << '\n' << args_indent << ": ";
-            args_indent.append("  ");
-          }
-        } else {
-          out << ",\n" << args_indent;
-        }
-        out << (*m_iter)->get_name() << "(" << dval << ")";
-      }
-    }
-    out << " {" << endl;
-    indent_up();
-    // TODO(dreiss): When everything else in Thrift is perfect,
-    // do more of these in the initializer list.
-    for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
-      t_type* t = get_true_type((*m_iter)->get_type());
-      if (!t->is_base_type() && !t->is_enum() && !is_reference(*m_iter)) {
-        t_const_value* cv = (*m_iter)->get_value();
-        if (cv != nullptr) {
-          print_const_value(out, (*m_iter)->get_name(), t, cv);
-        }
-      }
-    }
-    scope_down(out);
+    indent(out) << clsname_ctor << (has_default_value ? "" : " noexcept") << ";" << endl;
   }
 
   if (tstruct->annotations_.find("final") == tstruct->annotations_.end()) {
@@ -1254,27 +1335,10 @@
   if (!pointers) {
     // Should we generate default operators?
     if (!gen_no_default_operators_) {
-      // Generate an equality testing operator.  Make it inline since the compiler
-      // will do a better job than we would when deciding whether to inline it.
+      // Generate an equality testing operator.
       out << indent() << "bool operator == (const " << tstruct->get_name() << " & "
-          << (members.size() > 0 ? "rhs" : "/* rhs */") << ") const" << endl;
-      scope_up(out);
-      for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
-        // Most existing Thrift code does not use isset or optional/required,
-        // so we treat "default" fields as required.
-        if ((*m_iter)->get_req() != t_field::T_OPTIONAL) {
-          out << indent() << "if (!(" << (*m_iter)->get_name() << " == rhs."
-              << (*m_iter)->get_name() << "))" << endl << indent() << "  return false;" << endl;
-        } else {
-          out << indent() << "if (__isset." << (*m_iter)->get_name() << " != rhs.__isset."
-              << (*m_iter)->get_name() << ")" << endl << indent() << "  return false;" << endl
-              << indent() << "else if (__isset." << (*m_iter)->get_name() << " && !("
-              << (*m_iter)->get_name() << " == rhs." << (*m_iter)->get_name() << "))" << endl
-              << indent() << "  return false;" << endl;
-        }
-      }
-      indent(out) << "return true;" << endl;
-      scope_down(out);
+          << (members.size() > 0 ? "rhs" : "/* rhs */") << ") const;" << endl;
+
       out << indent() << "bool operator != (const " << tstruct->get_name() << " &rhs) const {"
           << endl << indent() << "  return !(*this == rhs);" << endl << indent() << "}" << endl
           << endl;
@@ -1350,7 +1414,8 @@
                                                  ostream& force_cpp_out,
                                                  t_struct* tstruct,
                                                  bool setters,
-                                                 bool is_user_struct) {
+                                                 bool is_user_struct,
+                                                 bool pointers) {
   // Get members
   vector<t_field*>::const_iterator m_iter;
   const vector<t_field*>& members = tstruct->get_members();
@@ -1365,6 +1430,14 @@
     force_cpp_out << indent() << "}" << endl << endl;
   }
 
+  if (!pointers)
+  {
+		// 'force_cpp_out' always goes into the .cpp file, and never into a .tcc
+		// file in case templates are involved. Since the constructor is not templated,
+		// putting it into the (later included) .tcc file would cause ODR violations.
+    generate_default_constructor(force_cpp_out, tstruct, false);
+  }
+
   // Create a setter function for each field
   if (setters) {
     for (m_iter = members.begin(); m_iter != members.end(); ++m_iter) {
@@ -2000,9 +2073,10 @@
     generate_struct_definition(out, f_service_, ts, false);
     generate_struct_reader(out, ts);
     generate_struct_writer(out, ts);
+
     ts->set_name(tservice->get_name() + "_" + (*f_iter)->get_name() + "_pargs");
     generate_struct_declaration(f_header_, ts, false, true, false, true);
-    generate_struct_definition(out, f_service_, ts, false);
+    generate_struct_definition(out, f_service_, ts, false, false, true);
     generate_struct_writer(out, ts, true);
     ts->set_name(name_orig);
 
@@ -3450,7 +3524,7 @@
 
   result.set_name(tservice->get_name() + "_" + tfunction->get_name() + "_presult");
   generate_struct_declaration(f_header_, &result, false, true, true, gen_cob_style_);
-  generate_struct_definition(out, f_service_, &result, false);
+  generate_struct_definition(out, f_service_, &result, false, false, true);
   generate_struct_reader(out, &result, true);
   if (gen_cob_style_) {
     generate_struct_writer(out, &result, true);
diff --git a/compiler/cpp/src/thrift/generate/t_java_generator.cc b/compiler/cpp/src/thrift/generate/t_java_generator.cc
index d7e0b65..1985a3d 100644
--- a/compiler/cpp/src/thrift/generate/t_java_generator.cc
+++ b/compiler/cpp/src/thrift/generate/t_java_generator.cc
@@ -3635,22 +3635,23 @@
   indent(f_service_) << "public Processor(I iface) {" << endl;
   indent(f_service_) << "  super(iface, getProcessMap(new java.util.HashMap<java.lang.String, "
                         "org.apache.thrift.ProcessFunction<I, ? extends "
-                        "org.apache.thrift.TBase>>()));"
+                        "org.apache.thrift.TBase, ? extends org.apache.thrift.TBase>>()));"
                      << endl;
   indent(f_service_) << "}" << endl << endl;
 
   indent(f_service_) << "protected Processor(I iface, java.util.Map<java.lang.String, "
-                        "org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> "
-                        "processMap) {"
+                        "org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase, ? "
+                        "extends org.apache.thrift.TBase>> processMap) {"
                      << endl;
   indent(f_service_) << "  super(iface, getProcessMap(processMap));" << endl;
   indent(f_service_) << "}" << endl << endl;
 
-  indent(f_service_) << "private static <I extends Iface> java.util.Map<java.lang.String,  "
-                        "org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase>> "
+  indent(f_service_) << "private static <I extends Iface> java.util.Map<java.lang.String, "
+                        "org.apache.thrift.ProcessFunction<I, ? extends org.apache.thrift.TBase, "
+                        "? extends org.apache.thrift.TBase>> "
                         "getProcessMap(java.util.Map<java.lang.String, "
                         "org.apache.thrift.ProcessFunction<I, ? extends "
-                        " org.apache.thrift.TBase>> processMap) {"
+                        " org.apache.thrift.TBase, ? extends org.apache.thrift.TBase>> processMap) {"
                      << endl;
   indent_up();
   for (f_iter = functions.begin(); f_iter != functions.end(); ++f_iter) {
@@ -3702,13 +3703,13 @@
   indent(f_service_) << "public AsyncProcessor(I iface) {" << endl;
   indent(f_service_) << "  super(iface, getProcessMap(new java.util.HashMap<java.lang.String, "
                         "org.apache.thrift.AsyncProcessFunction<I, ? extends "
-                        "org.apache.thrift.TBase, ?>>()));"
+                        "org.apache.thrift.TBase, ?, ? extends org.apache.thrift.TBase>>()));"
                      << endl;
   indent(f_service_) << "}" << endl << endl;
 
   indent(f_service_) << "protected AsyncProcessor(I iface, java.util.Map<java.lang.String,  "
                         "org.apache.thrift.AsyncProcessFunction<I, ? extends  "
-                        "org.apache.thrift.TBase, ?>> processMap) {"
+                        "org.apache.thrift.TBase, ?, ? extends org.apache.thrift.TBase>> processMap) {"
                      << endl;
   indent(f_service_) << "  super(iface, getProcessMap(processMap));" << endl;
   indent(f_service_) << "}" << endl << endl;
@@ -3716,9 +3717,9 @@
   indent(f_service_)
       << "private static <I extends AsyncIface> java.util.Map<java.lang.String,  "
          "org.apache.thrift.AsyncProcessFunction<I, ? extends  "
-         "org.apache.thrift.TBase,?>> getProcessMap(java.util.Map<java.lang.String,  "
+         "org.apache.thrift.TBase, ?, ? extends org.apache.thrift.TBase>> getProcessMap(java.util.Map<java.lang.String,  "
          "org.apache.thrift.AsyncProcessFunction<I, ? extends  "
-         "org.apache.thrift.TBase, ?>> processMap) {"
+         "org.apache.thrift.TBase, ?, ? extends org.apache.thrift.TBase>> processMap) {"
       << endl;
   indent_up();
   for (f_iter = functions.begin(); f_iter != functions.end(); ++f_iter) {
@@ -3783,7 +3784,7 @@
   // Open class
   indent(f_service_) << "public static class " << make_valid_java_identifier(tfunction->get_name())
                      << "<I extends AsyncIface> extends org.apache.thrift.AsyncProcessFunction<I, "
-                     << argsname << ", " << resulttype << "> {" << endl;
+                     << argsname << ", " << resulttype << ", " << resultname << "> {" << endl;
   indent_up();
 
   indent(f_service_) << "public " << make_valid_java_identifier(tfunction->get_name()) << "() {" << endl;
@@ -3791,6 +3792,16 @@
   indent(f_service_) << "}" << endl << endl;
 
   indent(f_service_) << java_override_annotation() << endl;
+  indent(f_service_) << "public " << resultname << " getEmptyResultInstance() {" << endl;
+  if (tfunction->is_oneway()) {
+    indent(f_service_) << "  return null;" << endl;
+  }
+  else {
+    indent(f_service_) << "  return new " << resultname << "();" << endl;
+  }
+  indent(f_service_) << "}" << endl << endl;
+
+  indent(f_service_) << java_override_annotation() << endl;
   indent(f_service_) << "public " << argsname << " getEmptyArgsInstance() {" << endl;
   indent(f_service_) << "  return new " << argsname << "();" << endl;
   indent(f_service_) << "}" << endl << endl;
@@ -3931,7 +3942,7 @@
   indent(f_service_) << "}" << endl << endl;
 
   indent(f_service_) << java_override_annotation() << endl;
-  indent(f_service_) << "protected boolean isOneway() {" << endl;
+  indent(f_service_) << "public boolean isOneway() {" << endl;
   indent(f_service_) << "  return " << ((tfunction->is_oneway()) ? "true" : "false") << ";" << endl;
   indent(f_service_) << "}" << endl << endl;
 
@@ -3989,7 +4000,7 @@
   // Open class
   indent(f_service_) << "public static class " << make_valid_java_identifier(tfunction->get_name())
                      << "<I extends Iface> extends org.apache.thrift.ProcessFunction<I, "
-                     << argsname << "> {" << endl;
+                     << argsname << ", " << resultname << "> {" << endl;
   indent_up();
 
   indent(f_service_) << "public " << make_valid_java_identifier(tfunction->get_name()) << "() {" << endl;
@@ -4002,7 +4013,7 @@
   indent(f_service_) << "}" << endl << endl;
 
   indent(f_service_) << java_override_annotation() << endl;
-  indent(f_service_) << "protected boolean isOneway() {" << endl;
+  indent(f_service_) << "public boolean isOneway() {" << endl;
   indent(f_service_) << "  return " << ((tfunction->is_oneway()) ? "true" : "false") << ";" << endl;
   indent(f_service_) << "}" << endl << endl;
 
@@ -4013,11 +4024,21 @@
   indent(f_service_) << "}" << endl << endl;
 
   indent(f_service_) << java_override_annotation() << endl;
+  indent(f_service_) << "public " << resultname << " getEmptyResultInstance() {" << endl;
+  if (tfunction->is_oneway()) {
+    indent(f_service_) << "  return null;" << endl;
+  }
+  else {
+    indent(f_service_) << "  return new " << resultname << "();" << endl;
+  }
+  indent(f_service_) << "}" << endl << endl;
+
+  indent(f_service_) << java_override_annotation() << endl;
   indent(f_service_) << "public " << resultname << " getResult(I iface, " << argsname
                      << " args) throws org.apache.thrift.TException {" << endl;
   indent_up();
   if (!tfunction->is_oneway()) {
-    indent(f_service_) << resultname << " result = new " << resultname << "();" << endl;
+    indent(f_service_) << resultname << " result = getEmptyResultInstance();" << endl;
   }
 
   t_struct* xs = tfunction->get_xceptions();
diff --git a/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc b/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc
index a017272..78917d9 100644
--- a/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc
+++ b/compiler/cpp/src/thrift/generate/t_kotlin_generator.cc
@@ -1615,7 +1615,7 @@
                  "org.apache.thrift.AsyncProcessFunction<"
               << tservice->get_name()
               << ", out org.apache.thrift.TBase<*, "
-                 "*>, out kotlin.Any>> = mapOf("
+                 "*>, out kotlin.Any, out org.apache.thrift.TBase<*, *>>> = mapOf("
               << endl;
   indent_up();
   {
@@ -1656,16 +1656,27 @@
                                                            t_function* tfunc) {
   string args_name = tservice->get_name() + "FunctionArgs." + tfunc->get_name() + "_args";
   string rtype = type_name(tfunc->get_returntype(), true);
+  string resultname = tservice->get_name() + "FunctionResult." + tfunc->get_name() + "_result";
 
   indent(out) << "class " << tfunc->get_name() << "<I : " << tservice->get_name()
               << ">(private val scope: kotlinx.coroutines.CoroutineScope) : "
                  "org.apache.thrift.AsyncProcessFunction<I, "
-              << args_name << ", " << rtype << ">(\"" << tfunc->get_name()
-              << "\"), ProcessFunction {" << endl;
+              << args_name << ", " << rtype << ", "
+              << (tfunc->is_oneway() ? "org.apache.thrift.TBase<*, *>" : resultname)
+              << ">(\"" << tfunc->get_name() << "\"), ProcessFunction {" 
+              << endl;
   indent_up();
   {
     indent(out) << "override fun isOneway() = " << (tfunc->is_oneway() ? "true" : "false") << endl;
     indent(out) << "override fun getEmptyArgsInstance() = " << args_name << "()" << endl;
+    indent(out) << "override fun getEmptyResultInstance() = ";
+    if (tfunc->is_oneway()) {
+      out << "null" << endl;
+    }
+    else {
+      out << resultname << "()" << endl;
+    }
+    indent(out) << endl;
     indent(out) << "override fun start(iface: I, args: " << args_name
                 << ", resultHandler: org.apache.thrift.async.AsyncMethodCallback<" << rtype
                 << ">) {" << endl;
diff --git a/compiler/cpp/src/thrift/version.h b/compiler/cpp/src/thrift/version.h
index 65e0f43..9692231 100644
--- a/compiler/cpp/src/thrift/version.h
+++ b/compiler/cpp/src/thrift/version.h
@@ -24,6 +24,6 @@
 #pragma once
 #endif // _MSC_VER
 
-#define THRIFT_VERSION "0.20.0"
+#define THRIFT_VERSION "0.21.0"
 
 #endif // _THRIFT_VERSION_H_
diff --git a/composer.json b/composer.json
index 454beea..900fb28 100644
--- a/composer.json
+++ b/composer.json
@@ -18,11 +18,15 @@
         "issues": "https://issues.apache.org/jira/browse/THRIFT"
     },
     "require": {
-        "php": "^5.5 || ^7.0 || ^8.0"
+        "php": "^7.1 || ^8.0"
     },
     "require-dev": {
-        "phpunit/phpunit": "~4.8.36",
-        "squizlabs/php_codesniffer": "3.*"
+        "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+        "squizlabs/php_codesniffer": "3.*",
+        "php-mock/php-mock-phpunit": "^2.10",
+        "ext-json": "*",
+        "ext-xml": "*",
+        "ext-curl": "*"
     },
     "autoload": {
         "psr-4": {"Thrift\\": "lib/php/lib/"}
diff --git a/configure.ac b/configure.ac
index eed4a84..c8eaa6d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -20,7 +20,7 @@
 AC_PREREQ(2.65)
 AC_CONFIG_MACRO_DIR([./aclocal])
 
-AC_INIT([thrift], [0.20.0])
+AC_INIT([thrift], [0.21.0])
 
 AC_CONFIG_AUX_DIR([.])
 
@@ -214,7 +214,7 @@
   AC_PATH_PROG([GRADLE], [gradle])
   AC_SUBST(CLASSPATH)
   AC_SUBST(GRADLE_OPTS)
-  if test "x$JAVA" != "x" && test "x$JAVAC" != "x" && "x$GRADLE" != "x" ; then
+  if test "x$JAVA" != "x" && test "x$JAVAC" != "x" && test "x$GRADLE" != "x" ; then
     have_kotlin="yes"
   fi
 fi
diff --git a/contrib/Rebus/Properties/AssemblyInfo.cs b/contrib/Rebus/Properties/AssemblyInfo.cs
index 33b0aa8..f1dc685 100644
--- a/contrib/Rebus/Properties/AssemblyInfo.cs
+++ b/contrib/Rebus/Properties/AssemblyInfo.cs
@@ -34,5 +34,5 @@
 
 [assembly: Guid("0af10984-40d3-453d-b1e5-421529e8c7e2")]
 
-[assembly: AssemblyVersion("0.20.0.0")]
-[assembly: AssemblyFileVersion("0.20.0.0")]
+[assembly: AssemblyVersion("0.21.0.0")]
+[assembly: AssemblyFileVersion("0.21.0.0")]
diff --git a/contrib/thrift-maven-plugin/pom.xml b/contrib/thrift-maven-plugin/pom.xml
index 46f3cb1..54ff1e0 100644
--- a/contrib/thrift-maven-plugin/pom.xml
+++ b/contrib/thrift-maven-plugin/pom.xml
@@ -29,7 +29,7 @@
   <artifactId>thrift-maven-plugin</artifactId>
   <packaging>maven-plugin</packaging>
   <name>thrift-maven-plugin</name>
-  <version>0.20.0</version>
+  <version>0.21.0</version>
 
   <properties>
     <maven.compiler.source>1.8</maven.compiler.source>
diff --git a/contrib/thrift.spec b/contrib/thrift.spec
index 90bf4aa..20dcc57 100644
--- a/contrib/thrift.spec
+++ b/contrib/thrift.spec
@@ -28,7 +28,7 @@
 License:        Apache License v2.0
 Group:          Development
 Summary:        RPC and serialization framework
-Version:        0.20.0
+Version:        0.21.0
 Release:        0
 URL:            http://thrift.apache.org
 Packager:       Thrift Developers <dev@thrift.apache.org>
diff --git a/contrib/zeromq/csharp/AssemblyInfo.cs b/contrib/zeromq/csharp/AssemblyInfo.cs
index 8d7ed39..7f91c33 100644
--- a/contrib/zeromq/csharp/AssemblyInfo.cs
+++ b/contrib/zeromq/csharp/AssemblyInfo.cs
@@ -36,7 +36,7 @@
 // The form "{Major}.{Minor}.*" will automatically update the build and revision,
 // and "{Major}.{Minor}.{Build}.*" will update just the revision.
 
-[assembly: AssemblyVersion("0.20.0.0")]
+[assembly: AssemblyVersion("0.21.0.0")]
 
 // The following attributes are used to specify the signing key for the assembly,
 // if desired. See the Mono documentation for more information about signing.
diff --git a/doc/ReleaseManagement.md b/doc/ReleaseManagement.md
index e06c9b4..51ecead 100644
--- a/doc/ReleaseManagement.md
+++ b/doc/ReleaseManagement.md
@@ -275,9 +275,10 @@
     The CHANGES list for this release is available at:
     https://github.com/apache/thrift/blob/release/1.0.0/CHANGES.md
 
-
     Please download, verify sig/sum, install and test the libraries and languages of your choice.
 
+    I start this voting thread with my own +1 vote.
+
     This vote will close in 72 hours on 2019-07-06 21:00 UTC
 
     [ ] +1 Release this as Apache Thrift 1.0.0
diff --git a/doc/install/README.md b/doc/install/README.md
index d073e91..0ebe77c 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -32,7 +32,7 @@
     * Gradle 8.4
 * C#: Mono 1.2.4 (and pkg-config to detect it) or Visual Studio 2005+
 * Python 2.6 (including header files for extension modules)
-* PHP 5.0 (optionally including header files for extension modules)
+* PHP 7.1 (optionally including header files for extension modules)
 * Ruby 1.8
     * bundler gem
 * Erlang R12 (R11 works but not recommended)
diff --git a/doc/specs/idl.md b/doc/specs/idl.md
index 0980f5b..f5803b5 100644
--- a/doc/specs/idl.md
+++ b/doc/specs/idl.md
@@ -1,6 +1,6 @@
 ## Thrift interface description language
 
-For Thrift version 0.20.0.
+For Thrift version 0.21.0.
 
 The Thrift interface definition language (IDL) allows for the definition of [Thrift Types](/docs/types). A Thrift IDL file is processed by the Thrift code generator to produce code for the various target languages to support the defined structs and services in the IDL file.
 
diff --git a/lib/d/src/thrift/base.d b/lib/d/src/thrift/base.d
index e843a5c..ddc0dd4 100644
--- a/lib/d/src/thrift/base.d
+++ b/lib/d/src/thrift/base.d
@@ -50,7 +50,7 @@
 /// The Thrift version string, used for informative purposes.
 // Note: This is currently hardcoded, but will likely be filled in by the build
 // system in future versions.
-enum VERSION = "0.20.0";
+enum VERSION = "0.21.0";
 
 /**
  * Functions used for logging inside Thrift.
diff --git a/lib/dart/pubspec.yaml b/lib/dart/pubspec.yaml
index 7e20548..069818f 100644
--- a/lib/dart/pubspec.yaml
+++ b/lib/dart/pubspec.yaml
@@ -16,7 +16,7 @@
 # under the License.
 
 name: thrift
-version: 0.20.0
+version: 0.21.0
 description: >
   A Dart library for Apache Thrift
 author: Apache Thrift Developers <dev@thrift.apache.org>
diff --git a/lib/delphi/src/Thrift.Protocol.Compact.pas b/lib/delphi/src/Thrift.Protocol.Compact.pas
index 80f1ce5..a8ad53a 100644
--- a/lib/delphi/src/Thrift.Protocol.Compact.pas
+++ b/lib/delphi/src/Thrift.Protocol.Compact.pas
@@ -176,6 +176,7 @@
     procedure WriteI64( const i64: Int64); override;
     procedure WriteDouble( const dub: Double); override;
     procedure WriteBinary( const b: TBytes); overload; override;
+    procedure WriteBinary( const bytes : IThriftBytes); overload; override;
     procedure WriteUuid( const uuid: TGuid); override;
 
   private  // unit visible stuff
@@ -542,6 +543,14 @@
   Transport.Write( b);
 end;
 
+
+procedure TCompactProtocolImpl.WriteBinary( const bytes : IThriftBytes);
+begin
+  WriteVarint32( Cardinal(bytes.Count));
+  Transport.Write( bytes.QueryRawDataPtr, 0, bytes.Count);
+end;
+
+
 procedure TCompactProtocolImpl.WriteUuid( const uuid: TGuid);
 var network : TGuid;  // in network order (Big Endian)
 begin
diff --git a/lib/delphi/src/Thrift.Protocol.pas b/lib/delphi/src/Thrift.Protocol.pas
index f5cb454..fd92da9 100644
--- a/lib/delphi/src/Thrift.Protocol.pas
+++ b/lib/delphi/src/Thrift.Protocol.pas
@@ -28,6 +28,7 @@
   Classes,
   SysUtils,
   Contnrs,
+  Math,
   Thrift.Exception,
   Thrift.Stream,
   Thrift.Utils,
@@ -388,6 +389,7 @@
     constructor Create; overload;
     constructor Create( const bytes : TBytes); overload;
     constructor Create( var bytes : TBytes; const aTakeOwnership : Boolean = FALSE); overload;
+    constructor Create( const pData : Pointer; const nCount : Integer); overload;
 
     function ToString : string; override;
   end;
@@ -441,6 +443,7 @@
     procedure WriteI64( const i64: Int64); override;
     procedure WriteDouble( const d: Double); override;
     procedure WriteBinary( const b: TBytes); override;
+    procedure WriteBinary( const bytes : IThriftBytes); overload; override;
     procedure WriteUuid( const uuid: TGuid); override;
 
     function ReadMessageBegin: TThriftMessage; override;
@@ -507,6 +510,7 @@
     procedure WriteString( const s: string ); override;
     procedure WriteAnsiString( const s: AnsiString); override;
     procedure WriteBinary( const b: TBytes); override;
+    procedure WriteBinary( const bytes : IThriftBytes); overload; override;
     procedure WriteUuid( const uuid: TGuid); override;
 
     function ReadMessageBegin: TThriftMessage; override;
@@ -747,6 +751,8 @@
 
 
 procedure TProtocolImpl.WriteBinary( const bytes : IThriftBytes);
+// This implementation works, but is rather inefficient due to the extra memory allocation
+// Consider overwriting this for your transport implementation
 var tmp : TBytes;
 begin
   SetLength( tmp, bytes.Count);
@@ -802,6 +808,13 @@
 end;
 
 
+constructor TThriftBytesImpl.Create( const pData : Pointer; const nCount : Integer);
+begin
+  SetLength(FData, Max(nCount,0));
+  if Length(FData) > 0 then Move( pData^, FData[0], Length(FData));
+end;
+
+
 function TThriftBytesImpl.ToString : string;
 var sb : TThriftStringBuilder;
 begin
@@ -1105,6 +1118,14 @@
   if iLen > 0 then FTrans.Write(b, 0, iLen);
 end;
 
+procedure TBinaryProtocolImpl.WriteBinary( const bytes : IThriftBytes);
+var iLen : Integer;
+begin
+  iLen := bytes.Count;
+  WriteI32( iLen);
+  if iLen > 0 then FTrans.Write( bytes.QueryRawDataPtr, 0, iLen);
+end;
+
 procedure TBinaryProtocolImpl.WriteUuid( const uuid: TGuid);
 var network : TGuid;  // in network order (Big Endian)
 begin
@@ -1509,6 +1530,12 @@
 end;
 
 
+procedure TProtocolDecorator.WriteBinary( const bytes : IThriftBytes);
+begin
+  FWrappedProtocol.WriteBinary( bytes);
+end;
+
+
 procedure TProtocolDecorator.WriteUuid( const uuid: TGuid);
 begin
   FWrappedProtocol.WriteUuid( uuid);
diff --git a/lib/delphi/src/Thrift.pas b/lib/delphi/src/Thrift.pas
index 6696a19..9e1b0bb 100644
--- a/lib/delphi/src/Thrift.pas
+++ b/lib/delphi/src/Thrift.pas
@@ -28,7 +28,7 @@
   Thrift.Protocol;
 
 const
-  Version = '0.20.0';
+  Version = '0.21.0';
 
 type
   TException = Thrift.Exception.TException; // compatibility alias
diff --git a/lib/delphi/test/serializer/TestSerializer.Tests.pas b/lib/delphi/test/serializer/TestSerializer.Tests.pas
index 6ed1a48..e6a309e 100644
--- a/lib/delphi/test/serializer/TestSerializer.Tests.pas
+++ b/lib/delphi/test/serializer/TestSerializer.Tests.pas
@@ -81,6 +81,7 @@
 
     procedure Test_Serializer_Deserializer;
     procedure Test_COM_Types;
+    procedure Test_ThriftBytesCTORs;
     procedure Test_OneOfEach(     const method : TMethod; const factory : TFactoryPair; const stream : TFileStream);
     procedure Test_CompactStruct( const method : TMethod; const factory : TFactoryPair; const stream : TFileStream);
 
@@ -325,11 +326,28 @@
 end;
 
 
+procedure TTestSerializer.Test_ThriftBytesCTORs;
+var one, two : IThriftBytes;
+    bytes : TBytes;
+    sAscii : AnsiString;
+begin
+  sAscii := 'ABC/xzy';
+  bytes  := TEncoding.ASCII.GetBytes(string(sAscii));
+
+  one := TThriftBytesImpl.Create( PAnsiChar(sAscii), Length(sAscii));
+  two := TThriftBytesImpl.Create( bytes, TRUE);
+
+  ASSERT( one.Count = two.Count);
+  ASSERT( CompareMem( one.QueryRawDataPtr, two.QueryRawDataPtr, one.Count));
+end;
+
+
 procedure TTestSerializer.RunTests;
 begin
   try
     Test_Serializer_Deserializer;
     Test_COM_Types;
+    Test_ThriftBytesCTORs;
   except
     on e:Exception do begin
       Writeln( e.ClassName+': '+ e.Message);
diff --git a/lib/erl/src/thrift.app.src b/lib/erl/src/thrift.app.src
index 0e25b67..8431213 100644
--- a/lib/erl/src/thrift.app.src
+++ b/lib/erl/src/thrift.app.src
@@ -22,7 +22,7 @@
     {description, "Thrift bindings"},
 
   % The version of the applicaton
-  {vsn, "0.20.0"},
+  {vsn, "0.21.0"},
 
     % All modules used by the application.
     {modules, []},
diff --git a/lib/go/test/Makefile.am b/lib/go/test/Makefile.am
index 22fad9e..379971e 100644
--- a/lib/go/test/Makefile.am
+++ b/lib/go/test/Makefile.am
@@ -62,7 +62,8 @@
 				ProcessorMiddlewareTest.thrift \
 				ClientMiddlewareExceptionTest.thrift \
 				ValidateTest.thrift \
-				ForwardType.thrift
+				ForwardType.thrift \
+				StringParseAllocationTest.thrift
 	mkdir -p gopath/src
 	grep -v list.*map.*list.*map $(THRIFTTEST) | grep -v 'set<Insanity>' > ThriftTest.thrift
 	$(THRIFT) $(THRIFTARGS) -r IncludesTest.thrift
@@ -98,6 +99,7 @@
 	$(THRIFT) $(THRIFTARGS) ClientMiddlewareExceptionTest.thrift
 	$(THRIFT) $(THRIFTARGS) ValidateTest.thrift
 	$(THRIFT) $(THRIFTARGS) ForwardType.thrift
+	$(THRIFT) $(THRIFTARGS) StringParseAllocationTest.thrift
 	ln -nfs ../../tests gopath/src/tests
 	cp -r ./dontexportrwtest gopath/src
 	touch gopath
@@ -171,6 +173,7 @@
 	RefAnnotationFieldsTest.thrift \
 	RequiredFieldTest.thrift \
 	ServicesTest.thrift \
+	StringParseAllocationTest.thrift \
 	TypedefFieldTest.thrift \
 	UnionBinaryTest.thrift \
 	UnionDefaultValueTest.thrift \
diff --git a/lib/php/test/TestValidators.thrift b/lib/go/test/StringParseAllocationTest.thrift
similarity index 78%
copy from lib/php/test/TestValidators.thrift
copy to lib/go/test/StringParseAllocationTest.thrift
index a980470..0ede9a5 100644
--- a/lib/php/test/TestValidators.thrift
+++ b/lib/go/test/StringParseAllocationTest.thrift
@@ -17,15 +17,6 @@
  * under the License.
  */
 
-namespace php TestValidators
-
-include "../../../test/v0.16/ThriftTest.thrift"
-
-union UnionOfStrings {
-  1: string aa;
-  2: string bb;
-}
-
-service TestService {
-    void test() throws(1: ThriftTest.Xception xception);
+struct StringStruct {
+  1: required string example
 }
diff --git a/lib/go/test/tests/string_parse_allocation_test.go b/lib/go/test/tests/string_parse_allocation_test.go
new file mode 100644
index 0000000..12790c4
--- /dev/null
+++ b/lib/go/test/tests/string_parse_allocation_test.go
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+package tests
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/apache/thrift/lib/go/test/gopath/src/stringparseallocationtest"
+	"github.com/apache/thrift/lib/go/thrift"
+)
+
+func TestSimpleJsonStringParse_Allocations(t *testing.T) {
+	byteAllocationLimit := 100 * 1024 // 100 KB
+	res := testing.Benchmark(BenchmarkSimpleJsonStringParse_Allocations)
+	if res.AllocedBytesPerOp() > int64(byteAllocationLimit) {
+		t.Errorf("Total memory allocation size too high: %d (> %d)", res.AllocedBytesPerOp(), byteAllocationLimit)
+	}
+}
+
+func BenchmarkSimpleJsonStringParse_Allocations(b *testing.B) {
+	b.ReportAllocs()
+	b.StopTimer()
+	numEscapedQuotes := 1000
+	var sb strings.Builder
+	for i := 0; i < numEscapedQuotes; i++ {
+		sb.WriteString(`\"`)
+	}
+
+	testString := fmt.Sprintf(`{"1": {"str": "this is a test with %d of escaped quotes %s"}}`, numEscapedQuotes, sb.String())
+	stringStruct := stringparseallocationtest.NewStringStruct()
+	transport := thrift.NewTMemoryBuffer()
+	p := thrift.NewTJSONProtocol(transport)
+
+	for i := 0; i < b.N; i++ {
+		transport.Reset()
+		transport.WriteString(testString)
+		transport.Flush(context.Background())
+		b.StartTimer()
+		_ = stringStruct.Read(context.Background(), p)
+		b.StopTimer()
+	}
+}
diff --git a/lib/go/thrift/simple_json_protocol.go b/lib/go/thrift/simple_json_protocol.go
index 8b1284f..da12248 100644
--- a/lib/go/thrift/simple_json_protocol.go
+++ b/lib/go/thrift/simple_json_protocol.go
@@ -30,6 +30,7 @@
 	"io"
 	"math"
 	"strconv"
+	"strings"
 )
 
 type _ParseContext int
@@ -922,15 +923,7 @@
 	if err != nil {
 		return "", NewTProtocolException(err)
 	}
-	l := len(line)
-	// count number of escapes to see if we need to keep going
-	i := 1
-	for ; i < l; i++ {
-		if line[l-i-1] != '\\' {
-			break
-		}
-	}
-	if i&0x01 == 1 {
+	if endsWithoutEscapedQuote(line) {
 		v, ok := jsonUnquote(string(JSON_QUOTE) + line)
 		if !ok {
 			return "", NewTProtocolException(err)
@@ -951,27 +944,29 @@
 }
 
 func (p *TSimpleJSONProtocol) ParseQuotedStringBody() (string, error) {
-	line, err := p.reader.ReadString(JSON_QUOTE)
-	if err != nil {
-		return "", NewTProtocolException(err)
+	var sb strings.Builder
+
+	for {
+		line, err := p.reader.ReadString(JSON_QUOTE)
+		if err != nil {
+			return "", NewTProtocolException(err)
+		}
+		sb.WriteString(line)
+		if endsWithoutEscapedQuote(line) {
+			return sb.String(), nil
+		}
 	}
-	l := len(line)
-	// count number of escapes to see if we need to keep going
+}
+
+func endsWithoutEscapedQuote(s string) bool {
+	l := len(s)
 	i := 1
 	for ; i < l; i++ {
-		if line[l-i-1] != '\\' {
+		if s[l-i-1] != '\\' {
 			break
 		}
 	}
-	if i&0x01 == 1 {
-		return line, nil
-	}
-	s, err := p.ParseQuotedStringBody()
-	if err != nil {
-		return "", NewTProtocolException(err)
-	}
-	v := line + s
-	return v, nil
+	return i&0x01 == 1
 }
 
 func (p *TSimpleJSONProtocol) ParseBase64EncodedBody() ([]byte, error) {
diff --git a/lib/haxe/haxelib.json b/lib/haxe/haxelib.json
index 78779da..6f5de0b 100644
--- a/lib/haxe/haxelib.json
+++ b/lib/haxe/haxelib.json
@@ -10,7 +10,7 @@
 		"framework"
 	],
 	"description": "Haxe bindings for the Apache Thrift RPC and serialization framework",
-	"version": "0.20.0",
+	"version": "0.21.0",
 	"releasenote": "Licensed under Apache License, Version 2.0. The Apache Thrift compiler needs to be installed separately.",
 	"contributors": ["ApacheThrift"],
 	"dependencies": { 
diff --git a/lib/java/gradle.properties b/lib/java/gradle.properties
index 404318b..a3912e3 100644
--- a/lib/java/gradle.properties
+++ b/lib/java/gradle.properties
@@ -1,7 +1,7 @@
 # This file is shared currently between this Gradle build and the
 # Ant builds for fd303 and JavaScript. Keep the dotted notation for
 # the properties to minimize the changes in the dependencies.
-thrift.version=0.20.0
+thrift.version=0.21.0
 thrift.groupid=org.apache.thrift
 release=false
 
diff --git a/lib/java/src/main/java/org/apache/thrift/AsyncProcessFunction.java b/lib/java/src/main/java/org/apache/thrift/AsyncProcessFunction.java
index c7c4be3..4e65ae6 100644
--- a/lib/java/src/main/java/org/apache/thrift/AsyncProcessFunction.java
+++ b/lib/java/src/main/java/org/apache/thrift/AsyncProcessFunction.java
@@ -23,20 +23,22 @@
 import org.apache.thrift.protocol.TProtocol;
 import org.apache.thrift.server.AbstractNonblockingServer;
 
-public abstract class AsyncProcessFunction<I, T extends TBase, R> {
+public abstract class AsyncProcessFunction<I, T extends TBase, R, A extends TBase> {
   final String methodName;
 
   public AsyncProcessFunction(String methodName) {
     this.methodName = methodName;
   }
 
-  protected abstract boolean isOneway();
+  public abstract boolean isOneway();
 
   public abstract void start(I iface, T args, AsyncMethodCallback<R> resultHandler)
       throws TException;
 
   public abstract T getEmptyArgsInstance();
 
+  public abstract A getEmptyResultInstance();
+
   public abstract AsyncMethodCallback<R> getResultHandler(
       final AbstractNonblockingServer.AsyncFrameBuffer fb, int seqid);
 
diff --git a/lib/java/src/main/java/org/apache/thrift/ProcessFunction.java b/lib/java/src/main/java/org/apache/thrift/ProcessFunction.java
index 7399342..ac99d8e 100644
--- a/lib/java/src/main/java/org/apache/thrift/ProcessFunction.java
+++ b/lib/java/src/main/java/org/apache/thrift/ProcessFunction.java
@@ -8,7 +8,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public abstract class ProcessFunction<I, T extends TBase> {
+public abstract class ProcessFunction<I, T extends TBase, A extends TBase> {
   private final String methodName;
 
   private static final Logger LOGGER = LoggerFactory.getLogger(ProcessFunction.class.getName());
@@ -81,12 +81,15 @@
     return false;
   }
 
-  protected abstract boolean isOneway();
+  public abstract boolean isOneway();
 
-  public abstract TBase getResult(I iface, T args) throws TException;
+  public abstract TBase<?, ?> getResult(I iface, T args) throws TException;
 
   public abstract T getEmptyArgsInstance();
 
+  /** Returns null when this is a oneWay function. */
+  public abstract A getEmptyResultInstance();
+
   public String getMethodName() {
     return methodName;
   }
diff --git a/lib/java/src/main/java/org/apache/thrift/TBaseAsyncProcessor.java b/lib/java/src/main/java/org/apache/thrift/TBaseAsyncProcessor.java
index 266f0c0..eedb8cb 100644
--- a/lib/java/src/main/java/org/apache/thrift/TBaseAsyncProcessor.java
+++ b/lib/java/src/main/java/org/apache/thrift/TBaseAsyncProcessor.java
@@ -30,15 +30,17 @@
   protected final Logger LOGGER = LoggerFactory.getLogger(getClass().getName());
 
   final I iface;
-  final Map<String, AsyncProcessFunction<I, ? extends TBase, ?>> processMap;
+  final Map<String, AsyncProcessFunction<I, ? extends TBase, ?, ? extends TBase>> processMap;
 
   public TBaseAsyncProcessor(
-      I iface, Map<String, AsyncProcessFunction<I, ? extends TBase, ?>> processMap) {
+      I iface,
+      Map<String, AsyncProcessFunction<I, ? extends TBase, ?, ? extends TBase>> processMap) {
     this.iface = iface;
     this.processMap = processMap;
   }
 
-  public Map<String, AsyncProcessFunction<I, ? extends TBase, ?>> getProcessMapView() {
+  public Map<String, AsyncProcessFunction<I, ? extends TBase, ?, ? extends TBase>>
+      getProcessMapView() {
     return Collections.unmodifiableMap(processMap);
   }
 
diff --git a/lib/java/src/main/java/org/apache/thrift/TBaseProcessor.java b/lib/java/src/main/java/org/apache/thrift/TBaseProcessor.java
index 05cd7b8..2cd805f 100644
--- a/lib/java/src/main/java/org/apache/thrift/TBaseProcessor.java
+++ b/lib/java/src/main/java/org/apache/thrift/TBaseProcessor.java
@@ -10,15 +10,16 @@
 
 public abstract class TBaseProcessor<I> implements TProcessor {
   private final I iface;
-  private final Map<String, ProcessFunction<I, ? extends TBase>> processMap;
+  private final Map<String, ProcessFunction<I, ? extends TBase, ? extends TBase>> processMap;
 
   protected TBaseProcessor(
-      I iface, Map<String, ProcessFunction<I, ? extends TBase>> processFunctionMap) {
+      I iface,
+      Map<String, ProcessFunction<I, ? extends TBase, ? extends TBase>> processFunctionMap) {
     this.iface = iface;
     this.processMap = processFunctionMap;
   }
 
-  public Map<String, ProcessFunction<I, ? extends TBase>> getProcessMapView() {
+  public Map<String, ProcessFunction<I, ? extends TBase, ? extends TBase>> getProcessMapView() {
     return Collections.unmodifiableMap(processMap);
   }
 
diff --git a/lib/java/src/main/java/org/apache/thrift/server/TSaslNonblockingServer.java b/lib/java/src/main/java/org/apache/thrift/server/TSaslNonblockingServer.java
index 6f22d8b..8c899d5 100644
--- a/lib/java/src/main/java/org/apache/thrift/server/TSaslNonblockingServer.java
+++ b/lib/java/src/main/java/org/apache/thrift/server/TSaslNonblockingServer.java
@@ -255,7 +255,7 @@
         } else if (selected.isWritable()) {
           saslHandler.handleWrite();
         } else {
-          LOGGER.error("Invalid intrest op " + selected.interestOps());
+          LOGGER.error("Invalid interest op " + selected.interestOps());
           closeChannel(selected);
           continue;
         }
diff --git a/lib/js/package-lock.json b/lib/js/package-lock.json
index 0d9de12..8802661 100644
--- a/lib/js/package-lock.json
+++ b/lib/js/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "thrift",
-  "version": "0.20.0",
+  "version": "0.21.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
diff --git a/lib/js/package.json b/lib/js/package.json
index 8543b25..d9ab5a6 100644
--- a/lib/js/package.json
+++ b/lib/js/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thrift",
-  "version": "0.20.0",
+  "version": "0.21.0",
   "description": "Thrift is a software framework for scalable cross-language services development.",
   "main": "./src/thrift",
   "author": {
diff --git a/lib/js/src/thrift.js b/lib/js/src/thrift.js
index 7dbb560..de5ca19 100644
--- a/lib/js/src/thrift.js
+++ b/lib/js/src/thrift.js
@@ -46,7 +46,7 @@
      * @const {string} Version
      * @memberof Thrift
      */
-    Version: '0.20.0',
+    Version: '0.21.0',
 
     /**
      * Thrift IDL type string to Id mapping.
diff --git a/lib/json/schema.json b/lib/json/schema.json
index f7b10df..e2b9977 100644
--- a/lib/json/schema.json
+++ b/lib/json/schema.json
@@ -64,7 +64,7 @@
       "required": [ "typeId", "keyTypeId", "valueTypeId" ]
     },
     "struct-type": {
-      "title": "Struct, union and exception schema",
+      "title": "Struct, union, enum and exception schema",
       "type": "object",
       "properties": {
         "typeId": {
@@ -145,9 +145,9 @@
       "type": "object",
       "allOf": [
         { "$ref": "#/definitions/name-and-doc" },
-        { "$ref": "#/definitions/type-desc" },
         {
           "properties": {
+            "type": { "$ref": "#/definitions/type-desc" },
             "value": {
               "oneOf": [
                 { "type": "string" },
diff --git a/lib/lua/Thrift.lua b/lib/lua/Thrift.lua
index 1f9a562..212359b 100644
--- a/lib/lua/Thrift.lua
+++ b/lib/lua/Thrift.lua
@@ -48,7 +48,7 @@
   return count
 end
 
-version = '0.20.0'
+version = '0.21.0'
 
 TType = {
   STOP   = 0,
diff --git a/lib/netstd/Tests/Thrift.IntegrationTests/Thrift.IntegrationTests.csproj b/lib/netstd/Tests/Thrift.IntegrationTests/Thrift.IntegrationTests.csproj
index 3f83459..5602f91 100644
--- a/lib/netstd/Tests/Thrift.IntegrationTests/Thrift.IntegrationTests.csproj
+++ b/lib/netstd/Tests/Thrift.IntegrationTests/Thrift.IntegrationTests.csproj
@@ -23,7 +23,7 @@
     <LangVersion>latestMajor</LangVersion>
     <AssemblyName>Thrift.IntegrationTests</AssemblyName>
     <PackageId>Thrift.IntegrationTests</PackageId>
-    <Version>0.20.0.0</Version>
+    <Version>0.21.0.0</Version>
     <OutputType>Exe</OutputType>
     <GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
     <GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
diff --git a/lib/netstd/Tests/Thrift.PublicInterfaces.Compile.Tests/Thrift.PublicInterfaces.Compile.Tests.csproj b/lib/netstd/Tests/Thrift.PublicInterfaces.Compile.Tests/Thrift.PublicInterfaces.Compile.Tests.csproj
index 7c57750..5745bd9 100644
--- a/lib/netstd/Tests/Thrift.PublicInterfaces.Compile.Tests/Thrift.PublicInterfaces.Compile.Tests.csproj
+++ b/lib/netstd/Tests/Thrift.PublicInterfaces.Compile.Tests/Thrift.PublicInterfaces.Compile.Tests.csproj
@@ -19,7 +19,7 @@
   -->
 
   <PropertyGroup>
-    <ThriftVersion>0.20.0</ThriftVersion>
+    <ThriftVersion>0.21.0</ThriftVersion>
     <ThriftVersionOutput>Thrift version $(ThriftVersion)</ThriftVersionOutput>
     <TargetFramework>net8.0</TargetFramework>
     <LangVersion>latestMajor</LangVersion>
diff --git a/lib/netstd/Tests/Thrift.Tests/Thrift.Tests.csproj b/lib/netstd/Tests/Thrift.Tests/Thrift.Tests.csproj
index a37be1b..7f9dcb7 100644
--- a/lib/netstd/Tests/Thrift.Tests/Thrift.Tests.csproj
+++ b/lib/netstd/Tests/Thrift.Tests/Thrift.Tests.csproj
@@ -21,7 +21,7 @@
   <PropertyGroup>
     <TargetFramework>net8.0</TargetFramework>
     <LangVersion>latestMajor</LangVersion>
-    <Version>0.20.0.0</Version>
+    <Version>0.21.0.0</Version>
     <Nullable>enable</Nullable>
   </PropertyGroup>
 
diff --git a/lib/netstd/Thrift/Properties/AssemblyInfo.cs b/lib/netstd/Thrift/Properties/AssemblyInfo.cs
index bd84c4e..383b256 100644
--- a/lib/netstd/Thrift/Properties/AssemblyInfo.cs
+++ b/lib/netstd/Thrift/Properties/AssemblyInfo.cs
@@ -52,5 +52,5 @@
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 
-[assembly: AssemblyVersion("0.20.0.0")]
-[assembly: AssemblyFileVersion("0.20.0.0")]
+[assembly: AssemblyVersion("0.21.0.0")]
+[assembly: AssemblyFileVersion("0.21.0.0")]
diff --git a/lib/netstd/Thrift/Thrift.csproj b/lib/netstd/Thrift/Thrift.csproj
index d255f37..3aa16d2 100644
--- a/lib/netstd/Thrift/Thrift.csproj
+++ b/lib/netstd/Thrift/Thrift.csproj
@@ -40,8 +40,8 @@
     <SignAssembly>true</SignAssembly>
     <AssemblyOriginatorKeyFile>thrift.snk</AssemblyOriginatorKeyFile>
     <DelaySign>false</DelaySign>
-    <Title>Apache Thrift 0.20.0</Title>
-    <Version>0.20.0.0</Version>
+    <Title>Apache Thrift 0.21.0</Title>
+    <Version>0.21.0.0</Version>
     <GeneratePackageOnBuild>false</GeneratePackageOnBuild>
     <PackageProjectUrl>http://thrift.apache.org/</PackageProjectUrl>
     <Authors>Apache Thrift Developers</Authors>
@@ -50,7 +50,7 @@
     <PackageDescription>C# .NET Core bindings for the Apache Thrift RPC system</PackageDescription>
     <PackageReleaseNotes></PackageReleaseNotes>
     <PackageTags>Apache Thrift RPC</PackageTags>
-    <PackageReleaseNotes>https://github.com/apache/thrift/blob/0.20.0/CHANGES.md</PackageReleaseNotes>
+    <PackageReleaseNotes>https://github.com/apache/thrift/blob/0.21.0/CHANGES.md</PackageReleaseNotes>
 	<PackageReadmeFile>README.md</PackageReadmeFile>
     <Copyright>Copyright 2023 The Apache Software Foundation</Copyright>
   </PropertyGroup>
diff --git a/lib/nodejs/lib/thrift/framed_transport.js b/lib/nodejs/lib/thrift/framed_transport.js
index 9a50a73..058d230 100644
--- a/lib/nodejs/lib/thrift/framed_transport.js
+++ b/lib/nodejs/lib/thrift/framed_transport.js
@@ -35,30 +35,27 @@
 Object.setPrototypeOf(TFramedTransport.prototype, THeaderTransport.prototype);
 
 TFramedTransport.receiver = function(callback, seqid) {
-  var residual = [];
+  var residual = new Buffer(0);
 
   return function(data) {
-    // push received data to residual
-    for(var i = 0; i < data.length; ++i) {
-      residual.push(data[i])
-    }
+    residual = Buffer.concat([residual, Buffer.from(data)]);
 
     while (residual.length > 0) {
       if (residual.length < 4) {
         // Not enough bytes to continue, save and resume on next packet
         return;
       }
-      // get single package sieze
-      var frameSize = binary.readI32(Buffer.from(residual.slice(0, 4)), 0);
+      // Get single package size
+      var frameSize = binary.readI32(residual, 0);
       // Not enough bytes to continue, save and resume on next packet
       if (residual.length < 4 + frameSize) {
         return;
       }
 
-      // splice first 4 bytes
-      residual.splice(0, 4)
-      // get package data
-      var frame = Buffer.from(residual.splice(0, frameSize));
+      // Get package data
+      var frame = residual.subarray(4, 4 + frameSize);
+      // Remove processed data from residual
+      residual = residual.subarray(4 + frameSize);
       callback(new TFramedTransport(frame), seqid);
     }
   };
diff --git a/lib/nodejs/test/header.test.js b/lib/nodejs/test/header.test.js
index 99bb832..12f1557 100644
--- a/lib/nodejs/test/header.test.js
+++ b/lib/nodejs/test/header.test.js
@@ -100,6 +100,18 @@
     assert.equals(otherHeaders.foo, undefined);
     assert.equals(otherHeaders.otherfoo, "baz");
     assert.end();
+  },
+  "Should handle large messages without crashing": function(assert) {
+    const callback = function() {};
+    const onData = TFramedTransport.receiver(callback);
+
+    const largeChunkSize = 2 * 100 * 1024 * 1024;
+    const largeChunk = Buffer.alloc(largeChunkSize, "A");
+    const sizeBuffer = new Buffer(4);
+    sizeBuffer.writeInt32BE(largeChunkSize + 4, 0);
+    onData(Buffer.concat([sizeBuffer, largeChunk]));
+
+    assert.end();
   }
 };
 
diff --git a/lib/nodejs/test/testAll.sh b/lib/nodejs/test/testAll.sh
index 37b6b43..144832e 100755
--- a/lib/nodejs/test/testAll.sh
+++ b/lib/nodejs/test/testAll.sh
@@ -118,6 +118,7 @@
 # unit tests
 
 node ${DIR}/binary.test.js || TESTOK=1
+node ${DIR}/header.test.js || TESTOK=1
 node ${DIR}/int64.test.js || TESTOK=1
 node ${DIR}/deep-constructor.test.js || TESTOK=1
 
diff --git a/lib/ocaml/_oasis b/lib/ocaml/_oasis
index be1ce12..adf6198 100644
--- a/lib/ocaml/_oasis
+++ b/lib/ocaml/_oasis
@@ -1,5 +1,5 @@
 Name: libthrift-ocaml
-Version: 0.20.0
+Version: 0.21.0
 OASISFormat: 0.3
 Synopsis: OCaml bindings for the Apache Thrift RPC system
 Authors: Apache Thrift Developers <dev@thrift.apache.org>
diff --git a/lib/perl/lib/Thrift.pm b/lib/perl/lib/Thrift.pm
index 4b5f781..45bf33b 100644
--- a/lib/perl/lib/Thrift.pm
+++ b/lib/perl/lib/Thrift.pm
@@ -31,6 +31,6 @@
 #
 
 package Thrift;
-use version 0.77; our $VERSION = version->declare("v0.20.0");
+use version 0.77; our $VERSION = version->declare("v0.21.0");
 
 1;
diff --git a/lib/php/README.apache.md b/lib/php/README.apache.md
index 5e92589..2fae257 100644
--- a/lib/php/README.apache.md
+++ b/lib/php/README.apache.md
@@ -29,7 +29,7 @@
 
 Sample Code
 ===========
-
+```php
 <?php
 
 namespace MyNamespace;
@@ -72,3 +72,4 @@
 $transport->open();
 $processor->process($protocol, $protocol);
 $transport->close();
+```
diff --git a/lib/php/README.md b/lib/php/README.md
index e7144fe..4bbc967 100644
--- a/lib/php/README.md
+++ b/lib/php/README.md
@@ -21,7 +21,7 @@
 
 # Using Thrift with PHP
 
-Thrift requires PHP 5. Thrift makes as few assumptions about your PHP
+Thrift requires PHP 7.1 Thrift makes as few assumptions about your PHP
 environment as possible while trying to make some more advanced PHP
 features (i.e. APCu cacheing using asbolute path URLs) as simple as possible.
 
diff --git a/lib/php/lib/Base/TBase.php b/lib/php/lib/Base/TBase.php
index c61b631..d946665 100644
--- a/lib/php/lib/Base/TBase.php
+++ b/lib/php/lib/Base/TBase.php
@@ -31,6 +31,7 @@
  * of PHP. Note that code is intentionally duplicated in here to avoid making
  * function calls for every field or member of a container..
  */
+#[\AllowDynamicProperties]
 abstract class TBase
 {
     public static $tmethod = array(
diff --git a/lib/php/lib/ClassLoader/ThriftClassLoader.php b/lib/php/lib/ClassLoader/ThriftClassLoader.php
index e4b4a17..c1da4cb 100644
--- a/lib/php/lib/ClassLoader/ThriftClassLoader.php
+++ b/lib/php/lib/ClassLoader/ThriftClassLoader.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,7 +36,7 @@
 
     /**
      * Thrift definition paths
-     * @var type
+     * @var array
      */
     protected $definitions = array();
 
@@ -101,8 +102,9 @@
      */
     public function loadClass($class)
     {
-        if ((true === $this->apcu && ($file = $this->findFileInApcu($class))) or
-            ($file = $this->findFile($class))
+        if (
+            (true === $this->apcu && ($file = $this->findFileInApcu($class)))
+            || ($file = $this->findFile($class))
         ) {
             require_once $file;
         }
@@ -166,6 +168,7 @@
 
             // Ignore wrong call
             if (count($m) <= 1) {
+                #HOW TO TEST THIS? HOW TEST CASE SHOULD LOOK LIKE?
                 return;
             }
 
@@ -183,8 +186,9 @@
                      * Available in service: Interface, Client, Processor, Rest
                      * And every service methods (_.+)
                      */
-                    if (0 === preg_match('#(.+)(if|client|processor|rest)$#i', $class, $n) and
-                        0 === preg_match('#(.+)_[a-z0-9]+_(args|result)$#i', $class, $n)
+                    if (
+                        0 === preg_match('#(.+)(if|client|processor|rest)$#i', $class, $n)
+                        && 0 === preg_match('#(.+)_[a-z0-9]+_(args|result)$#i', $class, $n)
                     ) {
                         $className = 'Types';
                     } else {
diff --git a/lib/php/lib/Exception/TException.php b/lib/php/lib/Exception/TException.php
index 228d761..168688c 100644
--- a/lib/php/lib/Exception/TException.php
+++ b/lib/php/lib/Exception/TException.php
@@ -38,6 +38,7 @@
  * @param mixed $p1 Message (string) or type-spec (array)
  * @param mixed $p2 Code (integer) or values (array)
  */
+#[\AllowDynamicProperties]
 class TException extends \Exception
 {
     public function __construct($p1 = null, $p2 = 0)
diff --git a/lib/php/lib/Factory/TBinaryProtocolFactory.php b/lib/php/lib/Factory/TBinaryProtocolFactory.php
index 2519183..fc02d71 100644
--- a/lib/php/lib/Factory/TBinaryProtocolFactory.php
+++ b/lib/php/lib/Factory/TBinaryProtocolFactory.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -23,21 +24,36 @@
 namespace Thrift\Factory;
 
 use Thrift\Protocol\TBinaryProtocol;
+use Thrift\Transport\TTransport;
 
 /**
  * Binary Protocol Factory
  */
 class TBinaryProtocolFactory implements TProtocolFactory
 {
+    /**
+     * @var bool
+     */
     private $strictRead_ = false;
+    /**
+     * @var bool
+     */
     private $strictWrite_ = false;
 
+    /**
+     * @param bool $strictRead
+     * @param bool $strictWrite
+     */
     public function __construct($strictRead = false, $strictWrite = false)
     {
         $this->strictRead_ = $strictRead;
         $this->strictWrite_ = $strictWrite;
     }
 
+    /**
+     * @param TTransport $trans
+     * @return TBinaryProtocol
+     */
     public function getProtocol($trans)
     {
         return new TBinaryProtocol($trans, $this->strictRead_, $this->strictWrite_);
diff --git a/lib/php/lib/Factory/TCompactProtocolFactory.php b/lib/php/lib/Factory/TCompactProtocolFactory.php
index 11fb8ff..9171f7b 100644
--- a/lib/php/lib/Factory/TCompactProtocolFactory.php
+++ b/lib/php/lib/Factory/TCompactProtocolFactory.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -23,16 +24,17 @@
 namespace Thrift\Factory;
 
 use Thrift\Protocol\TCompactProtocol;
+use Thrift\Transport\TTransport;
 
 /**
  * Compact Protocol Factory
  */
 class TCompactProtocolFactory implements TProtocolFactory
 {
-    public function __construct()
-    {
-    }
-
+    /**
+     * @param TTransport $trans
+     * @return TCompactProtocol
+     */
     public function getProtocol($trans)
     {
         return new TCompactProtocol($trans);
diff --git a/lib/php/lib/Factory/TJSONProtocolFactory.php b/lib/php/lib/Factory/TJSONProtocolFactory.php
index fbfb1d7..44852d0 100644
--- a/lib/php/lib/Factory/TJSONProtocolFactory.php
+++ b/lib/php/lib/Factory/TJSONProtocolFactory.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -23,16 +24,17 @@
 namespace Thrift\Factory;
 
 use Thrift\Protocol\TJSONProtocol;
+use Thrift\Transport\TTransport;
 
 /**
  * JSON Protocol Factory
  */
 class TJSONProtocolFactory implements TProtocolFactory
 {
-    public function __construct()
-    {
-    }
-
+    /**
+     * @param TTransport $trans
+     * @return TJSONProtocol
+     */
     public function getProtocol($trans)
     {
         return new TJSONProtocol($trans);
diff --git a/lib/php/lib/Factory/TProtocolFactory.php b/lib/php/lib/Factory/TProtocolFactory.php
index d3066c8..be990e7 100644
--- a/lib/php/lib/Factory/TProtocolFactory.php
+++ b/lib/php/lib/Factory/TProtocolFactory.php
@@ -22,6 +22,8 @@
 
 namespace Thrift\Factory;
 
+use Thrift\Protocol\TProtocol;
+
 /**
  * Protocol factory creates protocol objects from transports
  */
@@ -30,7 +32,7 @@
     /**
      * Build a protocol from the base transport
      *
-     * @return Thrift\Protocol\TProtocol protocol
+     * @return TProtocol protocol
      */
     public function getProtocol($trans);
 }
diff --git a/lib/php/lib/Factory/TStringFuncFactory.php b/lib/php/lib/Factory/TStringFuncFactory.php
index 30de4d7..4b1f9d3 100644
--- a/lib/php/lib/Factory/TStringFuncFactory.php
+++ b/lib/php/lib/Factory/TStringFuncFactory.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/StringFunc/Core.php b/lib/php/lib/StringFunc/Core.php
index 376e437..45fb0fe 100644
--- a/lib/php/lib/StringFunc/Core.php
+++ b/lib/php/lib/StringFunc/Core.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -23,6 +24,12 @@
 
 class Core implements TStringFunc
 {
+    /**
+     * @param string $str
+     * @param int $start
+     * @param int|null $length
+     * @return false|string
+     */
     public function substr($str, $start, $length = null)
     {
         // specifying a null $length would return an empty string
@@ -33,6 +40,10 @@
         return substr((string) $str, $start, $length);
     }
 
+    /**
+     * @param string $str
+     * @return int
+     */
     public function strlen($str)
     {
         return strlen((string) $str);
diff --git a/lib/php/lib/StringFunc/Mbstring.php b/lib/php/lib/StringFunc/Mbstring.php
index ac48309..be7f38a 100644
--- a/lib/php/lib/StringFunc/Mbstring.php
+++ b/lib/php/lib/StringFunc/Mbstring.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -23,6 +24,12 @@
 
 class Mbstring implements TStringFunc
 {
+    /**
+     * @param string $str
+     * @param int $start
+     * @param int|null $length
+     * @return false|string
+     */
     public function substr($str, $start, $length = null)
     {
         /**
@@ -39,6 +46,10 @@
         return mb_substr((string) $str, $start, $length, '8bit');
     }
 
+    /**
+     * @param string $str
+     * @return int
+     */
     public function strlen($str)
     {
         return mb_strlen((string) $str, '8bit');
diff --git a/lib/php/lib/StringFunc/TStringFunc.php b/lib/php/lib/StringFunc/TStringFunc.php
index dea497f..2b7a2cf 100644
--- a/lib/php/lib/StringFunc/TStringFunc.php
+++ b/lib/php/lib/StringFunc/TStringFunc.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -23,6 +24,17 @@
 
 interface TStringFunc
 {
+    /**
+     * @param string $str
+     * @param int $start
+     * @param int|null $length
+     * @return false|string
+     */
     public function substr($str, $start, $length = null);
+
+    /**
+     * @param string $str
+     * @return int
+     */
     public function strlen($str);
 }
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
new file mode 100644
index 0000000..2cbea95
--- /dev/null
+++ b/lib/php/phpunit.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         bootstrap="../../vendor/autoload.php"
+         cacheResult="false"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         stopOnWarning="true"
+         stopOnFailure="true"
+         processIsolation="true"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
+    <filter>
+        <whitelist processUncoveredFilesFromWhitelist="true">
+            <directory suffix=".php">./lib</directory>
+        </whitelist>
+    </filter>
+    <testsuites>
+        <testsuite name="Thrift PHP Test Suite">
+            <directory>./test/Unit</directory>
+        </testsuite>
+    </testsuites>
+</phpunit>
diff --git a/lib/php/src/Thrift.php b/lib/php/src/Thrift.php
index 0364c90..1dbf64b 100644
--- a/lib/php/src/Thrift.php
+++ b/lib/php/src/Thrift.php
@@ -68,6 +68,7 @@
  * @param mixed $p1 Message (string) or type-spec (array)
  * @param mixed $p2 Code (integer) or values (array)
  */
+#[\AllowDynamicProperties]
 class TException extends Exception
 {
   public function __construct($p1=null, $p2=0)
@@ -419,6 +420,7 @@
  * of PHP. Note that code is intentionally duplicated in here to avoid making
  * function calls for every field or member of a container..
  */
+#[\AllowDynamicProperties]
 abstract class TBase
 {
   static $tmethod = array(TType::BOOL   => 'Bool',
diff --git a/lib/php/test/Fixtures.php b/lib/php/test/Fixtures/Fixtures.php
similarity index 98%
rename from lib/php/test/Fixtures.php
rename to lib/php/test/Fixtures/Fixtures.php
index fd57d83..d48be40 100644
--- a/lib/php/test/Fixtures.php
+++ b/lib/php/test/Fixtures/Fixtures.php
@@ -17,11 +17,9 @@
  * 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;
+namespace Test\Thrift\Fixtures;
 
 use ThriftTest\Xtruct;
 use ThriftTest\Xtruct2;
diff --git a/lib/php/test/Protocol/TJSONProtocolFixtures.php b/lib/php/test/Fixtures/TJSONProtocolFixtures.php
similarity index 99%
rename from lib/php/test/Protocol/TJSONProtocolFixtures.php
rename to lib/php/test/Fixtures/TJSONProtocolFixtures.php
index dd9039f..77fb270 100644
--- a/lib/php/test/Protocol/TJSONProtocolFixtures.php
+++ b/lib/php/test/Fixtures/TJSONProtocolFixtures.php
@@ -17,11 +17,9 @@
  * 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\Protocol;
+namespace Test\Thrift\Fixtures;
 
 class TJSONProtocolFixtures
 {
diff --git a/lib/php/test/Protocol/TSimpleJSONProtocolFixtures.php b/lib/php/test/Fixtures/TSimpleJSONProtocolFixtures.php
similarity index 99%
rename from lib/php/test/Protocol/TSimpleJSONProtocolFixtures.php
rename to lib/php/test/Fixtures/TSimpleJSONProtocolFixtures.php
index 547fd86..0281a87 100644
--- a/lib/php/test/Protocol/TSimpleJSONProtocolFixtures.php
+++ b/lib/php/test/Fixtures/TSimpleJSONProtocolFixtures.php
@@ -17,11 +17,9 @@
  * 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\Protocol;
+namespace Test\Thrift\Fixtures;
 
 class TSimpleJSONProtocolFixtures
 {
diff --git a/lib/php/test/Makefile.am b/lib/php/test/Makefile.am
index 30765c3..00d51f6 100644
--- a/lib/php/test/Makefile.am
+++ b/lib/php/test/Makefile.am
@@ -19,37 +19,28 @@
 
 PHPUNIT=php $(top_srcdir)/vendor/bin/phpunit
 
-stubs: ../../../test/v0.16/ThriftTest.thrift  TestValidators.thrift
-	mkdir -p ./packages/php
-	$(THRIFT) --gen php -r --out ./packages/php ../../../test/v0.16/ThriftTest.thrift
-	mkdir -p ./packages/phpv
-	mkdir -p ./packages/phpvo
-	mkdir -p ./packages/phpjs
-	$(THRIFT) --gen php:validate     -r --out ./packages/phpv   TestValidators.thrift
-	$(THRIFT) --gen php:validate,oop -r --out ./packages/phpvo  TestValidators.thrift
-	$(THRIFT) --gen php:json         -r --out ./packages/phpjs  TestValidators.thrift
+stubs: Resources/ThriftTest.thrift
+	mkdir -p ./Resources/packages/php
+	mkdir -p ./Resources/packages/phpv
+	mkdir -p ./Resources/packages/phpvo
+	mkdir -p ./Resources/packages/phpjs
+	mkdir -p ./Resources/packages/phpcm
+	$(THRIFT) --gen php -r --out ./Resources/packages/php Resources/ThriftTest.thrift
+	$(THRIFT) --gen php:validate -r --out ./Resources/packages/phpv Resources/ThriftTest.thrift
+	$(THRIFT) --gen php:validate,oop -r --out ./Resources/packages/phpvo Resources/ThriftTest.thrift
+	$(THRIFT) --gen php:json -r --out ./Resources/packages/phpjs Resources/ThriftTest.thrift
+	$(THRIFT) --gen php:classmap,server,rest -r --out ./Resources/packages/phpcm Resources/ThriftTest.thrift
 
 deps: $(top_srcdir)/composer.json
 	composer install --working-dir=$(top_srcdir)
 
 all-local: deps
 
-check-json-serializer: deps stubs
-	$(PHPUNIT) --log-junit=TEST-log-json-serializer.xml JsonSerialize/
-
-check-validator: deps stubs
-	$(PHPUNIT) --log-junit=TEST-log-validator.xml Validator/
-
-check-protocol:	deps stubs
-	$(PHPUNIT) --log-junit=TEST-log-protocol.xml Protocol/
-
-check: deps stubs \
-  check-protocol \
-  check-validator \
-  check-json-serializer
+check: deps stubs
+	$(PHPUNIT) --log-junit=test-log-junit.xml -c phpunit.xml /
 
 distclean-local:
 
 clean-local:
-	$(RM) -r ./packages
-	$(RM) TEST-*.xml
+	$(RM) -r ./Resources/packages
+	$(RM) test-log-junit.xml
diff --git a/lib/php/test/TestValidators.thrift b/lib/php/test/Resources/ThriftTest.thrift
similarity index 94%
rename from lib/php/test/TestValidators.thrift
rename to lib/php/test/Resources/ThriftTest.thrift
index a980470..07ca6e4 100644
--- a/lib/php/test/TestValidators.thrift
+++ b/lib/php/test/Resources/ThriftTest.thrift
@@ -19,7 +19,7 @@
 
 namespace php TestValidators
 
-include "../../../test/v0.16/ThriftTest.thrift"
+include "../../../../test/v0.16/ThriftTest.thrift"
 
 union UnionOfStrings {
   1: string aa;
diff --git a/lib/php/test/Validator/BaseValidatorTest.php b/lib/php/test/Unit/BaseValidatorTest.php
similarity index 90%
rename from lib/php/test/Validator/BaseValidatorTest.php
rename to lib/php/test/Unit/BaseValidatorTest.php
index 6029083..4404e72 100644
--- a/lib/php/test/Validator/BaseValidatorTest.php
+++ b/lib/php/test/Unit/BaseValidatorTest.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -18,7 +19,7 @@
  * under the License.
  */
 
-namespace Test\Thrift;
+namespace Test\Thrift\Unit;
 
 use PHPUnit\Framework\TestCase;
 use Thrift\Exception\TProtocolException;
@@ -62,6 +63,7 @@
         $transport = new TMemoryBuffer("\000");
         $protocol = new TBinaryProtocol($transport);
         $bonk->read($protocol);
+        $this->assertTrue(true);
     }
 
     public function testWriteEmpty()
@@ -73,6 +75,8 @@
             $bonk->write($protocol);
             $this->fail('Bonk was able to write an empty object');
         } catch (TProtocolException $e) {
+            $this->expectExceptionMessage('Required field Bonk.message is unset!');
+            throw $e;
         }
     }
 
@@ -87,6 +91,8 @@
             $structa->write($protocol);
             $this->fail('StructA was able to write an empty object');
         } catch (TProtocolException $e) {
+            $this->expectExceptionMessage('Required field StructA.s is unset!');
+            throw $e;
         }
     }
 
@@ -114,6 +120,8 @@
     {
         if (!static::hasReadValidator($class)) {
             static::fail($class . ' class should have a read validator');
+        } else {
+            static::assertTrue(true);
         }
     }
 
@@ -121,6 +129,8 @@
     {
         if (static::hasReadValidator($class)) {
             static::fail($class . ' class should not have a write validator');
+        } else {
+            static::assertTrue(true);
         }
     }
 
@@ -128,6 +138,8 @@
     {
         if (!static::hasWriteValidator($class)) {
             static::fail($class . ' class should have a write validator');
+        } else {
+            static::assertTrue(true);
         }
     }
 
@@ -135,6 +147,8 @@
     {
         if (static::hasWriteValidator($class)) {
             static::fail($class . ' class should not have a write validator');
+        } else {
+            static::assertTrue(true);
         }
     }
 
diff --git a/lib/php/test/Protocol/BinarySerializerTest.php b/lib/php/test/Unit/BinarySerializerTest.php
similarity index 71%
rename from lib/php/test/Protocol/BinarySerializerTest.php
rename to lib/php/test/Unit/BinarySerializerTest.php
index 71b0bb5..744ca7a 100644
--- a/lib/php/test/Protocol/BinarySerializerTest.php
+++ b/lib/php/test/Unit/BinarySerializerTest.php
@@ -17,33 +17,26 @@
  * 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\Protocol;
+namespace Test\Thrift\Unit;
 
 use PHPUnit\Framework\TestCase;
+use Thrift\ClassLoader\ThriftClassLoader;
 use Thrift\Serializer\TBinarySerializer;
 
-require __DIR__ . '/../../../../vendor/autoload.php';
-
 /***
- * This test suite depends on running the compiler against the
- * standard ThriftTest.thrift file:
- *
- * lib/php/test$ ../../../compiler/cpp/thrift --gen php -r \
- *   --out ./packages ../../../test/ThriftTest.thrift
- *
- * @runTestsInSeparateProcesses
+ * This test suite depends on running the compiler against the ./Resources/ThriftTest.thrift file:
+ * lib/php/test$ ../../../compiler/cpp/thrift --gen php -r  --out ./Resources/packages/php ./Resources/ThriftTest.thrift
  */
 class BinarySerializerTest extends TestCase
 {
-    public function setUp()
+    public function setUp(): void
     {
-        /** @var \Composer\Autoload\ClassLoader $loader */
-        $loader = require __DIR__ . '/../../../../vendor/autoload.php';
-        $loader->addPsr4('', __DIR__ . '/../packages/php');
+        $loader = new ThriftClassLoader();
+        $loader->registerNamespace('ThriftTest', __DIR__ . '/../Resources/packages/php');
+        $loader->registerDefinition('ThriftTest', __DIR__ . '/../Resources/packages/php');
+        $loader->register();
     }
 
     /**
diff --git a/lib/php/test/JsonSerialize/JsonSerializeTest.php b/lib/php/test/Unit/JsonSerializeTest.php
similarity index 86%
rename from lib/php/test/JsonSerialize/JsonSerializeTest.php
rename to lib/php/test/Unit/JsonSerializeTest.php
index c668652..66e4d5e 100644
--- a/lib/php/test/JsonSerialize/JsonSerializeTest.php
+++ b/lib/php/test/Unit/JsonSerializeTest.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -18,26 +19,24 @@
  * under the License.
  */
 
-namespace Test\Thrift\JsonSerialize;
+namespace Test\Thrift\Unit;
 
 use PHPUnit\Framework\TestCase;
 use stdClass;
+use Thrift\ClassLoader\ThriftClassLoader;
 
-require __DIR__ . '/../../../../vendor/autoload.php';
-
-/**
- * @runTestsInSeparateProcesses
+/***
+ * This test suite depends on running the compiler against the ./Resources/ThriftTest.thrift file:
+ * lib/php/test$ ../../../compiler/cpp/thrift --gen php:json -r  --out ./Resources/packages/phpjs ./Resources/ThriftTest.thrift
  */
 class JsonSerializeTest extends TestCase
 {
-    protected function setUp()
+    protected function setUp(): void
     {
-        if (version_compare(phpversion(), '5.4', '<')) {
-            $this->markTestSkipped('Requires PHP 5.4 or newer!');
-        }
-        /** @var \Composer\Autoload\ClassLoader $loader */
-        $loader = require __DIR__ . '/../../../../vendor/autoload.php';
-        $loader->addPsr4('', __DIR__ . '/../packages/phpjs');
+        $loader = new ThriftClassLoader();
+        $loader->registerNamespace('ThriftTest', __DIR__ . '/../Resources/packages/phpjs');
+        $loader->registerDefinition('ThriftTest', __DIR__ . '/../Resources/packages/phpjs');
+        $loader->register();
     }
 
     public function testEmptyStruct()
diff --git a/lib/php/test/TestValidators.thrift b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/A/TestClass.php
similarity index 78%
copy from lib/php/test/TestValidators.thrift
copy to lib/php/test/Unit/Lib/ClassLoader/Fixtures/A/TestClass.php
index a980470..3652b25 100644
--- a/lib/php/test/TestValidators.thrift
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/A/TestClass.php
@@ -1,3 +1,5 @@
+<?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -17,15 +19,12 @@
  * under the License.
  */
 
-namespace php TestValidators
+namespace A;
 
-include "../../../test/v0.16/ThriftTest.thrift"
-
-union UnionOfStrings {
-  1: string aa;
-  2: string bb;
-}
-
-service TestService {
-    void test() throws(1: ThriftTest.Xception xception);
+class TestClass
+{
+    public function __invoke()
+    {
+        return __CLASS__;
+    }
 }
diff --git a/lib/php/test/TestValidators.thrift b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/B/TestClass.php
similarity index 78%
copy from lib/php/test/TestValidators.thrift
copy to lib/php/test/Unit/Lib/ClassLoader/Fixtures/B/TestClass.php
index a980470..1d5a543 100644
--- a/lib/php/test/TestValidators.thrift
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/B/TestClass.php
@@ -1,3 +1,5 @@
+<?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -17,15 +19,12 @@
  * under the License.
  */
 
-namespace php TestValidators
+namespace B;
 
-include "../../../test/v0.16/ThriftTest.thrift"
-
-union UnionOfStrings {
-  1: string aa;
-  2: string bb;
-}
-
-service TestService {
-    void test() throws(1: ThriftTest.Xception xception);
+class TestClass
+{
+    public function __invoke()
+    {
+        return __CLASS__;
+    }
 }
diff --git a/lib/php/test/TestValidators.thrift b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/C/TestClass.php
similarity index 78%
copy from lib/php/test/TestValidators.thrift
copy to lib/php/test/Unit/Lib/ClassLoader/Fixtures/C/TestClass.php
index a980470..58bae58 100644
--- a/lib/php/test/TestValidators.thrift
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/C/TestClass.php
@@ -1,3 +1,5 @@
+<?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -17,15 +19,12 @@
  * under the License.
  */
 
-namespace php TestValidators
+namespace C;
 
-include "../../../test/v0.16/ThriftTest.thrift"
-
-union UnionOfStrings {
-  1: string aa;
-  2: string bb;
-}
-
-service TestService {
-    void test() throws(1: ThriftTest.Xception xception);
+class TestClass
+{
+    public function __invoke()
+    {
+        return __CLASS__;
+    }
 }
diff --git a/lib/php/test/TestValidators.thrift b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/D/TestClass.php
similarity index 78%
copy from lib/php/test/TestValidators.thrift
copy to lib/php/test/Unit/Lib/ClassLoader/Fixtures/D/TestClass.php
index a980470..592fe56 100644
--- a/lib/php/test/TestValidators.thrift
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/D/TestClass.php
@@ -1,3 +1,5 @@
+<?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -17,15 +19,12 @@
  * under the License.
  */
 
-namespace php TestValidators
+namespace D;
 
-include "../../../test/v0.16/ThriftTest.thrift"
-
-union UnionOfStrings {
-  1: string aa;
-  2: string bb;
-}
-
-service TestService {
-    void test() throws(1: ThriftTest.Xception xception);
+class TestClass
+{
+    public function __invoke()
+    {
+        return __CLASS__;
+    }
 }
diff --git a/lib/php/test/TestValidators.thrift b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/E/TestClass.php
similarity index 78%
copy from lib/php/test/TestValidators.thrift
copy to lib/php/test/Unit/Lib/ClassLoader/Fixtures/E/TestClass.php
index a980470..56b5679 100644
--- a/lib/php/test/TestValidators.thrift
+++ b/lib/php/test/Unit/Lib/ClassLoader/Fixtures/E/TestClass.php
@@ -1,3 +1,5 @@
+<?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -17,15 +19,12 @@
  * under the License.
  */
 
-namespace php TestValidators
+namespace E;
 
-include "../../../test/v0.16/ThriftTest.thrift"
-
-union UnionOfStrings {
-  1: string aa;
-  2: string bb;
-}
-
-service TestService {
-    void test() throws(1: ThriftTest.Xception xception);
+class TestClass
+{
+    public function __invoke()
+    {
+        return __CLASS__;
+    }
 }
diff --git a/lib/php/test/Unit/Lib/ClassLoader/ThriftClassLoaderTest.php b/lib/php/test/Unit/Lib/ClassLoader/ThriftClassLoaderTest.php
new file mode 100644
index 0000000..46ed2ec
--- /dev/null
+++ b/lib/php/test/Unit/Lib/ClassLoader/ThriftClassLoaderTest.php
@@ -0,0 +1,218 @@
+<?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\ClassLoader;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\ClassLoader\ThriftClassLoader;
+
+/***
+ * This test depends on running the compiler against the ./Resources/ThriftTest.thrift file:
+ * lib/php/test$ ../../../compiler/cpp/thrift --gen php:classmap,server,rest -r  --out ./Resources/packages/phpcm ./Resources/ThriftTest.thrift
+ */
+class ThriftClassLoaderTest extends TestCase
+{
+    use PHPMock;
+
+    /**
+     * @dataProvider registerNamespaceDataProvider
+     */
+    public function testRegisterNamespace(
+        $namespaces,
+        $class,
+        $isClassExist = true,
+        $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);
+        }
+        $loader->register();
+        $loader->loadClass($class);
+        if ($isClassExist) {
+            $this->assertTrue(class_exists($class, false), "->loadClass() loads '$class'");
+        } else {
+            $this->assertFalse(class_exists($class, false), "->loadClass() loads '$class'");
+        }
+    }
+
+    public function registerNamespaceDataProvider()
+    {
+        yield 'default' => [
+            'namespaces' => [
+                'A' => __DIR__ . '/Fixtures',
+            ],
+            'class' => 'A\TestClass',
+        ];
+        yield 'missedClass' => [
+            'namespaces' => [
+                'A' => __DIR__ . '/Fixtures',
+            ],
+            'class' => 'A\MissedClass',
+            'isClassExist' => false,
+        ];
+        yield 'pathAsArray' => [
+            'namespaces' => [
+                'B' => [__DIR__ . '/Fixtures'],
+            ],
+            'class' => 'B\TestClass',
+        ];
+        yield 'loadClassWithSlash' => [
+            'namespaces' => [
+                'C' => __DIR__ . '/Fixtures',
+            ],
+            'class' => '\C\TestClass',
+            ];
+        yield 'severalNamespaces' => [
+            'namespaces' => [
+                'D' => __DIR__ . '/Fixtures',
+                'E' => __DIR__ . '/Fixtures',
+            ],
+            'class' => '\E\TestClass',
+        ];
+        yield 'useApcu' => [
+            'namespaces' => [
+                'D' => __DIR__ . '/Fixtures',
+                'E' => __DIR__ . '/Fixtures',
+            ],
+            'class' => '\E\TestClass',
+            'isClassExist' => true,
+            'useApcu' => true,
+            'apcuPrefix' => 'APCU_PREFIX',
+        ];
+    }
+
+    /**
+     * @dataProvider registerDefinitionDataProvider
+     */
+    public function testRegisterDefinition(
+        $definitions,
+        $class,
+        $checkInterfaceExist = false,
+        $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);
+        }
+        $loader->register();
+
+        $loader->loadClass($class);
+        if ($checkInterfaceExist) {
+            $this->assertTrue(interface_exists($class, false), "->loadClass() loads '$class'");
+        } else {
+            $this->assertTrue(class_exists($class, false), "->loadClass() loads '$class'");
+        }
+    }
+
+    public function registerDefinitionDataProvider()
+    {
+        yield 'loadType' => [
+            'definitions' => [
+                'ThriftTest' => __DIR__ . '/../../../Resources/packages/phpcm',
+            ],
+            'class' => 'ThriftTest\Xtruct',
+        ];
+        yield 'loadInterface' => [
+            'definitions' => [
+                'ThriftTest' => __DIR__ . '/../../../Resources/packages/phpcm',
+            ],
+            'class' => '\ThriftTest\ThriftTestIf',
+            'checkInterfaceExist' => true,
+        ];
+        yield 'loadClient' => [
+            'definitions' => [
+                'ThriftTest' => __DIR__ . '/../../../Resources/packages/phpcm',
+            ],
+            'class' => '\ThriftTest\ThriftTestClient',
+        ];
+        yield 'loadProcessor' => [
+            'definitions' => [
+                'ThriftTest' => __DIR__ . '/../../../Resources/packages/phpcm',
+            ],
+            'class' => '\ThriftTest\ThriftTestProcessor',
+        ];
+        yield 'loadRest' => [
+            'definitions' => [
+                'ThriftTest' => __DIR__ . '/../../../Resources/packages/phpcm',
+            ],
+            'class' => '\ThriftTest\ThriftTestRest',
+        ];
+        yield 'load_args' => [
+            'definitions' => [
+                'ThriftTest' => __DIR__ . '/../../../Resources/packages/phpcm',
+            ],
+            'class' => '\ThriftTest\ThriftTest_testVoid_args',
+        ];
+        yield 'load_result' => [
+            'definitions' => [
+                'ThriftTest' => __DIR__ . '/../../../Resources/packages/phpcm',
+            ],
+            'class' => '\ThriftTest\ThriftTest_testVoid_result',
+        ];
+        yield 'pathAsArray' => [
+            'definitions' => [
+                'ThriftTest' => [__DIR__ . '/../../../Resources/packages/phpcm'],
+            ],
+            'class' => 'ThriftTest\Xtruct',
+        ];
+        yield 'severalDefinitions' => [
+            'definitions' => [
+                'ThriftTest' => [__DIR__ . '/../../../Resources/packages/phpcm'],
+                'TestValidators' => [__DIR__ . '/../../../Resources/packages/phpcm'],
+            ],
+            'class' => '\TestValidators\TestServiceClient',
+        ];
+        yield 'useApcu' => [
+            'definitions' => [
+                'ThriftTest' => [__DIR__ . '/../../../Resources/packages/phpcm'],
+                'TestValidators' => [__DIR__ . '/../../../Resources/packages/phpcm'],
+            ],
+            'class' => '\TestValidators\TestServiceClient',
+            'checkInterfaceExist' => false,
+            'useApcu' => true,
+            'apcuPrefix' => 'APCU_PREFIX',
+        ];
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Exception/TExceptionTest.php b/lib/php/test/Unit/Lib/Exception/TExceptionTest.php
new file mode 100644
index 0000000..add8803
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Exception/TExceptionTest.php
@@ -0,0 +1,60 @@
+<?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\Exception;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Exception\TException;
+
+class TExceptionTest extends TestCase
+{
+    public function testExceptionWithMessageAndCode()
+    {
+        $message = 'Test exception message';
+        $code = 42;
+
+        $exception = new TException($message, $code);
+
+        $this->assertInstanceOf(TException::class, $exception);
+        $this->assertSame($message, $exception->getMessage());
+        $this->assertSame($code, $exception->getCode());
+    }
+
+    public function testExceptionWithSpecAndVals()
+    {
+        $spec = [
+            ['var' => 'string'],
+            ['var' => 'int'],
+            ['var' => 'bool'],
+        ];
+
+        $vals = [
+            'string' => 'Test value',
+            'int' => 123456,
+            'bool' => true,
+        ];
+        $exception = new TException($spec, $vals);
+
+        $this->assertEquals('Test value', $exception->string);
+        $this->assertEquals(123456, $exception->int);
+        $this->assertEquals(true, $exception->bool);
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php
new file mode 100644
index 0000000..76ff187
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Factory/TBinaryProtocolFactoryTest.php
@@ -0,0 +1,79 @@
+<?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\Factory;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Factory\TBinaryProtocolFactory;
+use Thrift\Protocol\TBinaryProtocol;
+use Thrift\Transport\TTransport;
+
+class TBinaryProtocolFactoryTest extends TestCase
+{
+    /**
+     * @dataProvider getProtocolDataProvider
+     * @param bool $strictRead
+     * @param bool $strictWrite
+     * @return void
+     */
+    public function testGetProtocol(
+        $strictRead,
+        $strictWrite
+    ) {
+        $transport = $this->createMock(TTransport::class);
+        $factory = new TBinaryProtocolFactory($strictRead, $strictWrite);
+        $protocol = $factory->getProtocol($transport);
+
+        $this->assertInstanceOf(TBinaryProtocol::class, $protocol);
+
+        $ref = new \ReflectionClass($protocol);
+        $refStrictRead = $ref->getProperty('strictRead_');
+        $refStrictRead->setAccessible(true);
+        $refStrictWrite = $ref->getProperty('strictWrite_');
+        $refStrictWrite->setAccessible(true);
+        $refTrans = $ref->getProperty('trans_');
+        $refTrans->setAccessible(true);
+
+        $this->assertEquals($strictRead, $refStrictRead->getValue($protocol));
+        $this->assertEquals($strictWrite, $refStrictWrite->getValue($protocol));
+        $this->assertSame($transport, $refTrans->getValue($protocol));
+    }
+
+    public function getProtocolDataProvider()
+    {
+        yield 'allTrue' => [
+            'strictRead' => true,
+            'strictWrite' => true,
+        ];
+        yield 'allFalse' => [
+            'strictRead' => false,
+            'strictWrite' => false,
+        ];
+        yield 'strictReadTrue' => [
+            'strictRead' => true,
+            'strictWrite' => false,
+        ];
+        yield 'strictWriteTrue' => [
+            'strictRead' => false,
+            'strictWrite' => true,
+        ];
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php
new file mode 100644
index 0000000..1483c6a
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Factory/TCompactProtocolFactoryTest.php
@@ -0,0 +1,48 @@
+<?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\Factory;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Factory\TCompactProtocolFactory;
+use Thrift\Protocol\TCompactProtocol;
+use Thrift\Transport\TTransport;
+
+class TCompactProtocolFactoryTest extends TestCase
+{
+    /**
+     * @return void
+     */
+    public function testGetProtocol()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $factory = new TCompactProtocolFactory();
+        $protocol = $factory->getProtocol($transport);
+
+        $this->assertInstanceOf(TCompactProtocol::class, $protocol);
+
+        $ref = new \ReflectionClass($protocol);
+        $refTrans = $ref->getProperty('trans_');
+        $refTrans->setAccessible(true);
+
+        $this->assertSame($transport, $refTrans->getValue($protocol));
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php
new file mode 100644
index 0000000..3b8b5cc
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Factory/TFramedTransportFactoryTest.php
@@ -0,0 +1,54 @@
+<?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\Factory;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Factory\TFramedTransportFactory;
+use Thrift\Transport\TFramedTransport;
+use Thrift\Transport\TTransport;
+
+class TFramedTransportFactoryTest extends TestCase
+{
+    /**
+     * @return void
+     */
+    public function testGetTransport()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $factory = new TFramedTransportFactory();
+        $framedTransport = $factory::getTransport($transport);
+
+        $this->assertInstanceOf(TFramedTransport::class, $framedTransport);
+
+        $ref = new \ReflectionClass($framedTransport);
+        $refRead = $ref->getProperty('read_');
+        $refRead->setAccessible(true);
+        $refWrite = $ref->getProperty('write_');
+        $refWrite->setAccessible(true);
+        $refTrans = $ref->getProperty('transport_');
+        $refTrans->setAccessible(true);
+
+        $this->assertTrue($refRead->getValue($framedTransport));
+        $this->assertTrue($refWrite->getValue($framedTransport));
+        $this->assertSame($transport, $refTrans->getValue($framedTransport));
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Factory/TJSONProtocolFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TJSONProtocolFactoryTest.php
new file mode 100644
index 0000000..9c7055d
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Factory/TJSONProtocolFactoryTest.php
@@ -0,0 +1,48 @@
+<?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\Factory;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\Factory\TJSONProtocolFactory;
+use Thrift\Protocol\TJSONProtocol;
+use Thrift\Transport\TTransport;
+
+class TJSONProtocolFactoryTest extends TestCase
+{
+    /**
+     * @return void
+     */
+    public function testGetProtocol()
+    {
+        $transport = $this->createMock(TTransport::class);
+        $factory = new TJSONProtocolFactory();
+        $protocol = $factory->getProtocol($transport);
+
+        $this->assertInstanceOf(TJSONProtocol::class, $protocol);
+
+        $ref = new \ReflectionClass($protocol);
+        $refTrans = $ref->getProperty('trans_');
+        $refTrans->setAccessible(true);
+
+        $this->assertSame($transport, $refTrans->getValue($protocol));
+    }
+}
diff --git a/lib/php/test/Unit/Lib/Factory/TStringFuncFactoryTest.php b/lib/php/test/Unit/Lib/Factory/TStringFuncFactoryTest.php
new file mode 100644
index 0000000..c6feb2c
--- /dev/null
+++ b/lib/php/test/Unit/Lib/Factory/TStringFuncFactoryTest.php
@@ -0,0 +1,74 @@
+<?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\Factory;
+
+use phpmock\phpunit\PHPMock;
+use PHPUnit\Framework\TestCase;
+use Thrift\Factory\TStringFuncFactory;
+use Thrift\StringFunc\Core;
+use Thrift\StringFunc\Mbstring;
+use Thrift\StringFunc\TStringFunc;
+
+class TStringFuncFactoryTest extends TestCase
+{
+    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
+         */
+        $ref = new \ReflectionClass($factory);
+        $refInstance = $ref->getProperty('_instance');
+        $refInstance->setAccessible(true);
+        $refInstance->setValue($factory, null);
+
+        $stringFunc = $factory::create();
+
+        $this->assertInstanceOf(TStringFunc::class, $stringFunc);
+        $this->assertInstanceOf($expectedClass, $stringFunc);
+    }
+
+    public function createDataProvider()
+    {
+        yield 'mbstring' => [
+            'mbstring.func_overload' => 2,
+            'expected' => Mbstring::class
+        ];
+
+        yield 'string' => [
+            'mbstring.func_overload' => 0,
+            'expected' => Core::class
+        ];
+    }
+}
diff --git a/lib/php/test/Validator/ValidatorTest.php b/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php
similarity index 63%
rename from lib/php/test/Validator/ValidatorTest.php
rename to lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php
index fa6c7a9..a8a791a 100644
--- a/lib/php/test/Validator/ValidatorTest.php
+++ b/lib/php/test/Unit/Lib/Factory/TTransportFactoryTest.php
@@ -1,4 +1,5 @@
 <?php
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements. See the NOTICE file
@@ -18,24 +19,23 @@
  * under the License.
  */
 
-namespace Test\Thrift;
+namespace Test\Thrift\Unit\Lib\Factory;
 
-require __DIR__ . '/../../../../vendor/autoload.php';
+use PHPUnit\Framework\TestCase;
+use Thrift\Factory\TTransportFactory;
+use Thrift\Transport\TTransport;
 
-use Thrift\ClassLoader\ThriftClassLoader;
-
-/**
- * Class TestValidators
- * @package Test\Thrift
- *
- * @runTestsInSeparateProcesses
- */
-class ValidatorTest extends BaseValidatorTest
+class TTransportFactoryTest extends TestCase
 {
-    public function setUp()
+    /**
+     * @return void
+     */
+    public function testGetTransport()
     {
-        /** @var \Composer\Autoload\ClassLoader $loader */
-        $loader = require __DIR__ . '/../../../../vendor/autoload.php';
-        $loader->addPsr4('', __DIR__ . '/../packages/phpv');
+        $transport = $this->createMock(TTransport::class);
+        $factory = new TTransportFactory();
+        $result = $factory::getTransport($transport);
+
+        $this->assertSame($transport, $result);
     }
 }
diff --git a/lib/php/test/Unit/Lib/StringFunc/CoreTest.php b/lib/php/test/Unit/Lib/StringFunc/CoreTest.php
new file mode 100644
index 0000000..b2eaac2
--- /dev/null
+++ b/lib/php/test/Unit/Lib/StringFunc/CoreTest.php
@@ -0,0 +1,215 @@
+<?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\StringFunc;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\StringFunc\Core;
+
+class CoreTest extends TestCase
+{
+    /**
+     * @dataProvider substrDataProvider
+     */
+    public function testSubstr(
+        $expected,
+        $str,
+        $start = 0,
+        $length = null
+    ) {
+        $core = new Core();
+        $this->assertEquals($expected, $core->substr($str, $start, $length));
+    }
+
+    /**
+     * @dataProvider strlenDataProvider
+     */
+    public function testStrlen(
+        $expectedLength,
+        $str
+    ) {
+        $core = new Core();
+        $this->assertEquals($expectedLength, $core->strlen($str));
+    }
+
+    public function substrDataProvider()
+    {
+        yield 'Afrikaans' => [
+            'expected' => 'Afrikaans',
+            'str' => 'Afrikaans',
+        ];
+        yield 'Alemannisch' => [
+            'expected' => 'Alemannisch',
+            'str' => 'Alemannisch',
+        ];
+        yield 'Aragonés' => [
+            'expected' => 'Aragonés',
+            'str' => 'Aragonés',
+        ];
+        yield 'العربية' => [
+            'expected' => 'العربية',
+            'str' => 'العربية',
+        ];
+        yield 'مصرى' => [
+            'expected' => 'مصرى',
+            'str' => 'مصرى',
+        ];
+        yield 'മലയാളം' => [
+            'expected' => 'മലയാളം',
+            'str' => 'മലയാളം',
+        ];
+        yield 'Slovenščina' => [
+            'expected' => 'Slovenščina',
+            'str' => 'Slovenščina',
+        ];
+        yield 'Українська' => [
+            'expected' => 'Українська',
+            'str' => 'Українська',
+        ];
+        yield 'اردو' => [
+            'expected' => 'اردو',
+            'str' => 'اردو',
+        ];
+        yield '中文' => [
+            'expected' => '中文',
+            'str' => '中文',
+        ];
+        yield '粵語' => [
+            'expected' => '粵語',
+            'str' => '粵語',
+        ];
+        yield 'Afrikaans_SUB' => [
+            'expected' => 'rikaan',
+            'str' => 'Afrikaans',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'Alemannisch_SUB' => [
+            'expected' => 'emanni',
+            'str' => 'Alemannisch',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'Aragonés_SUB' => [
+            'expected' => 'agoné',
+            'str' => 'Aragonés',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'العربية_SUB' => [
+            'expected' => 'لعر',
+            'str' => 'العربية',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'مصرى_SUB' => [
+            'expected' => 'صرى',
+            'str' => 'مصرى',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'മലയാളം_SUB' => [
+            'expected' => 'ലയ',
+            'str' => 'മലയാളം',
+            'start' => 3,
+            'length' => 6,
+        ];
+        yield 'Slovenščina_SUB' => [
+            'expected' => 'ovenš',
+            'str' => 'Slovenščina',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'Українська_SUB' => [
+            'expected' => 'кра',
+            'str' => 'Українська',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'اردو_SUB' => [
+            'expected' => 'ردو',
+            'str' => 'اردو',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield '中文_SUB' => [
+            'expected' => '文',
+            'str' => '中文',
+            'start' => 3,
+            'length' => 3,
+        ];
+        yield '粵語_SUB' => [
+            'expected' => '語',
+            'str' => '粵語',
+            'start' => 3,
+            'length' => 3,
+        ];
+    }
+
+    public function strlenDataProvider()
+    {
+        yield 'Afrikaans' => [
+            'expectedLength' => 9,
+            'str' => 'Afrikaans',
+        ];
+        yield 'Alemannisch' => [
+            'expectedLength' => 11,
+            'str' => 'Alemannisch',
+        ];
+        yield 'Aragonés' => [
+            'expectedLength' => 9,
+            'str' => 'Aragonés',
+        ];
+        yield 'العربية' => [
+            'expectedLength' => 14,
+            'str' => 'العربية',
+        ];
+        yield 'مصرى' => [
+            'expectedLength' => 8,
+            'str' => 'مصرى',
+        ];
+        yield 'മലയാളം' => [
+            'expectedLength' => 18,
+            'str' => 'മലയാളം',
+        ];
+        yield 'Slovenščina' => [
+            'expectedLength' => 13,
+            'str' => 'Slovenščina',
+        ];
+        yield 'Українська' => [
+            'expectedLength' => 20,
+            'str' => 'Українська',
+        ];
+        yield 'اردو' => [
+            'expectedLength' => 8,
+            'str' => 'اردو',
+        ];
+        yield '中文' => [
+            'expectedLength' => 6,
+            'str' => '中文',
+        ];
+        yield '粵語' => [
+            'expectedLength' => 6,
+            'str' => '粵語',
+        ];
+    }
+}
diff --git a/lib/php/test/Unit/Lib/StringFunc/MbStringTest.php b/lib/php/test/Unit/Lib/StringFunc/MbStringTest.php
new file mode 100644
index 0000000..5670219
--- /dev/null
+++ b/lib/php/test/Unit/Lib/StringFunc/MbStringTest.php
@@ -0,0 +1,215 @@
+<?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\StringFunc;
+
+use PHPUnit\Framework\TestCase;
+use Thrift\StringFunc\Mbstring;
+
+class MbStringTest extends TestCase
+{
+    /**
+     * @dataProvider substrDataProvider
+     */
+    public function testSubstr(
+        $expected,
+        $str,
+        $start = 0,
+        $length = null
+    ) {
+        $core = new Mbstring();
+        $this->assertEquals($expected, $core->substr($str, $start, $length));
+    }
+
+    /**
+     * @dataProvider strlenDataProvider
+     */
+    public function testStrlen(
+        $expectedLength,
+        $str
+    ) {
+        $core = new Mbstring();
+        $this->assertEquals($expectedLength, $core->strlen($str));
+    }
+
+    public function substrDataProvider()
+    {
+        yield 'Afrikaans' => [
+            'expected' => 'Afrikaans',
+            'str' => 'Afrikaans',
+        ];
+        yield 'Alemannisch' => [
+            'expected' => 'Alemannisch',
+            'str' => 'Alemannisch',
+        ];
+        yield 'Aragonés' => [
+            'expected' => 'Aragonés',
+            'str' => 'Aragonés',
+        ];
+        yield 'العربية' => [
+            'expected' => 'العربية',
+            'str' => 'العربية',
+        ];
+        yield 'مصرى' => [
+            'expected' => 'مصرى',
+            'str' => 'مصرى',
+        ];
+        yield 'മലയാളം' => [
+            'expected' => 'മലയാളം',
+            'str' => 'മലയാളം',
+        ];
+        yield 'Slovenščina' => [
+            'expected' => 'Slovenščina',
+            'str' => 'Slovenščina',
+        ];
+        yield 'Українська' => [
+            'expected' => 'Українська',
+            'str' => 'Українська',
+        ];
+        yield 'اردو' => [
+            'expected' => 'اردو',
+            'str' => 'اردو',
+        ];
+        yield '中文' => [
+            'expected' => '中文',
+            'str' => '中文',
+        ];
+        yield '粵語' => [
+            'expected' => '粵語',
+            'str' => '粵語',
+        ];
+        yield 'Afrikaans_SUB' => [
+            'expected' => 'rikaan',
+            'str' => 'Afrikaans',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'Alemannisch_SUB' => [
+            'expected' => 'emanni',
+            'str' => 'Alemannisch',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'Aragonés_SUB' => [
+            'expected' => 'agoné',
+            'str' => 'Aragonés',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'العربية_SUB' => [
+            'expected' => 'لعر',
+            'str' => 'العربية',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'مصرى_SUB' => [
+            'expected' => 'صرى',
+            'str' => 'مصرى',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'മലയാളം_SUB' => [
+            'expected' => 'ലയ',
+            'str' => 'മലയാളം',
+            'start' => 3,
+            'length' => 6,
+        ];
+        yield 'Slovenščina_SUB' => [
+            'expected' => 'ovenš',
+            'str' => 'Slovenščina',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'Українська_SUB' => [
+            'expected' => 'кра',
+            'str' => 'Українська',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield 'اردو_SUB' => [
+            'expected' => 'ردو',
+            'str' => 'اردو',
+            'start' => 2,
+            'length' => 6,
+        ];
+        yield '中文_SUB' => [
+            'expected' => '文',
+            'str' => '中文',
+            'start' => 3,
+            'length' => 3,
+        ];
+        yield '粵語_SUB' => [
+            'expected' => '語',
+            'str' => '粵語',
+            'start' => 3,
+            'length' => 3,
+        ];
+    }
+
+    public function strlenDataProvider()
+    {
+        yield 'Afrikaans' => [
+            'expectedLength' => 9,
+            'str' => 'Afrikaans',
+        ];
+        yield 'Alemannisch' => [
+            'expectedLength' => 11,
+            'str' => 'Alemannisch',
+        ];
+        yield 'Aragonés' => [
+            'expectedLength' => 9,
+            'str' => 'Aragonés',
+        ];
+        yield 'العربية' => [
+            'expectedLength' => 14,
+            'str' => 'العربية',
+        ];
+        yield 'مصرى' => [
+            'expectedLength' => 8,
+            'str' => 'مصرى',
+        ];
+        yield 'മലയാളം' => [
+            'expectedLength' => 18,
+            'str' => 'മലയാളം',
+        ];
+        yield 'Slovenščina' => [
+            'expectedLength' => 13,
+            'str' => 'Slovenščina',
+        ];
+        yield 'Українська' => [
+            'expectedLength' => 20,
+            'str' => 'Українська',
+        ];
+        yield 'اردو' => [
+            'expectedLength' => 8,
+            'str' => 'اردو',
+        ];
+        yield '中文' => [
+            'expectedLength' => 6,
+            'str' => '中文',
+        ];
+        yield '粵語' => [
+            'expectedLength' => 6,
+            'str' => '粵語',
+        ];
+    }
+}
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());
+    }
+}
diff --git a/lib/php/test/Protocol/TJSONProtocolTest.php b/lib/php/test/Unit/TJSONProtocolTest.php
similarity index 94%
rename from lib/php/test/Protocol/TJSONProtocolTest.php
rename to lib/php/test/Unit/TJSONProtocolTest.php
index bf0ecce..9837803 100644
--- a/lib/php/test/Protocol/TJSONProtocolTest.php
+++ b/lib/php/test/Unit/TJSONProtocolTest.php
@@ -17,44 +17,38 @@
  * 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\Protocol;
+namespace Test\Thrift\Unit;
 
 use PHPUnit\Framework\TestCase;
-use Test\Thrift\Fixtures;
+use Test\Thrift\Fixtures\Fixtures;
+use Test\Thrift\Fixtures\TJSONProtocolFixtures;
+use Thrift\ClassLoader\ThriftClassLoader;
 use Thrift\Protocol\TJSONProtocol;
 use Thrift\Transport\TMemoryBuffer;
 
-require __DIR__ . '/../../../../vendor/autoload.php';
-
 /***
- * This test suite depends on running the compiler against the
- * standard ThriftTest.thrift file:
- *
- * lib/php/test$ ../../../compiler/cpp/thrift --gen php -r \
- *   --out ./packages ../../../test/ThriftTest.thrift
- *
- * @runTestsInSeparateProcesses
+ * This test suite depends on running the compiler against the ./Resources/ThriftTest.thrift file:
+ * lib/php/test$ ../../../compiler/cpp/thrift --gen php -r  --out ./Resources/packages/php ./Resources/ThriftTest.thrift
  */
 class TJSONProtocolTest extends TestCase
 {
     private $transport;
     private $protocol;
 
-    public static function setUpBeforeClass()
+    public static function setUpBeforeClass(): void
     {
-        /** @var \Composer\Autoload\ClassLoader $loader */
-        $loader = require __DIR__ . '/../../../../vendor/autoload.php';
-        $loader->addPsr4('', __DIR__ . '/../packages/php');
+        $loader = new ThriftClassLoader();
+        $loader->registerNamespace('ThriftTest', __DIR__ . '/../Resources/packages/php');
+        $loader->registerDefinition('ThriftTest', __DIR__ . '/../Resources/packages/php');
+        $loader->register();
 
         Fixtures::populateTestArgs();
         TJSONProtocolFixtures::populateTestArgsJSON();
     }
 
-    public function setUp()
+    public function setUp(): void
     {
         $this->transport = new TMemoryBuffer();
         $this->protocol = new TJSONProtocol($this->transport);
@@ -265,7 +259,9 @@
             TJSONProtocolFixtures::$testArgsJSON['testVoid']
         );
         $args = new \ThriftTest\ThriftTest_testVoid_args();
-        $args->read($this->protocol);
+        $result = $args->read($this->protocol);
+
+        $this->assertEquals(0, $result);
     }
 
     public function testString1Read()
diff --git a/lib/php/test/Protocol/TSimpleJSONProtocolTest.php b/lib/php/test/Unit/TSimpleJSONProtocolTest.php
similarity index 90%
rename from lib/php/test/Protocol/TSimpleJSONProtocolTest.php
rename to lib/php/test/Unit/TSimpleJSONProtocolTest.php
index e4a1373..8e6a6d9 100644
--- a/lib/php/test/Protocol/TSimpleJSONProtocolTest.php
+++ b/lib/php/test/Unit/TSimpleJSONProtocolTest.php
@@ -17,45 +17,38 @@
  * 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\Protocol;
+namespace Test\Thrift\Unit;
 
 use PHPUnit\Framework\TestCase;
-use Test\Thrift\Fixtures;
+use Test\Thrift\Fixtures\Fixtures;
+use Test\Thrift\Fixtures\TSimpleJSONProtocolFixtures;
+use Thrift\ClassLoader\ThriftClassLoader;
 use Thrift\Protocol\TSimpleJSONProtocol;
 use Thrift\Transport\TMemoryBuffer;
 
-require __DIR__ . '/../../../../vendor/autoload.php';
-
 /***
- * This test suite depends on running the compiler against the
- * standard ThriftTest.thrift file:
- *
- * lib/php/test$ ../../../compiler/cpp/thrift --gen php -r \
- *   --out ./packages ../../../test/ThriftTest.thrift
- *
- * @runTestsInSeparateProcesses
+ * This test suite depends on running the compiler against the ./Resources/ThriftTest.thrift file:
+ * lib/php/test$ ../../../compiler/cpp/thrift --gen php -r  --out ./Resources/packages/php ./Resources/ThriftTest.thrift
  */
 class TSimpleJSONProtocolTest extends TestCase
 {
     private $transport;
     private $protocol;
 
-    public static function setUpBeforeClass()
+    public static function setUpBeforeClass(): void
     {
-
-        /** @var \Composer\Autoload\ClassLoader $loader */
-        $loader = require __DIR__ . '/../../../../vendor/autoload.php';
-        $loader->addPsr4('', __DIR__ . '/../packages/php');
+        $loader = new ThriftClassLoader();
+        $loader->registerNamespace('ThriftTest', __DIR__ . '/../Resources/packages/php');
+        $loader->registerDefinition('ThriftTest', __DIR__ . '/../Resources/packages/php');
+        $loader->register();
 
         Fixtures::populateTestArgs();
         TSimpleJSONProtocolFixtures::populateTestArgsSimpleJSON();
     }
 
-    public function setUp()
+    public function setUp(): void
     {
         $this->transport = new TMemoryBuffer();
         $this->protocol = new TSimpleJSONProtocol($this->transport);
diff --git a/lib/php/test/Unit/ValidatorTest.php b/lib/php/test/Unit/ValidatorTest.php
new file mode 100644
index 0000000..b125424
--- /dev/null
+++ b/lib/php/test/Unit/ValidatorTest.php
@@ -0,0 +1,41 @@
+<?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;
+
+use Thrift\ClassLoader\ThriftClassLoader;
+
+/***
+ * This test suite depends on running the compiler against the ./Resources/ThriftTest.thrift file:
+ * lib/php/test$ ../../../compiler/cpp/thrift --gen php:validate -r  --out ./Resources/packages/phpv ./Resources/ThriftTest.thrift
+ */
+class ValidatorTest extends BaseValidatorTest
+{
+    public function setUp(): void
+    {
+        $loader = new ThriftClassLoader();
+        $loader->registerNamespace('ThriftTest', __DIR__ . '/../Resources/packages/phpv');
+        $loader->registerDefinition('ThriftTest', __DIR__ . '/../Resources/packages/phpv');
+        $loader->registerNamespace('TestValidators', __DIR__ . '/../Resources/packages/phpv');
+        $loader->registerDefinition('TestValidators', __DIR__ . '/../Resources/packages/phpv');
+        $loader->register();
+    }
+}
diff --git a/lib/php/test/Unit/ValidatorTestOop.php b/lib/php/test/Unit/ValidatorTestOop.php
new file mode 100644
index 0000000..9558182
--- /dev/null
+++ b/lib/php/test/Unit/ValidatorTestOop.php
@@ -0,0 +1,41 @@
+<?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;
+
+use Thrift\ClassLoader\ThriftClassLoader;
+
+/***
+ * This test suite depends on running the compiler against the ./Resources/ThriftTest.thrift file:
+ * lib/php/test$ ../../../compiler/cpp/thrift --gen php:validate,oop -r --out ./Resources/packages/phpvo ./Resources/ThriftTest.thrift
+ */
+class ValidatorTestOop extends BaseValidatorTest
+{
+    public function setUp(): void
+    {
+        $loader = new ThriftClassLoader();
+        $loader->registerNamespace('ThriftTest', __DIR__ . '/../Resources/packages/phpvo');
+        $loader->registerDefinition('ThriftTest', __DIR__ . '/../Resources/packages/phpvo');
+        $loader->registerNamespace('TestValidators', __DIR__ . '/../Resources/packages/phpvo');
+        $loader->registerDefinition('TestValidators', __DIR__ . '/../Resources/packages/phpvo');
+        $loader->register();
+    }
+}
diff --git a/lib/php/test/Validator/ValidatorTestOop.php b/lib/php/test/Validator/ValidatorTestOop.php
deleted file mode 100644
index 93bca4d..0000000
--- a/lib/php/test/Validator/ValidatorTestOop.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?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;
-
-require_once __DIR__ . '/../../../../vendor/autoload.php';
-
-use Thrift\ClassLoader\ThriftClassLoader;
-
-/**
- * Class TestValidatorsOop
- * @package Test\Thrift
- *
- * @runTestsInSeparateProcesses
- */
-class ValidatorTestOop extends BaseValidatorTest
-{
-    public function setUp()
-    {
-        /** @var \Composer\Autoload\ClassLoader $loader */
-        $loader = require __DIR__ . '/../../../../vendor/autoload.php';
-        $loader->addPsr4('', __DIR__ . '/../packages/phpvo');
-    }
-}
diff --git a/lib/py/setup.py b/lib/py/setup.py
index f5371af..31e2bae 100644
--- a/lib/py/setup.py
+++ b/lib/py/setup.py
@@ -105,7 +105,7 @@
     twisted_deps = ['twisted']
 
     setup(name='thrift',
-          version='0.20.0',
+          version='0.21.0',
           description='Python bindings for the Apache Thrift RPC system',
           long_description=read_file("README.md"),
           long_description_content_type="text/markdown",
diff --git a/lib/py/src/protocol/TBinaryProtocol.py b/lib/py/src/protocol/TBinaryProtocol.py
index 6b2facc..e59e0dc 100644
--- a/lib/py/src/protocol/TBinaryProtocol.py
+++ b/lib/py/src/protocol/TBinaryProtocol.py
@@ -18,6 +18,7 @@
 #
 
 from .TProtocol import TType, TProtocolBase, TProtocolException, TProtocolFactory
+from ..compat import binary_to_str
 from struct import pack, unpack
 
 
@@ -145,7 +146,7 @@
             if self.strictRead:
                 raise TProtocolException(type=TProtocolException.BAD_VERSION,
                                          message='No protocol version header')
-            name = self.trans.readAll(sz)
+            name = binary_to_str(self.trans.readAll(sz))
             type = self.readByte()
             seqid = self.readI32()
         return (name, type, seqid)
diff --git a/lib/py/test/thrift_TBinaryProtocol.py b/lib/py/test/thrift_TBinaryProtocol.py
index f7d05ff..b257626 100644
--- a/lib/py/test/thrift_TBinaryProtocol.py
+++ b/lib/py/test/thrift_TBinaryProtocol.py
@@ -152,15 +152,19 @@
     protocol.readStructEnd()
 
 
-def testMessage(data):
+def testMessage(data, strict=True):
     message = {}
     message['name'] = data[0]
     message['type'] = data[1]
     message['seqid'] = data[2]
 
+    strictRead, strictWrite = True, True
+    if not strict:
+        strictRead, strictWrite = False, False
+
     buf = TTransport.TMemoryBuffer()
     transport = TTransport.TBufferedTransportFactory().getTransport(buf)
-    protocol = TBinaryProtocol(transport)
+    protocol = TBinaryProtocol(transport, strictRead=strictRead, strictWrite=strictWrite)
     protocol.writeMessageBegin(message['name'], message['type'], message['seqid'])
     protocol.writeMessageEnd()
 
@@ -169,7 +173,7 @@
 
     buf = TTransport.TMemoryBuffer(data_r)
     transport = TTransport.TBufferedTransportFactory().getTransport(buf)
-    protocol = TBinaryProtocol(transport)
+    protocol = TBinaryProtocol(transport, strictRead=strictRead, strictWrite=strictWrite)
     result = protocol.readMessageBegin()
     protocol.readMessageEnd()
     return result
@@ -259,6 +263,24 @@
             print("Assertion fail")
             raise e
 
+    def test_TBinaryProtocol_no_strict_write_read(self):
+        TMessageType = {"T_CALL": 1, "T_REPLY": 2, "T_EXCEPTION": 3, "T_ONEWAY": 4}
+        test_data = [("short message name", TMessageType['T_CALL'], 0),
+                        ("1", TMessageType['T_REPLY'], 12345),
+                        ("loooooooooooooooooooooooooooooooooong", TMessageType['T_EXCEPTION'], 1 << 16),
+                        ("one way push", TMessageType['T_ONEWAY'], 12),
+                        ("Janky", TMessageType['T_CALL'], 0)]
+
+        try:
+            for dt in test_data:
+                result = testMessage(dt, strict=False)
+                self.assertEqual(result[0], dt[0])
+                self.assertEqual(result[1], dt[1])
+                self.assertEqual(result[2], dt[2])
+        except Exception as e:
+            print("Assertion fail")
+            raise e
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/lib/rb/thrift.gemspec b/lib/rb/thrift.gemspec
index 6b510c7..86d2189 100644
--- a/lib/rb/thrift.gemspec
+++ b/lib/rb/thrift.gemspec
@@ -3,7 +3,7 @@
 
 Gem::Specification.new do |s|
   s.name        = 'thrift'
-  s.version     = '0.20.0'
+  s.version     = '0.21.0'
   s.authors     = ['Apache Thrift Developers']
   s.email       = ['dev@thrift.apache.org']
   s.homepage    = 'http://thrift.apache.org'
diff --git a/lib/rs/Cargo.toml b/lib/rs/Cargo.toml
index a6e8533..dd4a1b6 100644
--- a/lib/rs/Cargo.toml
+++ b/lib/rs/Cargo.toml
@@ -2,7 +2,7 @@
 name = "thrift"
 description = "Rust bindings for the Apache Thrift RPC system"
 edition = "2021"
-version = "0.20.0"
+version = "0.21.0"
 license = "Apache-2.0"
 authors = ["Apache Thrift Developers <dev@thrift.apache.org>"]
 homepage = "http://thrift.apache.org"
diff --git a/lib/st/package.xml b/lib/st/package.xml
index 7af883e..d606aad 100644
--- a/lib/st/package.xml
+++ b/lib/st/package.xml
@@ -17,7 +17,7 @@
  specific language governing permissions and limitations
  under the License.
  -->
-<!-- Apache Thrift Smalltalk library version 0.20.0 -->
+<!-- Apache Thrift Smalltalk library version 0.21.0 -->
 <package>
   <name>libthrift-st</name>
   <file>thrift.st</file>
diff --git a/lib/swift/Sources/Thrift.swift b/lib/swift/Sources/Thrift.swift
index 22981a0..d78cff6 100644
--- a/lib/swift/Sources/Thrift.swift
+++ b/lib/swift/Sources/Thrift.swift
@@ -1,3 +1,3 @@
 class Thrift {
-	let version = "0.20.0"
+	let version = "0.21.0"
 }
diff --git a/lib/swift/Tests/ThriftTests/ThriftTests.swift b/lib/swift/Tests/ThriftTests/ThriftTests.swift
index 3c6854c..10121e7 100644
--- a/lib/swift/Tests/ThriftTests/ThriftTests.swift
+++ b/lib/swift/Tests/ThriftTests/ThriftTests.swift
@@ -3,7 +3,7 @@
 
 class ThriftTests: XCTestCase {
   func testVersion() {
-    XCTAssertEqual(Thrift().version, "0.20.0")
+    XCTAssertEqual(Thrift().version, "0.21.0")
   }
 
   static var allTests : [(String, (ThriftTests) -> () throws -> Void)] {
diff --git a/lib/ts/package-lock.json b/lib/ts/package-lock.json
index f99d8a3..31e0c52 100644
--- a/lib/ts/package-lock.json
+++ b/lib/ts/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "thrift",
-  "version": "0.20.0",
+  "version": "0.21.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
diff --git a/lib/ts/package.json b/lib/ts/package.json
index 054807f..f296065 100644
--- a/lib/ts/package.json
+++ b/lib/ts/package.json
@@ -1,6 +1,6 @@
 {
   "name": "thrift",
-  "version": "0.20.0",
+  "version": "0.21.0",
   "description": "Thrift is a software framework for scalable cross-language services development.",
   "author": {
     "name": "Apache Thrift Developers",
diff --git a/package-lock.json b/package-lock.json
index 3f8780a..5c3b06d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "thrift",
-  "version": "0.20.0",
+  "version": "0.21.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
diff --git a/package.json b/package.json
index 47f9e87..2925cc8 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
     "type": "git",
     "url": "https://github.com/apache/thrift.git"
   },
-  "version": "0.20.0",
+  "version": "0.21.0",
   "author": {
     "name": "Apache Thrift Developers",
     "email": "dev@thrift.apache.org",
diff --git a/sonar-project.properties b/sonar-project.properties
index 258dfe9..02b1195 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -16,7 +16,7 @@
 services that work efficiently and seamlessly between all major languages.
 
 # Apache Thrift Version
-sonar.projectVersion=0.20.0
+sonar.projectVersion=0.21.0
 # use this to set another version string
 # $ sonar-runner -D sonar.projectVersion=`git rev-parse HEAD`
 # set projectDate in combination with projectVersion for imports of old releases
@@ -54,7 +54,7 @@
 module1.sonar.projectBaseDir=lib/java
 module1.sonar.sources=src
 module1.sonar.tests=test
-module1.sonar.binaries=build/libs/libthrift-0.20.0.jar
+module1.sonar.binaries=build/libs/libthrift-0.21.0.jar
 module1.sonar.libraries=build/deps/*.jar
 module1.sonar.language=java
 
@@ -62,7 +62,7 @@
 module2.sonar.projectBaseDir=.
 module2.sonar.sources=tutorial/java/src, tutorial/java/gen-java
 module2.sonar.binaries=tutorial/java/tutorial.jar
-module2.sonar.libraries=lib/java/build/deps/*.jar,lib/java/build/libs/libthrift-0.20.0.jar
+module2.sonar.libraries=lib/java/build/deps/*.jar,lib/java/build/libs/libthrift-0.21.0.jar
 module2.sonar.language=java
 
 module3.sonar.projectName=Apache Thrift - JavaScript Library
diff --git a/test/dart/test_client/pubspec.yaml b/test/dart/test_client/pubspec.yaml
index 177e2cd..f4d0135 100644
--- a/test/dart/test_client/pubspec.yaml
+++ b/test/dart/test_client/pubspec.yaml
@@ -16,7 +16,7 @@
 # under the License.
 
 name: thrift_test_client
-version: 0.20.0
+version: 0.21.0
 description: A client integration test for the Dart Thrift library
 author: Apache Thrift Developers <dev@thrift.apache.org>
 homepage: http://thrift.apache.org
diff --git a/test/erl/src/thrift_test.app.src b/test/erl/src/thrift_test.app.src
index 9d68a56..b78fc29 100644
--- a/test/erl/src/thrift_test.app.src
+++ b/test/erl/src/thrift_test.app.src
@@ -22,7 +22,7 @@
   {description, "Thrift cross language test"},
 
   % The version of the applicaton
-  {vsn, "0.20.0"},
+  {vsn, "0.21.0"},
 
   % All modules used by the application.
   {modules, [
diff --git a/test/netstd/Client/Client.csproj b/test/netstd/Client/Client.csproj
index aa90e2c..977303b 100644
--- a/test/netstd/Client/Client.csproj
+++ b/test/netstd/Client/Client.csproj
@@ -24,7 +24,7 @@
     <AssemblyName>Client</AssemblyName>
     <PackageId>Client</PackageId>
     <OutputType>Exe</OutputType>
-    <Version>0.20.0.0</Version>
+    <Version>0.21.0.0</Version>
     <GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
     <GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
     <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
diff --git a/test/netstd/Server/Server.csproj b/test/netstd/Server/Server.csproj
index 63ce613..359578a 100644
--- a/test/netstd/Server/Server.csproj
+++ b/test/netstd/Server/Server.csproj
@@ -24,7 +24,7 @@
     <AssemblyName>Server</AssemblyName>
     <PackageId>Server</PackageId>
     <OutputType>Exe</OutputType>
-    <Version>0.20.0.0</Version>
+    <Version>0.21.0.0</Version>
     <GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
     <GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
     <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
diff --git a/tutorial/dart/client/pubspec.yaml b/tutorial/dart/client/pubspec.yaml
index e8c6db8..b486ec8 100644
--- a/tutorial/dart/client/pubspec.yaml
+++ b/tutorial/dart/client/pubspec.yaml
@@ -16,7 +16,7 @@
 # under the License.
 
 name: tutorial_client
-version: 0.20.0
+version: 0.21.0
 description: A Dart client implementation of the Apache Thrift tutorial
 author: Apache Thrift Developers <dev@thrift.apache.org>
 homepage: http://thrift.apache.org
diff --git a/tutorial/dart/console_client/pubspec.yaml b/tutorial/dart/console_client/pubspec.yaml
index e5c0938..bf61ded 100644
--- a/tutorial/dart/console_client/pubspec.yaml
+++ b/tutorial/dart/console_client/pubspec.yaml
@@ -16,7 +16,7 @@
 # under the License.
 
 name: tutorial_console_client
-version: 0.20.0
+version: 0.21.0
 description: >
   A Dart console client to implementation of the Apache Thrift tutorial
 author: Apache Thrift Developers <dev@thrift.apache.org>
diff --git a/tutorial/dart/server/pubspec.yaml b/tutorial/dart/server/pubspec.yaml
index 5f7edb9..29811eb 100644
--- a/tutorial/dart/server/pubspec.yaml
+++ b/tutorial/dart/server/pubspec.yaml
@@ -16,7 +16,7 @@
 # under the License.
 
 name: tutorial_server
-version: 0.20.0
+version: 0.21.0
 description: A Dart server to support the Apache Thrift tutorial
 author: Apache Thrift Developers <dev@thrift.apache.org>
 homepage: http://thrift.apache.org
diff --git a/tutorial/delphi/DelphiClient/DelphiClient.dproj b/tutorial/delphi/DelphiClient/DelphiClient.dproj
index 34d9f03..fbb63f7 100644
--- a/tutorial/delphi/DelphiClient/DelphiClient.dproj
+++ b/tutorial/delphi/DelphiClient/DelphiClient.dproj
@@ -124,13 +124,13 @@
 					<VersionInfoKeys>
 						<VersionInfoKeys Name="CompanyName"/>
 						<VersionInfoKeys Name="FileDescription">Thrift Tutorial</VersionInfoKeys>
-						<VersionInfoKeys Name="FileVersion">0.20.0.0</VersionInfoKeys>
+						<VersionInfoKeys Name="FileVersion">0.21.0.0</VersionInfoKeys>
 						<VersionInfoKeys Name="InternalName">DelphiClient</VersionInfoKeys>
 						<VersionInfoKeys Name="LegalCopyright">Copyright © 2012 The Apache Software Foundation</VersionInfoKeys>
 						<VersionInfoKeys Name="LegalTrademarks"/>
 						<VersionInfoKeys Name="OriginalFilename">DelphiClient.exe</VersionInfoKeys>
 						<VersionInfoKeys Name="ProductName">Thrift</VersionInfoKeys>
-						<VersionInfoKeys Name="ProductVersion">0.20.0.0</VersionInfoKeys>
+						<VersionInfoKeys Name="ProductVersion">0.21.0.0</VersionInfoKeys>
 						<VersionInfoKeys Name="Comments"/>
 					</VersionInfoKeys>
 					<Source>
diff --git a/tutorial/delphi/DelphiServer/DelphiServer.dproj b/tutorial/delphi/DelphiServer/DelphiServer.dproj
index fa8cb92..376d90a 100644
--- a/tutorial/delphi/DelphiServer/DelphiServer.dproj
+++ b/tutorial/delphi/DelphiServer/DelphiServer.dproj
@@ -121,13 +121,13 @@
 					<VersionInfoKeys>
 						<VersionInfoKeys Name="CompanyName"/>
 						<VersionInfoKeys Name="FileDescription">Thrift Tutorial</VersionInfoKeys>
-						<VersionInfoKeys Name="FileVersion">0.20.0.0</VersionInfoKeys>
+						<VersionInfoKeys Name="FileVersion">0.21.0.0</VersionInfoKeys>
 						<VersionInfoKeys Name="InternalName">DelphiServer</VersionInfoKeys>
 						<VersionInfoKeys Name="LegalCopyright">Copyright © 2012 The Apache Software Foundation</VersionInfoKeys>
 						<VersionInfoKeys Name="LegalTrademarks"/>
 						<VersionInfoKeys Name="OriginalFilename">DelphiServer.exe</VersionInfoKeys>
 						<VersionInfoKeys Name="ProductName">Thrift</VersionInfoKeys>
-						<VersionInfoKeys Name="ProductVersion">0.20.0.0</VersionInfoKeys>
+						<VersionInfoKeys Name="ProductVersion">0.21.0.0</VersionInfoKeys>
 						<VersionInfoKeys Name="Comments"/>
 					</VersionInfoKeys>
 					<Source>
diff --git a/tutorial/netstd/Client/Client.csproj b/tutorial/netstd/Client/Client.csproj
index 994d6be..399b0e0 100644
--- a/tutorial/netstd/Client/Client.csproj
+++ b/tutorial/netstd/Client/Client.csproj
@@ -24,7 +24,7 @@
     <AssemblyName>Client</AssemblyName>
     <PackageId>Client</PackageId>
     <OutputType>Exe</OutputType>
-    <Version>0.20.0.0</Version>
+    <Version>0.21.0.0</Version>
     <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
     <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
     <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
diff --git a/tutorial/netstd/Interfaces/Interfaces.csproj b/tutorial/netstd/Interfaces/Interfaces.csproj
index c352457..576722b 100644
--- a/tutorial/netstd/Interfaces/Interfaces.csproj
+++ b/tutorial/netstd/Interfaces/Interfaces.csproj
@@ -22,7 +22,7 @@
     <TargetFramework>net6.0</TargetFramework>
     <AssemblyName>Interfaces</AssemblyName>
     <PackageId>Interfaces</PackageId>
-    <Version>0.20.0.0</Version>
+    <Version>0.21.0.0</Version>
     <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
     <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
     <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
diff --git a/tutorial/netstd/Server/Server.csproj b/tutorial/netstd/Server/Server.csproj
index c72ec88..38131a6 100644
--- a/tutorial/netstd/Server/Server.csproj
+++ b/tutorial/netstd/Server/Server.csproj
@@ -24,7 +24,7 @@
     <AssemblyName>Server</AssemblyName>
     <PackageId>Server</PackageId>
     <OutputType>Exe</OutputType>
-    <Version>0.20.0.0</Version>
+    <Version>0.21.0.0</Version>
     <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
     <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
     <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
diff --git a/tutorial/ocaml/_oasis b/tutorial/ocaml/_oasis
index 6072e4b..180f865 100644
--- a/tutorial/ocaml/_oasis
+++ b/tutorial/ocaml/_oasis
@@ -1,5 +1,5 @@
 Name: tutorial
-Version: 0.20.0
+Version: 0.21.0
 OASISFormat: 0.3
 Synopsis: OCaml Tutorial example
 Authors: Apache Thrift Developers <dev@thrift.apache.org>