Completed
Push — master ( 2bc10c...73f449 )
by Kristof
06:26
created

EventExportService::exportEvents()   C

Complexity

Conditions 11
Paths 106

Size

Total Lines 107
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 107
rs 5.1683
cc 11
eloc 65
nc 106
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @file
4
 */
5
6
namespace CultuurNet\UDB3\EventExport;
7
8
use Broadway\UuidGenerator\UuidGeneratorInterface;
9
use CultuurNet\UDB3\EventExport\Notification\NotificationMailerInterface;
10
use CultuurNet\UDB3\EventNotFoundException;
11
use CultuurNet\UDB3\EventServiceInterface;
12
use CultuurNet\UDB3\Iri\IriGeneratorInterface;
13
use CultuurNet\UDB3\Search\SearchServiceInterface;
14
use Guzzle\Http\Exception\ClientErrorResponseException;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\NullLogger;
17
use ValueObjects\Web\EmailAddress;
18
19
class EventExportService implements EventExportServiceInterface
20
{
21
    /**
22
     * @var EventServiceInterface
23
     */
24
    protected $eventService;
25
26
    /**
27
     * @var SearchServiceInterface
28
     */
29
    protected $searchService;
30
    /**
31
     * @var UuidGeneratorInterface
32
     */
33
    protected $uuidGenerator;
34
35
    /**
36
     * Publicly accessible directory where exports will be stored.
37
     *
38
     * @var string
39
     */
40
    protected $publicDirectory;
41
42
    /**
43
     * @var NotificationMailerInterface
44
     */
45
    protected $mailer;
46
47
    /**
48
     * @var IriGeneratorInterface
49
     */
50
    protected $iriGenerator;
51
52
    /**
53
     * @param EventServiceInterface $eventService
54
     * @param SearchServiceInterface $searchService
55
     * @param UuidGeneratorInterface $uuidGenerator
56
     * @param string $publicDirectory
57
     * @param IriGeneratorInterface $iriGenerator
58
     * @param NotificationMailerInterface $mailer
59
     */
60
    public function __construct(
61
        EventServiceInterface $eventService,
62
        SearchServiceInterface $searchService,
63
        UuidGeneratorInterface $uuidGenerator,
64
        $publicDirectory,
65
        IriGeneratorInterface $iriGenerator,
66
        NotificationMailerInterface $mailer
67
    ) {
68
        $this->eventService = $eventService;
69
        $this->searchService = $searchService;
70
        $this->uuidGenerator = $uuidGenerator;
71
        $this->publicDirectory = $publicDirectory;
72
        $this->iriGenerator = $iriGenerator;
73
        $this->mailer = $mailer;
74
    }
75
76
    /**
77
     * @param FileFormatInterface $fileFormat
78
     *  The file format of the exported file.
79
     *
80
     * @param EventExportQuery $query
81
     *  The query that will be exported.
82
     *  A query has to be specified even if you are exporting a selection of events.
83
     *
84
     * @param EmailAddress|null $address
85
     *  An optional email address that will receive an email containing the exported file.
86
     *
87
     * @param LoggerInterface|null $logger
88
     *  An optional logger that reports unknown events and empty exports.
89
     *
90
     * @param string[]|null $selection
91
     *  A selection of items that will be included in the export.
92
     *  When left empty the whole query will export.
93
     *
94
     * @return bool|string
95
     *  The destination url of the export file or false if no events were found.
96
     */
97
    public function exportEvents(
98
        FileFormatInterface $fileFormat,
99
        EventExportQuery $query,
100
        EmailAddress $address = null,
101
        LoggerInterface $logger = null,
102
        $selection = null
103
    ) {
104
        if (!$logger instanceof LoggerInterface) {
105
            $logger = new NullLogger();
106
        }
107
108
        // do a pre query to test if the query is valid and check the item count
109
        try {
110
            $preQueryResult = $this->searchService->search(
111
                (string)$query,
112
                1,
113
                0
114
            );
115
            $totalItemCount = $preQueryResult->getTotalItems()->toNative();
116
        } catch (ClientErrorResponseException $e) {
117
            $logger->error(
118
                'not_exported',
119
                array(
120
                    'query' => (string)$query,
121
                    'error' => $e->getMessage(),
122
                    'exception_class' => get_class($e),
123
                )
124
            );
125
126
            throw ($e);
127
        }
128
129
        $logger->debug(
130
            'total items: {totalItems}',
131
            [
132
                'totalItems' => $totalItemCount,
133
                'query' => (string)$query,
134
            ]
135
        );
136
137
        if ($totalItemCount < 1) {
138
            $logger->error(
139
                'not_exported',
140
                array(
141
                    'query' => (string)$query,
142
                    'error' => "query did not return any results"
143
                )
144
            );
145
146
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface CultuurNet\UDB3\EventExp...Interface::exportEvents of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
147
        }
148
149
        try {
150
            $tmpDir = sys_get_temp_dir();
151
            $tmpFileName = $this->uuidGenerator->generate();
152
            $tmpPath = "{$tmpDir}/{$tmpFileName}";
153
154
            // $events are keyed here by the authoritative event ID.
155
            if (is_array($selection)) {
156
                $events = $this->getEventsAsJSONLD($selection, $logger);
0 ignored issues
show
Documentation introduced by
$selection is of type array, but the function expects a object<Traversable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
157
            } else {
158
                $events = $this->search(
159
                    $totalItemCount,
160
                    $query,
161
                    $logger
162
                );
163
            }
164
165
            $fileWriter = $fileFormat->getWriter();
166
            $fileWriter->write($tmpPath, $events);
167
168
            $finalPath = $this->getFinalFilePath($fileFormat, $tmpPath);
169
170
            $moved = copy($tmpPath, $finalPath);
171
            unlink($tmpPath);
172
173
            if (!$moved) {
174
                throw new \RuntimeException(
175
                    'Unable to move export file to public directory ' .
176
                    $this->publicDirectory
177
                );
178
            }
179
180
            $finalUrl = $this->iriGenerator->iri(
181
                basename($finalPath)
182
            );
183
184
            $logger->info(
185
                'job_info',
186
                [
187
                    'location' => $finalUrl,
188
                ]
189
            );
190
191
            if ($address) {
192
                $this->notifyByMail($address, $finalUrl);
193
            }
194
195
            return $finalUrl;
196
        } catch (\Exception $e) {
197
            if (isset($tmpPath) && $tmpPath && file_exists($tmpPath)) {
198
                unlink($tmpPath);
199
            }
200
201
            throw $e;
202
        }
203
    }
204
205
    /**
206
     * Get all events formatted as JSON-LD.
207
     *
208
     * @param \Traversable $events
209
     * @param LoggerInterface $logger
210
     * @return \Generator
211
     */
212
    private function getEventsAsJSONLD($events, LoggerInterface $logger)
213
    {
214
        foreach ($events as $eventId) {
215
            $event = $this->getEvent($eventId, $logger);
216
217
            if ($event) {
218
                yield $eventId => $event;
219
            }
220
        }
221
    }
222
223
    /**
224
     * @param string $id
225
     *   A string uniquely identifying an event.
226
     *
227
     * @param LoggerInterface $logger
228
     *
229
     * @return array|null
230
     *   An event array or null if the event was not found.
231
     */
232
    private function getEvent($id, LoggerInterface $logger)
233
    {
234
        try {
235
            $event = $this->eventService->getEvent($id);
236
        } catch (EventNotFoundException $e) {
237
            $logger->error(
238
                $e->getMessage(),
239
                [
240
                    'eventId' => $id,
241
                    'exception' => $e,
242
                ]
243
            );
244
245
            $event = null;
246
        }
247
248
        return $event;
249
    }
250
251
    /**
252
     * @param FileFormatInterface $fileFormat
253
     * @param string $tmpPath
254
     * @return string
255
     */
256
    private function getFinalFilePath(
257
        FileFormatInterface $fileFormat,
258
        $tmpPath
259
    ) {
260
        $fileUniqueId = basename($tmpPath);
261
        $extension = $fileFormat->getFileNameExtension();
262
        $finalFileName = $fileUniqueId . '.' . $extension;
263
        $finalPath = $this->publicDirectory . '/' . $finalFileName;
264
265
        return $finalPath;
266
    }
267
268
    /**
269
     * Generator that yields each unique search result.
270
     *
271
     * @param int $totalItemCount
272
     * @param string|object $query
273
     * @param LoggerInterface $logger
274
     *
275
     * @return \Generator
276
     */
277
    private function search($totalItemCount, $query, LoggerInterface $logger)
278
    {
279
        // change this pageSize value to increase or decrease the page size;
280
        $pageSize = 10;
281
        $pageCount = ceil($totalItemCount / $pageSize);
282
        $pageCounter = 0;
283
        $exportedEventIds = [];
284
285
        // Page querying the search service;
286
        while ($pageCounter < $pageCount) {
287
            $start = $pageCounter * $pageSize;
288
            // Sort ascending by creation date to make sure we get a quite consistent paging.
289
            $sort = 'creationdate asc';
290
            $results = $this->searchService->search(
291
                (string)$query,
292
                $pageSize,
293
                $start,
294
                $sort
295
            );
296
297
            // Iterate the results of the current page and get their IDs
298
            // by stripping them from the json-LD representation
299
            foreach ($results->getItems() as $event) {
300
                $expoId = explode('/', $event['@id']);
301
                $eventId = array_pop($expoId);
302
303
                if (!array_key_exists($eventId, $exportedEventIds)) {
304
                    $exportedEventIds[$eventId] = $pageCounter;
305
306
                    $event = $this->getEvent($eventId, $logger);
307
308
                    if ($event) {
309
                        yield $eventId => $event;
310
                    }
311
                } else {
312
                    $logger->error(
313
                        'query_duplicate_event',
314
                        array(
315
                            'query' => $query,
316
                            'error' => "found duplicate event {$eventId} on page {$pageCounter}, occurred first time on page {$exportedEventIds[$eventId]}"
317
                        )
318
                    );
319
                }
320
            }
321
            ++$pageCounter;
322
        };
323
    }
324
325
    /**
326
     * @param EmailAddress $address
327
     * @param string $url
328
     */
329
    protected function notifyByMail(EmailAddress $address, $url)
330
    {
331
        $this->mailer->sendNotificationMail(
332
            $address,
333
            new EventExportResult($url)
334
        );
335
    }
336
}
337