RemotePlusClient::formatDateForRemotePlus()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 8
rs 10
cc 2
nc 2
nop 1
1
<?php
2
3
namespace DPRMC\IceRemotePlusClient;
4
5
use DPRMC\CUSIP;
6
use DPRMC\IceRemotePlusClient\Exceptions\DateSentToConstructorIsNotParsable;
7
use DPRMC\IceRemotePlusClient\Exceptions\RemotePlusError;
8
use GuzzleHttp\Client;
9
use GuzzleHttp\Psr7\Request;
10
use GuzzleHttp\Psr7\Response;
11
12
/**
13
 * This is the parent class that all API calls must extend.
14
 * Class RemotePlusClient
15
 * @package DPRMC\InteractiveData
16
 */
17
class RemotePlusClient {
18
19
    /**
20
     * The base URI for the Remote Plus system.
21
     */
22
    const BASE_URI = 'http://rplus.interactivedata.com';
23
24
    /**
25
     * The page (resource) to POST your Remote Plus query.
26
     */
27
    const PAGE = '/cgi/nph-rplus';
28
29
    /**
30
     * @var string Your username supplied by Interactive Data.
31
     */
32
    protected $user = '';
33
34
    /**
35
     * @var string The password assigned to your username from Interactive Data.
36
     */
37
    protected $pass = '';
38
39
    /**
40
     * @var \GuzzleHttp\Client The GuzzleHttp client used to POST to the Remote Plus API.
41
     */
42
    protected $client;
43
44
    /**
45
     * @var Request; The request to the Remote Plus API
46
     */
47
    protected $request;
48
49
    /**
50
     * @var Response The response from the Remote Plus API
51
     */
52
    protected $response;
53
54
    /**
55
     * @var string The value required by Remote Plus for authentication.
56
     */
57
    protected $authorizationHeaderValue = '';
58
59
    /**
60
     * @var bool A parameter we pass in the request to Remote Plus to enable debugging information to be returned.
61
     */
62
    protected $remotePlusDebug = FALSE;
63
64
    /**
65
     * @var float The HTTP version that Remote Plus expects for requests.
66
     */
67
    protected $remotePlusHttpVersion = 1.0;
68
69
    /**
70
     * @var string The Content-Type header value that Remote Plus is expecting.
71
     */
72
    protected $remotePlusContentType = 'application/x-www-form-urlencoded';
73
74
75
    /**
76
     * @var string The formatted body of the request being sent to the Remote Plus API.
77
     */
78
    protected $requestBody = '';
79
80
    /**
81
     * @var array An array of security identifiers that you want to pull data on.
82
     */
83
    protected $identifiers = [];
84
85
    /**
86
     * @var array An array of item codes that represent data points that you want to pull for each security.
87
     */
88
    protected $items = [];
89
90
    /**
91
     * @var string The date you want to get item values on these securities.
92
     */
93
    protected $date;
94
95
    /**
96
     * RemotePlusClient constructor.
97
     *
98
     * @param $user string The username given to you by Interactive Data
99
     * @param $pass string The password for the above username.
100
     */
101
    public function __construct( $user, $pass ) {
102
        $this->user                     = $user;
103
        $this->pass                     = $pass;
104
        $this->client                   = new Client( [ 'base_uri' => self::BASE_URI ] );
105
        $this->authorizationHeaderValue = $this->getAuthenticationHeaderValue( $this->user, $this->pass );
106
    }
107
108
    public static function instantiate( string $user, string $pass ) {
109
        return new static( $user, $pass );
110
    }
111
112
    /**
113
     * Adds an array of security identifiers to the list you want to retrieve.
114
     * @param array $identifiers An array of security identifiers that you want to retrieve data on.
115
     * @return $this
116
     */
117
    public function addIdentifiers( array $identifiers ) {
118
        foreach ( $identifiers as $identifier ):
119
            $this->addIdentifier( $identifier );
120
        endforeach;
121
        return $this;
122
    }
123
124
    /**
125
     * Adds an individual security identifier to the list you want to retrieve.
126
     * @param string $identifier
127
     * @return $this
128
     */
129
    public function addIdentifier( string $identifier ) {
130
        if ( FALSE == $this->identifierExists( $identifier ) ):
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
131
            $this->identifiers[] = trim( $identifier );
132
        endif;
133
        return $this;
134
    }
135
136
    /**
137
     * Logic function to improve readability.
138
     * @param string $identifier
139
     * @return bool
140
     */
141
    protected function identifierExists( string $identifier ): bool {
142
        if ( FALSE === array_search( $identifier, $this->identifiers ) ):
143
            return FALSE;
144
        endif;
145
        return TRUE;
146
    }
147
148
    /**
149
     * A wrapper around the addIdentifiers() fluent method when the identifiers are known to a list of CUSIPs.
150
     * @param array $cusips An array of CUSIP security identifiers.
151
     * @return $this
152
     */
153
    public function addCusips( array $cusips ) {
154
        foreach ( $cusips as $cusip ):
155
            $this->addCusip( $cusip );
156
        endforeach;
157
        return $this;
158
    }
159
160
161
    /**
162
     * Adds an individual CUSIP to the list of security identifiers you want to retrieve.
163
     * @param string $cusip A CUSIP security identifier.
164
     * @return $this
165
     */
166
    public function addCusip( string $cusip ) {
167
        if ( CUSIP::isCUSIP( $cusip ) ):
168
            $this->addIdentifier( $cusip );
169
        endif;
170
        return $this;
171
    }
172
173
174
    /**
175
     * Add an item code to the list of data points we want to retrieve with this request.
176
     * @param string $item
177
     * @return $this
178
     */
179
    public function addItem( string $item ) {
180
        if ( FALSE === $this->itemExists( $item ) ):
181
            $this->items[] = $item;
182
        endif;
183
        return $this;
184
    }
185
186
    /**
187
     * A convenience method to add a list of item codes to this request.
188
     * @param array $items
189
     * @return $this
190
     */
191
    public function addItems( array $items ) {
192
        foreach ( $items as $item ):
193
            $this->addItem( $item );
194
        endforeach;
195
        return $this;
196
    }
197
198
    /**
199
     * A logic function to improve readability.
200
     * @param string $item
201
     * @return bool
202
     */
203
    protected function itemExists( string $item ): bool {
204
        if ( FALSE === array_search( $item, $this->items ) ):
205
            return FALSE;
206
        endif;
207
        return TRUE;
208
    }
209
210
    /**
211
     * @param string $date A string date that can be parsed by PHP's strtotime() function.
212
     * @return $this
213
     * @throws DateSentToConstructorIsNotParsable
214
     */
215
    public function addDate( string $date ) {
216
        $this->date = $this->formatDateForRemotePlus( $date );
217
        return $this;
218
    }
219
220
    /**
221
     * @param bool $debug
222
     * @return $this
223
     */
224
    public function setDebug( bool $debug ) {
225
        $this->remotePlusDebug = $debug;
226
        return $this;
227
    }
228
229
230
    /**
231
     * Returns the value required by Remote Plus for the Authorization header.
232
     *
233
     * @param string $username The username set by Interactive Data
234
     * @param string $pass The password assigned by Interactive Data
235
     *
236
     * @return string The value needed for the Authorization header.
237
     */
238
    protected function getAuthenticationHeaderValue( $username, $pass ) {
239
        return "Basic " . $this->encodeUserAndPassForBasicAuthentication( $username, $pass );
240
    }
241
242
    /**
243
     * Encodes the user and pass as required by the Basic Authorization.
244
     * @see https://en.wikipedia.org/wiki/Basic_access_authentication
245
     *
246
     * @param string $username The username set by Interactive Data
247
     * @param string $pass The password assigned by Interactive Data
248
     *
249
     * @return string The base64 encoded user:pass string.
250
     */
251
    protected function encodeUserAndPassForBasicAuthentication( $username, $pass ) {
252
        return base64_encode( $username . ':' . $pass );
253
    }
254
255
    /**
256
     * @return RemotePlusResponse
257
     * @throws RemotePlusError
258
     * @throws \GuzzleHttp\Exception\GuzzleException
259
     */
260
    public function run(): RemotePlusResponse {
261
        $this->generateBodyForRequest();
262
        $this->sendRequest();
263
264
        return $this->processResponse();
265
    }
266
267
    /**
268
     * Sends the request to Remote Plus, and saves the Response object into our local $response property.
269
     * @throws \GuzzleHttp\Exception\GuzzleException
270
     */
271
    protected function sendRequest() {
272
        $this->response = $this->client->request( 'POST', self::PAGE, [
273
            'debug'   => $this->remotePlusDebug,
274
            'version' => $this->remotePlusHttpVersion,
275
            'headers' => [ 'Content-Type'  => $this->remotePlusContentType,
276
                           'Authorization' => $this->getAuthenticationHeaderValue( $this->user, $this->pass ), ],
277
            'body'    => $this->requestBody,
278
        ] );
279
    }
280
281
282
    /**
283
     * The RemotePlus system requires dates to be formatted as yyyymmdd
284
     * @param string $date Any string that can be parsed by PHP's strtotime()
285
     * @return string The $date parameter formatted as yyyymmdd (or in PHP's syntax: Ymd)
286
     * @throws \DPRMC\IceRemotePlusClient\Exceptions\DateSentToConstructorIsNotParsable
287
     */
288
    protected function formatDateForRemotePlus( string $date ) {
289
        $strTime = strtotime( $date );
290
        if ( $strTime === FALSE ):
291
            throw new DateSentToConstructorIsNotParsable( "We could not parse the date you sent to the constructor: [" . $date . "]" );
292
        endif;
293
        $date = date( 'Ymd', $strTime );
294
295
        return (string)$date;
296
    }
297
298
299
    /**
300
     * Extracted this into it's own function so I can stub and test without
301
     * having to make a request to the IDC server.
302
     * @return string
303
     */
304
    protected function getBodyFromResponse(): string {
305
        return (string)$this->response->getBody();
306
    }
307
308
309
    /**
310
     * @return RemotePlusResponse
311
     * @throws RemotePlusError
312
     */
313
    protected function processResponse(): RemotePlusResponse {
314
        $body = $this->getBodyFromResponse();
315
316
        $this->checkForError( $body );
317
318
        $itemValues = explode( "\n", $body );
319
        $itemValues = array_map( 'trim', $itemValues );
320
        $itemValues = array_filter( $itemValues );
321
        array_pop( $itemValues ); // Remove the CRC check.
322
323
        $remotePlusResponse = new RemotePlusResponse();
324
325
        foreach ( $this->identifiers as $i => $identifier ):
326
            $securityResponse     = SecurityResponse::instantiate()
327
                                                    ->addIdentifier( $identifier );
328
329
            if ( isset( $this->date ) ):
330
                $securityResponse->addDate( $this->date );
331
            endif;
332
333
            $individualItemValues = explode( ',', $itemValues[ $i ] );
334
335
            foreach ( $individualItemValues as $j => $item ):
336
                $securityResponse->addItem(
337
                    $this->items[ $j ],
338
                    $item
339
                );
340
            endforeach;
341
342
            $remotePlusResponse->addResponse( $securityResponse );
343
        endforeach;
344
345
        return $remotePlusResponse;
346
    }
347
348
    /**
349
     * Throws an error if the response from RemotePlus indicated an error occurred.
350
     * If you contact Interactive Data for help with a system message, please note the error number and the
351
     * exact wording of the message.
352
     * @param string $body
353
     * @throws RemotePlusError
354
     */
355
    protected function checkForError( string $body ) {
356
357
        // All RemotePlus errors start with "!E
358
        // So if the body does not start with those 3 characters, then no error occurred, and we can continue.
359
        if ( '"!E' != substr( $body, 0, 3 ) ):
360
            return;
361
        endif;
362
363
        $itemValues = explode( "\n", $body );
364
        $itemValues = array_map( 'trim', $itemValues );
365
        $itemValues = array_filter( $itemValues );
366
        array_pop( $itemValues ); // Remove the CRC check.
367
368
        $errorLine  = $itemValues[ 0 ];
369
        $errorParts = explode( '","', $errorLine );
370
        $errorParts = array_map( function ( $item ) {
371
            return trim( $item, '"' );
372
        }, $errorParts );
373
374
        throw new RemotePlusError( $errorParts[ 1 ], 0, NULL, $errorParts[ 0 ] );
375
    }
376
377
    /**
378
     * Sets the $this->requestBody property. Every type of request sent to
379
     * Remote Plus has a different syntax. It makes sense to force the child
380
     * classes to implement that code.
381
     */
382
    /**
383
     * The Remote Plus API requires the request body to be formatted in a very specific way.
384
     * The following body is formatted to pull the prices for a list of CUSIPs from a specific date.
385
     */
386
    protected function generateBodyForRequest() {
387
388
        $identifiers = implode( ',', $this->identifiers );
389
        $items       = implode( ',', $this->items );
390
391
        $this->requestBody = 'Request=' . urlencode( "GET,(" . $identifiers ) . "),(" . $items . ")," . $this->date . "&Done=flag\n";
392
    }
393
}