Completed
Push — master ( 32f37c...b12769 )
by Dan Michael O.
04:22
created

Report::isInitialized()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Scriptotek\Alma\Analytics;
4
5
use Danmichaelo\QuiteSimpleXMLElement\QuiteSimpleXMLElement;
6
use Scriptotek\Alma\Client;
7
use Scriptotek\Alma\Exception\RequestFailed;
8
use Scriptotek\Alma\Model\LazyResource;
9
use Scriptotek\Alma\Model\PaginatedList;
10
11
/**
12
 * A single Report resource.
13
 */
14
class Report extends LazyResource implements \Iterator, \Countable
15
{
16
    use PaginatedList;
17
18
    /** @var Client */
19
    protected $client;
20
21
    /** @var string */
22
    public $path;
23
24
    /** @var string */
25
    public $filter;
26
27
    /** @var array */
28
    protected $headers = [];
29
30
    /** @var string */
31
    protected $resumptionToken = null;
32
33
    /** @var boolean */
34
    protected $isFinished = false;
35
36
    /** @var integer */
37
    public $chunkSize = 1000;
38
39
    /** @var Row[] */
40
    protected $resources = [];
41
42
    public static $maxAttempts = 5;
43
44
    public static $retryDelayTime = 3;
45
46
    public function __construct(Client $client = null, $path = null, $headers = [], $filter = null)
47
    {
48
        parent::__construct($client);
0 ignored issues
show
Bug introduced by
It seems like $client defined by parameter $client on line 46 can be null; however, Scriptotek\Alma\Model\Model::__construct() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
49
50
        $this->path = $path;
51
        $this->headers = $headers;
52
        $this->filter = $filter;
53
    }
54
55
    /**
56
     * @deprecated
57
     * @return $this
58
     */
59
    public function getRows()
60
    {
61
        return $this;
62
    }
63
64
    public function getHeaders()
65
    {
66
        if (!count($this->headers)) {
67
            $this->fetchBatch();
68
        }
69
70
        return $this->headers;
71
    }
72
73
    /**
74
     * Generate the base URL for this resource.
75
     *
76
     * @return string
77
     */
78
    protected function urlBase()
79
    {
80
        return '/analytics/reports';
81
    }
82
83
    /**
84
     * Convert a retrieved resource to an object.
85
     *
86
     * @param $data
87
     * @return mixed
88
     */
89
    protected function convertToResource($data)
90
    {
91
        return new Row($data, $this->headers);
92
    }
93
94
    /**
95
     * Note: chunkSize must be between 25 and 1000.
96
     *
97
     * @param int $attempt
98
     * @param int $chunkSize
99
     * @return void
100
     */
101
    protected function fetchBatch($attempt = 1, $chunkSize = null)
102
    {
103
        if ($this->isFinished) {
104
            return;
105
        }
106
107
        $results = $this->client->getXML($this->url('', [
108
            'path'   => $this->resumptionToken ? null : $this->path,
109
            'limit'  => $chunkSize ?: $this->chunkSize,
110
            'token'  => $this->resumptionToken,
111
            'filter' => $this->filter ? str_replace(['\''], ['&apos;'], $this->filter) : null,
112
        ]));
113
114
        $results->registerXPathNamespaces([
115
            'rowset'  => 'urn:schemas-microsoft-com:xml-analysis:rowset',
116
            'xsd'     => 'http://www.w3.org/2001/XMLSchema',
117
            'saw-sql' => 'urn:saw-sql',
118
        ]);
119
120
        $this->readColumnHeaders($results);
121
122
        $rows = $results->all('//rowset:Row');
123
124
        foreach ($rows as $row) {
125
            $this->resources[] = $this->convertToResource($row);
126
        }
127
128
        $this->resumptionToken = $results->text('/report/QueryResult/ResumptionToken') ?: $this->resumptionToken;
129
        $this->isFinished = ($results->text('/report/QueryResult/IsFinished') === 'true');
130
131
132
        if (!count($rows) && !$this->isFinished) {
133
            // If the Analytics server spends too long time preparing the results, it can
134
            // sometimes return an empty result set. If this happens, we should just wait
135
            // a little and retry the request.
136
            // See: https://bitbucket.org/uwlib/uwlib-alma-analytic-tools/wiki/Understanding_Analytic_GET_Requests#!analytic-still-loading
137
            if ($attempt >= self::$maxAttempts) {
138
                // Give up
139
                throw new RequestFailed(
140
                    'Not getting any data from the Analytics server - max number of retries exhausted.'
141
                );
142
            }
143
            // Sleep for a few seconds, then retry
144
            sleep(self::$retryDelayTime);
145
            $this->fetchBatch($attempt + 1);
146
        }
147
    }
148
149
    protected function fetchData()
150
    {
151
        do {
152
            $this->fetchBatch();
153
        } while (!$this->isFinished);
154
        return null;
155
    }
156
157
    /**
158
     * Read column headers from response, and check that we got the right number of columns back.
159
     *
160
     * @param QuiteSimpleXMLElement $results
161
     */
162
    protected function readColumnHeaders(QuiteSimpleXMLElement $results)
163
    {
164
        $headers = array_map(function (QuiteSimpleXMLElement $node) {
165
            return $node->attr('saw-sql:columnHeading');
166
        }, $results->all('//xsd:complexType[@name="Row"]/xsd:sequence/xsd:element[position()>1]'));
167
168
        if (!count($headers)) {
169
            // No column headers included in this response. They're only
170
            // included in the first response, so that's probably fine.
171
            return;
172
        }
173
174
        if (!count($this->headers)) {
175
            $this->headers = $headers;
176
177
            return;
178
        }
179
180
        if (count($headers) != count($this->headers)) {
181
            throw new \RuntimeException(sprintf(
182
                'The number of returned columns (%d) does not match the number of assigned headers (%d).',
183
                count($headers),
184
                count($this->headers)
185
            ));
186
        }
187
    }
188
189
    /**
190
     * Check if we have the full representation of our data object. We cannot
191
     * really know from the data object alone, but when this method is called
192
     * we should have all the data.
193
     *
194
     * @param \stdClass $data
195
     * @return boolean
196
     */
197
    protected function isInitialized($data)
198
    {
199
        return true;
200
    }
201
202
    /**
203
     * Total number of resources. Note that we don't get this number from API upfront,
204
     * so we have to fetch all the rows to find out.
205
     *
206
     * @link http://php.net/manual/en/countable.count.php
207
     * @return int
208
     */
209
    public function count()
210
    {
211
        return count($this->init()->resources);
212
    }
213
214
    /**
215
     * Magic!
216
     * @param string $key
217
     * @return mixed
218
     */
219
    public function __get($key)
220
    {
221
        if ($key === 'headers') {
222
            return $this->getHeaders();
223
        }
224
        if ($key === 'rows') {
225
            return $this;
226
        }
227
    }
228
}
229