astax astax - 1 year ago 51
PHP Question

Android push (GCM) from App Engine using Zend Framework

I'm trying to implement GCM server using PHP and Zend Framework on Google App Engine. So far it works fine locally, but fails with this message when uploaded to App Engine:

Here is the code:

$ids = '....my device id....';
$apiKey = '...my api key...';

$data = array( 'message' => 'Hello World!' );
$gcm_client = new Client();
$gcm_client->setApiKey($apiKey);
$gcm_message = new Message();
$gcm_message->setTimeToLive(86400);
$gcm_message->setData($data);
$gcm_message->setRegistrationIds($ids);

$response = $gcm_client->send($gcm_message);
var_dump($response);


And it fails with this error message:


PHP Fatal error: Uncaught exception 'ErrorException' with message
'stream_socket_client(): unable to connect to
android.googleapis.com:443 (Unknown error 4294967295)' in
/base/data/home/..../backend:v1.375711862873219029/vendor/zendframework/zend-http/Zend/Http/Client/Adapter/Socket.php:253


I know App Engine doesn't allow socket connections and offers urlFetch wrapper for http and https, but how do I tell Zend Framework to use this transport?

Answer Source

Promoted this from a comment - I ended up making my own class implementing Zend\Http\Client\Adapter\AdapterInterface that uses URLFetch by opening a URL using the usual fopen with stream context to send POST request. Although this works I'm not sure it's the best way. Would prefer to use the framework capabilities, if possible.

I'm not sure if this is going to help anyone, as both ZendFramework and AppEngine have evolved since I asked the question, but here is the adapter I've implemented:

use Zend\Http\Client\Adapter\AdapterInterface;
use Zend\Http\Client\Adapter\Exception\RuntimeException;
use Zend\Http\Client\Adapter\Exception\TimeoutException;
use Zend\Stdlib\ErrorHandler;

class URLFetchHttpAdapter implements AdapterInterface
{
    protected $stream;
    protected $options;

    /**
     * Set the configuration array for the adapter
     *
     * @param array $options
     */
    public function setOptions($options = array())
    {
        $this->options = $options;
    }

    /**
     * Connect to the remote server
     *
     * @param string $host
     * @param int $port
     * @param  bool $secure
     */
    public function connect($host, $port = 80, $secure = false)
    {
        // no connection yet - it's performed in "write" method
    }

    /**
     * Send request to the remote server
     *
     * @param string $method
     * @param \Zend\Uri\Uri $url
     * @param string $httpVer
     * @param array $headers
     * @param string $body
     *
     * @throws \Zend\Loader\Exception\RuntimeException
     * @return string Request as text
     */
    public function write($method, $url, $httpVer = '1.1', $headers = array(), $body = '')
    {
        $headers_str = '';
        foreach ($headers as $k => $v) {
            if (is_string($k))
                $v = ucfirst($k) . ": $v";
            $headers_str .= "$v\r\n";
        }

        if (!is_array($this->options))
            $this->options = array();

        $context_arr = array("http" =>
                                 array( "method" => $method,
                                        "content" => $body,
                                        "header" => $headers_str,
                                        "protocol_version" => $httpVer,
                                        'ignore_errors' => true,
                                        'follow_location' => false,
                                 ) + $this->options
        );
        $context = stream_context_create($context_arr);

        ErrorHandler::start();
        $this->stream = fopen((string)$url, 'r', null, $context);
        $error = ErrorHandler::stop();
        if (!$this->stream) {
            throw new \Zend\Loader\Exception\RuntimeException('', 0, $error);
        }
    }


    /**
     * Read response from server
     *
     * @throws \Zend\Http\Client\Adapter\Exception\RuntimeException
     * @return string
     */
    public function read()
    {
        if ($this->stream) {
            ErrorHandler::start();
            $metadata = stream_get_meta_data($this->stream);
            $headers = join("\r\n", $metadata['wrapper_data']);

            $contents = stream_get_contents($this->stream);
            $error = ErrorHandler::stop();
            if ($error)
                throw $error;

            $this->close();

            //echo $headers."\r\n\r\n".$contents;
            return $headers."\r\n\r\n".$contents;
        } else {
            throw new RuntimeException("No connection exists");
        }
    }

    /**
     * Close the connection to the server
     *
     */
    public function close()
    {
        if (is_resource($this->stream)) {
            ErrorHandler::start();
            fclose($this->stream);
            ErrorHandler::stop();
            $this->stream = null;
        }
    }

    /**
     * Check if the socket has timed out - if so close connection and throw
     * an exception
     *
     * @throws TimeoutException with READ_TIMEOUT code
     */
    protected function _checkSocketReadTimeout()
    {
        if ($this->stream) {
            $info = stream_get_meta_data($this->stream);
            $timedout = $info['timed_out'];
            if ($timedout) {
                $this->close();
                throw new TimeoutException(
                    "Read timed out after {$this->options['timeout']} seconds",
                    TimeoutException::READ_TIMEOUT
                );
            }
        }
    }

}