twilio / twilio-php

A PHP library for communicating with the Twilio REST API and generating TwiML.
MIT License
1.57k stars 562 forks source link

Twilio\Rest\Api\V2010\Account\MessageInstance::__construct() must be of the type array, null given #690

Closed trevrobwhite closed 3 years ago

trevrobwhite commented 3 years ago

Issue Summary

When calling

$message = $client->account->messages->create($recipient, [ 'from' => $from, 'body' => $body, ]);

I get the error Twilio\Rest\Api\V2010\Account\MessageInstance::__construct() must be of the type array, null given

I'm using a proxy server so had to extend the class:

/**
 * Class GeTwilioClient allows talking to the Twilio Endpoint through the proxy server
 * @link https://www.twilio.com/docs/libraries/php/custom-http-clients-php
 */

class GeTwilioClient extends \Twilio\Http\CurlClient
{
    protected $http = null;
    protected $proxy = null;

    /**
     * GeTwilioClient constructor.
     * @link  https://www.twilio.com/docs/libraries/php/custom-http-clients-php
     * @param null $proxy Proxy Server
     * @param null $cainfo CA Info for the proxy
     */
    public function __construct($proxy = null, $cainfo = null)
    {
        $this->proxy = $proxy;
        $this->cainfo = $cainfo;
        $this->http = new CurlClient();
    }

    public function request($method, $url, $params = array(), $data = array(),
                            $headers = array(), $user = null, $password = null,
                            $timeout = null): Response
    {
        $options = $this->options($method, $url, $params, $data, $headers,
            $user, $password, $timeout);
        {
            // Here you can change the URL, headers and other request parameters
            $options = $this->options($method, $url, $params, $data, $headers,
                $user, $password, $timeout);

            $curl = curl_init($url);
            curl_setopt_array($curl, $options);
            if (!empty($this->proxy))
                curl_setopt($curl, CURLOPT_PROXY, $this->proxy);

            if (!empty($this->cainfo))
                curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_HTTPPROXYTUNNEL, true);
            $response = curl_exec($curl);

            $parts = explode("\r\n\r\n", $response, 3);
            list($head, $body) = ($parts[0] == 'HTTP/1.1 100 Continue'
                || $parts[0] == 'HTTP/1.1 200 Connection established')
                ? array($parts[1], $parts[2])
                : array($parts[0], $parts[1]);

            $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

            $responseHeaders = array();
            $headerLines = explode("\r\n", $head);
            array_shift($headerLines);
            foreach ($headerLines as $line) {
                list($key, $value) = explode(':', $line, 2);
                $responseHeaders[$key] = $value;
            }

            curl_close($curl);

            if (isset($buffer) && is_resource($buffer)) {
                fclose($buffer);
            }
            return new Response($statusCode, $body, $responseHeaders);
        }
    }
}

Using this function I call the Twilio Client:

   function sendSMSTwilio($recipient, $body, $from = null, &$errordetail = null)
    {
        if (empty($from)) $from = $this->smsSettings['TWILIO']['from']; // default the from number

        // Needed to get through our proxy
        try {
            $httpClient = new GeTwilioClient($this->smsSettings['TWILIO']['PROXY'] ?? null, $this->smsSettings['TWILIO']['CAINFO'] ?? null);

            $client = new Client($this->smsSettings['TWILIO']['account_sid'], $this->smsSettings['TWILIO']['auth_token'], null, null, $httpClient);

            $message = $client->account->messages->create($recipient,
                [
                    'from' => $from,
                    'body' => $body,
                ]);

        } catch (\Exception $ex) {
            // Twilio threw me an exception, tell him to play nice!
            $errordetail = "Caught Exception from Twilio " . $ex->getMessage() . PHP_EOL;
            return false;
        }

        // All is good, return SID
        return (string)$message->sid;

    }

Debugging the code, I can see MessageList.php (line 68) calls new MessageInstance, there the payload is null, the call before is calling $this->version->create.

image

Digging into this I can see that Version.php line 222 checks the status code, for me this returns 201 (and an SMS is sent to my phone), however $response->getContent() returns NULL which is what is being passed to MessageInstance without checking and causes the exception.

image

Exception/Log

