Completed
Pull Request — master (#114)
by Kristof
16:03 queued 07:12
created

EventExportService::exportEvents()   C

Complexity

Conditions 11
Paths 102

Size

Total Lines 101
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 101
rs 5.2323
cc 11
eloc 62
nc 102
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
    public function exportEvents(
77
        FileFormatInterface $fileFormat,
78
        EventExportQuery $query,
79
        EmailAddress $address = null,
80
        LoggerInterface $logger = null,
81
        $selection = null
82
    ) {
83
        if (!$logger instanceof LoggerInterface) {
84
            $logger = new NullLogger();
85
        }
86
87
        // do a pre query to test if the query is valid and check the item count
88
        try {
89
            $preQueryResult = $this->searchService->search(
90
                (string)$query,
91
                1,
92
                0
93
            );
94
            $totalItemCount = $preQueryResult->getTotalItems()->toNative();
95
        } catch (ClientErrorResponseException $e) {
96
            $logger->error(
97
                'not_exported',
98
                array(
99
                    'query' => (string)$query,
100
                    'error' => $e->getMessage(),
101
                    'exception_class' => get_class($e),
102
                )
103
            );
104
105
            throw ($e);
106
        }
107
108
        print($totalItemCount) . PHP_EOL;
109
110
        if ($totalItemCount < 1) {
111
            $logger->error(
112
                'not_exported',
113
                array(
114
                    'query' => (string)$query,
115
                    'error' => "query did not return any results"
116
                )
117
            );
118
119
            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...
120
        }
121
122
        try {
123
            $tmpDir = sys_get_temp_dir();
124
            $tmpFileName = $this->uuidGenerator->generate();
125
            $tmpPath = "{$tmpDir}/{$tmpFileName}";
126
127
            // $events are keyed here by the authoritative event ID.
128
            if ($selection) {
129
                $events = $this->getEventsAsJSONLD($selection);
0 ignored issues
show
Documentation introduced by
$selection is of type array<integer,string>, 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...
130
            } else {
131
                $events = $this->search(
132
                    $totalItemCount,
133
                    $query,
134
                    $logger
135
                );
136
            }
137
138
            $fileWriter = $fileFormat->getWriter();
139
            $fileWriter->write($tmpPath, $events);
140
141
            $finalPath = $this->getFinalFilePath($fileFormat, $tmpPath);
142
143
            $moved = copy($tmpPath, $finalPath);
144
            unlink($tmpPath);
145
146
            if (!$moved) {
147
                throw new \RuntimeException(
148
                    'Unable to move export file to public directory ' .
149
                    $this->publicDirectory
150
                );
151
            }
152
153
            $finalUrl = $this->iriGenerator->iri(
154
                basename($finalPath)
155
            );
156
157
            $logger->info(
158
                'job_info',
159
                [
160
                    'location' => $finalUrl,
161
                ]
162
            );
163
164
            if ($address) {
165
                $this->notifyByMail($address, $finalUrl);
166
            }
167
168
            return $finalUrl;
169
        } catch (\Exception $e) {
170
            if (isset($tmpPath) && $tmpPath && file_exists($tmpPath)) {
171
                unlink($tmpPath);
172
            }
173
174
            throw $e;
175
        }
176
    }
177
178
    /**
179
     * Get all events formatted as JSON-LD.
180
     *
181
     * @param \Traversable $events
182
     * @return \Generator
183
     */
184
    private function getEventsAsJSONLD($events)
185
    {
186
        foreach ($events as $eventId) {
187
            yield $eventId => $this->eventService->getEvent($eventId);
188
        }
189
    }
190
191
    /**
192
     * @param FileFormatInterface $fileFormat
193
     * @param string $tmpPath
194
     * @return string
195
     */
196
    private function getFinalFilePath(
197
        FileFormatInterface $fileFormat,
198
        $tmpPath
199
    ) {
200
        $fileUniqueId = basename($tmpPath);
201
        $extension = $fileFormat->getFileNameExtension();
202
        $finalFileName = $fileUniqueId . '.' . $extension;
203
        $finalPath = $this->publicDirectory . '/' . $finalFileName;
204
205
        return $finalPath;
206
    }
207
208
    /**
209
     * Generator that yields each unique search result.
210
     *
211
     * @param int $totalItemCount
212
     * @param string|object $query
213
     * @param LoggerInterface $logger
214
     *
215
     * @return \Generator
216
     */
217
    private function search($totalItemCount, $query, LoggerInterface $logger)
218
    {
219
        // change this pageSize value to increase or decrease the page size;
220
        $pageSize = 10;
221
        $pageCount = ceil($totalItemCount / $pageSize);
222
        $pageCounter = 0;
223
        $exportedEventIds = [];
224
225
        // Page querying the search service;
226
        while ($pageCounter < $pageCount) {
227
            $start = $pageCounter * $pageSize;
228
            // Sort ascending by creation date to make sure we get a quite consistent paging.
229
            $sort = 'creationdate asc';
230
            $results = $this->searchService->search(
231
                (string)$query,
232
                $pageSize,
233
                $start,
234
                $sort
235
            );
236
237
            // Iterate the results of the current page and get their IDs
238
            // by stripping them from the json-LD representation
239
            foreach ($results->getItems() as $event) {
240
                $expoId = explode('/', $event['@id']);
241
                $eventId = array_pop($expoId);
242
243
                if (!array_key_exists($eventId, $exportedEventIds)) {
244
                    $exportedEventIds[$eventId] = $pageCounter;
245
246
                    try {
247
                        $event = $this->eventService->getEvent($eventId);
248
                    } catch (EventNotFoundException $e) {
249
                        $logger->error(
250
                            $e->getMessage(),
251
                            [
252
                                'query' => $query,
253
                                'exception' => $e,
254
                            ]
255
                        );
256
257
                        continue;
258
                    }
259
260
                    yield $eventId => $event;
261
                } else {
262
                    $logger->error(
263
                        'query_duplicate_event',
264
                        array(
265
                            'query' => $query,
266
                            'error' => "found duplicate event {$eventId} on page {$pageCounter}, occurred first time on page {$exportedEventIds[$eventId]}"
267
                        )
268
                    );
269
                }
270
            }
271
            ++$pageCounter;
272
        };
273
    }
274
275
    /**
276
     * @param EmailAddress $address
277
     * @param string $url
278
     */
279
    protected function notifyByMail(EmailAddress $address, $url)
280
    {
281
        $this->mailer->sendNotificationMail(
282
            $address,
283
            new EventExportResult($url)
284
        );
285
    }
286
}
287