Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
43.66% covered (danger)
43.66%
31 / 71
18.18% covered (danger)
18.18%
2 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
AblyRest
43.66% covered (danger)
43.66%
31 / 71
18.18% covered (danger)
18.18%
2 / 11
296.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
86.67% covered (warning)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
4.04
 channel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 stats
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 time
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 systemTime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 post
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 requestInternal
44.83% covered (danger)
44.83%
13 / 29
0.00% covered (danger)
0.00%
0 / 1
46.92
 request
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 requestWithFallback
25.00% covered (danger)
25.00%
3 / 12
0.00% covered (danger)
0.00%
0 / 1
27.67
 setLibraryFlavourString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2namespace Ably;
3
4use Ably\Models\ClientOptions;
5use Ably\Models\PaginatedResult;
6use Ably\Models\HttpPaginatedResponse;
7use Ably\Exceptions\AblyException;
8use Ably\Exceptions\AblyRequestException;
9
10/**
11 * Ably REST client
12 */
13class AblyRest {
14
15    const API_VERSION = '1.0';
16    const LIB_VERSION = '1.0.1';
17
18    protected $options;
19    protected static $libFlavour = '';
20
21    /**
22     * @var \Ably\Http $http object for making HTTP requests
23     */
24    public $http;
25    /**
26     * @var \Ably\Auth $auth object providing authorisation functionality
27     */
28    public $auth;
29    /**
30     * @var \Ably\Channels $channels object for creating and releasing channels
31     */
32    public $channels;
33
34    /**
35     * Constructor
36     * @param \Ably\Models\ClientOptions|string array with options or a string with app key or token
37     */
38    public function __construct( $options = [] ) {
39
40        # convert to options if a single key is provided
41        if ( is_string( $options ) ) {
42            if ( strpos( $options, ':' ) === false ) {
43                $options = [ 'token' => $options ];
44            } else {
45                $options = [ 'key' => $options ];
46            }
47        }
48
49        $this->options = new ClientOptions( $options );
50
51        Log::setLogLevel( $this->options->logLevel );
52        if ( !empty( $this->options->logHandler ) ) {
53            Log::setLogCallback( $this->options->logHandler );
54        } else {
55            Log::setLogCallback( null );
56        }
57
58        $httpClass = $this->options->httpClass;
59        $this->http = new $httpClass( $this->options );
60        $authClass = $this->options->authClass;
61        $this->auth = new $authClass( $this, $this->options );
62        $this->channels = new Channels( $this );
63
64        return $this;
65    }
66
67    /**
68     * Shorthand to $this->channels->get()
69     * @return \Ably\Channel Channel
70     */
71    public function channel( $name, $options = [] ) {
72        return $this->channels->get( $name, $options );
73    }
74
75    /**
76     * Gets application-level usage statistics , covering messages sent
77     * and received, API requests and connections
78     * @return array Statistics
79     */
80    public function stats( $params = [] ) {
81        return new PaginatedResult( $this, 'Ably\Models\Stats', $cipher = false, 'GET', '/stats', $params );
82    }
83
84    /**
85     * Retrieves server time
86     * @return integer server time in milliseconds
87     */
88    public function time() {
89        $res = $this->get( '/time', $params = [], $headers = [], $returnHeaders = false, $authHeaders = false );
90        return $res[0];
91    }
92
93    /**
94     * Returns local time
95     * @return integer system time in milliseconds
96     */
97    public function systemTime() {
98        return round( microtime(true) * 1000 );
99    }
100
101    /**
102     * Does a GET request, automatically injecting auth headers and handling fallback on server failure
103     * @see AblyRest::request()
104     */
105    public function get( $path, $headers = [], $params = [], $returnHeaders = false, $auth = true ) {
106        return $this->requestInternal( 'GET', $path, $headers, $params, $returnHeaders, $auth );
107    }
108
109    /**
110     * Does a POST request, automatically injecting auth headers and handling fallback on server failure
111     * @see AblyRest::request()
112     */
113    public function post( $path, $headers = [], $params = [], $returnHeaders = false, $auth = true ) {
114        return $this->requestInternal( 'POST', $path, $headers, $params, $returnHeaders, $auth );
115    }
116
117    /**
118     * Does a HTTP request, automatically injecting auth headers and handling fallback on server failure.
119     * This method is used internally and `request` is the preferable method to use.
120     *
121     * @param string $method HTTP method (GET, POST, PUT, DELETE, ...)
122     * @param string $path root-relative path, e.g. /channels/example/messages
123     * @param array $headers HTTP headers to send
124     * @param array|string $params Array of parameters to submit or a JSON string
125     * @param boolean $returnHeaders if true, returns both headers and body as array, otherwise returns just body
126     * @param boolean $auth if authentication headers should be automatically injected
127     * @return mixed either array with 'headers' and 'body' fields or just body, depending on $returnHeaders, body is automatically decoded
128     * @throws AblyRequestException if the request fails
129     */
130    public function requestInternal( $method, $path, $headers = [], $params = [], $returnHeaders = false, $auth = true ) {
131        $mergedHeaders = array_merge( [
132            'Accept', 'application/json',
133            'X-Ably-Version' => self::API_VERSION,
134            'X-Ably-Lib' => 'php-' . self::$libFlavour . self::LIB_VERSION,
135        ], $headers );
136
137        if ( $auth ) { // inject auth headers
138            $mergedHeaders = array_merge( $this->auth->getAuthHeaders(), $mergedHeaders );
139        }
140
141        try {
142            if ( !empty( $this->options->fallbackHosts ) ) {
143                $res = $this->requestWithFallback( $method, $path, $mergedHeaders, $params );
144            } else {
145                $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->restHost;
146
147                if ( $this->options->tls && !empty( $this->options->tlsPort ) ) {
148                    $server .= ':' . $this->options->tlsPort;
149                }
150                if ( !$this->options->tls && !empty( $this->options->port ) ) {
151                    $server .= ':' . $this->options->port;
152                }
153
154                $res = $this->http->request( $method, $server . $path, $mergedHeaders, $params );
155            }
156        } catch (AblyRequestException $e) {
157            // check if the exception was caused by an expired token = authorised request + using token auth + specific error message
158            $res = $e->getResponse();
159
160            $causedByExpiredToken = $auth
161                && !$this->auth->isUsingBasicAuth()
162                && ($e->getCode() >= 40140)
163                && ($e->getCode() < 40150);
164
165            if ( $causedByExpiredToken ) { // renew the token
166                $this->auth->authorize();
167
168                // merge headers now and use auth = false to prevent potential endless recursion
169                $mergedHeaders = array_merge( $this->auth->getAuthHeaders(), $headers );
170
171                return $this->requestInternal( $method, $path, $mergedHeaders, $params, $returnHeaders, $auth = false );
172            } else {
173                throw $e;
174            }
175        }
176
177        if (!$returnHeaders) {
178            $res = $res['body'];
179        }
180        return $res;
181    }
182
183    /**
184     * Does an HTTP request with automatic pagination, automatically injected
185     * auth headers and automatic server failure handling using fallbackHosts.
186     *
187     * @param string $method HTTP method (GET, POST, PUT, DELETE, ...)
188     * @param string $path root-relative path, e.g. /channels/example/messages
189     * @param array $params GET parameters to append to $path
190     * @param array|object $body JSON-encodable structure to send in the body - leave empty for GET requests
191     * @param array $headers HTTP headers to send
192     * @return \Ably\Models\HttpPaginatedResponse
193     * @throws AblyRequestException This exception is only thrown for status codes >= 500
194     */
195    public function request( $method, $path, $params = [], $body = '', $headers = []) {
196        if ( count( $params ) ) {
197            $path .= '?' . http_build_query( $params );
198        }
199
200        if ( $method == 'GET' && $body ) {
201            throw new AblyException( 'GET requests cannot have a JSON body', 400, 40000 );
202        }
203
204        if ( !is_string( $body ) ) {
205            $body = json_encode( $body );
206        }
207
208        return new HttpPaginatedResponse( $this, 'Ably\Models\Untyped', null, $method, $path, $body, $headers );
209    }
210
211    /**
212     * Does a HTTP request backed up by fallback servers
213     */
214    protected function requestWithFallback( $method, $path, $headers = [], $params = [], $attempt = 0 ) {
215        try {
216            if ( $attempt == 0 ) { // using default host
217                $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->restHost;
218            } else { // using a fallback host
219                Log::d( 'Connection failed, attempting with fallback server #' . $attempt );
220                // attempt 1 uses fallback host with index 0
221                $server = ($this->options->tls ? 'https://' : 'http://') . $this->options->fallbackHosts[$attempt - 1];
222            }
223
224            return $this->http->request( $method, $server . $path, $headers, $params );
225        }
226        catch (AblyRequestException $e) {
227            if ( $e->getCode() >= 50000 ) {
228                if ( $attempt < min( $this->options->httpMaxRetryCount, count( $this->options->fallbackHosts ) ) ) {
229                    return $this->requestWithFallback( $method, $path, $headers, $params, $attempt + 1);
230                } else {
231                    Log::e( 'Failed to connect to server and all of the fallback servers.' );
232                    throw $e;
233                }
234            }
235
236            throw $e; // other error code than timeout, rethrow exception
237        }
238    }
239
240    /**
241     * Sets a "flavour string", that is sent in the `X-Ably-Lib` request header.
242     * Used for internal statistics.
243     * For instance setting 'laravel' results in: `X-Ably-Lib: php-laravel-1.0.0`
244     */
245    public static function setLibraryFlavourString( $flavour = '' ) {
246        self::$libFlavour = $flavour ? $flavour.'-' : '';
247    }
248}