TypeError::__set_state(array(
   'message' => 'Argument 2 passed to Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance::__construct() must be of the type array, null given, called in /opt/openitc/frontend/plugins/GeneralElectricModule/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php on line 68',
   'string' => '',
   'code' => 0,
   'file' => '/opt/openitc/frontend/plugins/GeneralElectricModule/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageInstance.php',
   'line' => 55,
   'trace' => 
  array (
    0 => 
    array (
      'file' => '/opt/openitc/frontend/plugins/GeneralElectricModule/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php',
      'line' => 68,
      'function' => '__construct',
      'class' => 'Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance',
      'type' => '->',
    ),
    1 => 
    array (
      'file' => '/opt/openitc/frontend/plugins/GeneralElectricModule/src/Core/GeSMS.php',
      'line' => 130,
      'function' => 'create',
      'class' => 'Twilio\\Rest\\Api\\V2010\\Account\\MessageList',
      'type' => '->',
    ),
    2 => 
    array (
      'file' => '/opt/openitc/frontend/plugins/GeneralElectricModule/src/Core/GeSMS.php',
      'line' => 80,
      'function' => 'sendSMSTwilio',
      'class' => 'GeneralElectricModule\\Core\\GeSMS',
      'type' => '->',
    ),
),
  'previous' => NULL,
   'xdebug_message' => '
TypeError: Argument 2 passed to Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance::__construct() must be of the type array, null given, called in /opt/openitc/frontend/plugins/GeneralElectricModule/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php on line 68 in /opt/openitc/frontend/plugins/GeneralElectricModule/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageInstance.php on line 55

Call Stack:
    0.0648     397544   1. {main}() /opt/openitc/frontend/bin/cake.php:0
    0.0888    2432360   2. Cake\\Console\\CommandRunner->run(???, ???) /opt/openitc/frontend/bin/cake.php:12
    0.2213   11075816   3. Cake\\Console\\CommandRunner->runCommand(???, ???, ???) /opt/openitc/frontend/vendor/cakephp/cakephp/src/Console/CommandRunner.php:172
    0.2213   11075816   4. GeneralElectricModule\\Command\\GeNotificationCommand->run(???, ???) /opt/openitc/frontend/vendor/cakephp/cakephp/src/Console/CommandRunner.php:336
    0.2339   15342528   5. GeneralElectricModule\\Command\\GeNotificationCommand->execute(???, ???) /opt/openitc/frontend/vendor/cakephp/cakephp/src/Console/BaseCommand.php:179
    6.0319   31652680   6. GeneralElectricModule\\Command\\GeNotificationCommand->smsNotification(???, ???, ???, ???) /opt/openitc/frontend/plugins/GeneralElectricModule/src/Command/GeNotificationCommand.php:273
    8.3746   31855928   7. GeneralElectricModule\\Core\\GeSMS->sendSMSfromScheduler(???, ???) /opt/openitc/frontend/plugins/GeneralElectricModule/src/Command/GeNotificationCommand.php:723
    8.9015   31857088   8. GeneralElectricModule\\Core\\GeSMS->sendSMSTwilio(???, ???, ???, ???) /opt/openitc/frontend/plugins/GeneralElectricModule/src/Core/GeSMS.php:80
    9.3892   32296856   9. Twilio\\Rest\\Api\\V2010\\Account\\MessageList->create(???, ???) /opt/openitc/frontend/plugins/GeneralElectricModule/src/Core/GeSMS.php:130
  728.8312   32353168  10. Twilio\\Rest\\Api\\V2010\\Account\\MessageInstance->__construct(???, ???, ???, ???) /opt/openitc/frontend/plugins/GeneralElectricModule/vendor/twilio/sdk/src/Twilio/Rest/Api/V2010/Account/MessageList.php:68
',
))

Technical details:

eshanholtz commented 3 years ago

The most likely reason this isn't working as expected is because your custom client isn't properly constructing the Response it returns from the call to request(). $response->getContent() calls json_decode on the $content member, which you're setting from the exploded response content parts. Have you verified that the element in the array being set to the body is indeed a JSON string?

trevrobwhite commented 3 years ago

@eshanholtz thanks for the hint your right, the issue was with the proxy script, I got the example from https://www.twilio.com/docs/libraries/php/custom-http-clients-php# under "Custom HTTP Client for PHP" but this code isn't written very well, using some help from https://www.php.net/manual/en/function.curl-exec.php#80442 I was able to rewrite my request function, and I share it below to help others (and maybe the Twilio documentation should be updated)


/**
 * Class GeTwilioClient allows talking to the Twilio Endpoint through the proxy server
 * @link https://www.twilio.com/docs/libraries/php/custom-http-clients-php
 */

class GeTwilioClient extends \Twilio\Http\CurlClient
{
    protected $http = null;
    protected $proxy = null;

    /**
     * GeTwilioClient constructor.
     * @link  https://www.twilio.com/docs/libraries/php/custom-http-clients-php
     * @param null $proxy Proxy Server
     * @param null $cainfo CA Info for the proxy
     */
    public function __construct($proxy = null, $cainfo = null)
    {
        $this->proxy = $proxy;
        $this->cainfo = $cainfo;
        $this->http = new CurlClient();
    }

    public function request($method, $url, $params = array(), $data = array(),
                            $headers = array(), $user = null, $password = null,
                            $timeout = null): Response
    {
        $options = $this->options($method, $url, $params, $data, $headers,
            $user, $password, $timeout);
        {
            // Here you can change the URL, headers and other request parameters
            $options = $this->options($method, $url, $params, $data, $headers,
                $user, $password, $timeout);

            $curl = curl_init($url);
            curl_setopt_array($curl, $options);
            if (!empty($this->proxy))
                curl_setopt($curl, CURLOPT_PROXY, $this->proxy);

            if (!empty($this->cainfo))
                curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_HTTPPROXYTUNNEL, true);
            $response = curl_exec($curl);

            // Updated using example: https://www.php.net/manual/en/function.curl-exec.php#80442
            $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
            $headerSize = curl_getinfo($curl,CURLINFO_HEADER_SIZE);
            $head = substr($response, 0, $headerSize);
            $body = substr( $response, $headerSize );

            $responseHeaders = array();
            $headerLines = preg_split("/\r?\n/", $head);
            foreach ($headerLines as $line) {
                if (! preg_match("/:/", $line)) continue;
                list($key, $value) = explode(':', $line, 2);
                $responseHeaders[trim($key)] = trim($value);
            }

            curl_close($curl);

            if (isset($buffer) && is_resource($buffer)) {
                fclose($buffer);
            }
            return new Response($statusCode, $body, $responseHeaders);
        }
    }
}
eshanholtz commented 3 years ago

@trevrobwhite

I pass along your feedback to our docs team. They're going to work on getting that example updated, thank you for including your implementation. I'm glad the hint worked for you.

I'm going to go ahead and close this issue as there's no further work that needs to be done in this repo.