Report   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 220
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 0
loc 220
rs 10
c 0
b 0
f 0
wmc 27
lcom 1
cbo 6

11 Methods

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