Failed Conditions
Push — master ( 029e81...ce5fbf )
by Luca
06:29
created

Exporter   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 162
Duplicated Lines 0 %

Test Coverage

Coverage 66.67%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 15
eloc 71
dl 0
loc 162
ccs 44
cts 66
cp 0.6667
rs 10
c 2
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A export() 0 30 2
A writeCards() 0 19 4
A getWriter() 0 11 4
A exportAndSendMessage() 0 14 2
A exportAsync() 0 14 1
A __construct() 0 18 1
A reloadExport() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service\Exporter;
6
7
use Application\DBAL\Types\ExportFormatType;
8
use Application\Model\Export;
9
use Application\Model\User;
10
use Application\Repository\CardRepository;
11
use Application\Repository\ExportRepository;
12
use Application\Service\MessageQueuer;
13
use Ecodev\Felix\Api\Exception;
14
use Ecodev\Felix\Service\Mailer;
15
use InvalidArgumentException;
16
use Throwable;
17
18
class Exporter
19
{
20
    private ExportRepository $exportRepository;
21
22
    private CardRepository $cardRepository;
23
24
    private MessageQueuer $messageQueuer;
25
26
    private Mailer $mailer;
27
28
    private Writer $zip;
29
30
    private Writer $pptx;
31
32
    private Writer $csv;
33
34
    private string $phpPath;
35
36 1
    public function __construct(
37
        ExportRepository $exportRepository,
38
        CardRepository $cardRepository,
39
        MessageQueuer $messageQueuer,
40
        Mailer $mailer,
41
        Writer $zip,
42
        Writer $pptx,
43
        Writer $csv,
44
        string $phpPath
45
    ) {
46 1
        $this->exportRepository = $exportRepository;
47 1
        $this->cardRepository = $cardRepository;
48 1
        $this->messageQueuer = $messageQueuer;
49 1
        $this->mailer = $mailer;
50 1
        $this->zip = $zip;
51 1
        $this->pptx = $pptx;
52 1
        $this->csv = $csv;
53 1
        $this->phpPath = $phpPath;
54 1
    }
55
56
    /**
57
     * Export asynchronously in a separate process.
58
     *
59
     * This should be the preferred way to do big export
60
     */
61
    public function exportAsync(Export $export): void
62
    {
63
        $args = [
64
            realpath('bin/export.php'),
65
            $export->getId(),
66
        ];
67
68
        $escapedArgs = array_map('escapeshellarg', $args);
69
70
        // Forward SERVER_NAME to CLI command
71
        $env = 'SERVER_NAME=' . escapeshellarg(getenv('SERVER_NAME'));
72
73
        $cmd = $env . ' ' . escapeshellcmd($this->phpPath) . ' ' . implode(' ', $escapedArgs) . ' > /dev/null 2>&1 &';
74
        exec($cmd);
75
    }
76
77
    /**
78
     * Export immediately and return the v $export object
79
     *
80
     * Because this method will indirectly clear the EntityManager any existing object
81
     * before calling this method will become invalid and must be re-fetched from DB.
82
     */
83 2
    public function export(Export $export): Export
84
    {
85 2
        $writer = $this->getWriter($export);
86 2
        $title = $export->getSite() . '-' . $export->getId();
87
88
        // Poor man's security by using hard-to-guess suffix
89 2
        $suffix = bin2hex(random_bytes(5));
90
91 2
        $filename = $title . '-' . $suffix . '.' . $writer->getExtension();
92
93 2
        $export->markAsInProgress($filename);
94 2
        _em()->flush();
95
96
        try {
97 2
            $writer->initialize($export, $title);
98 2
            $this->writeCards($writer, $export);
99 2
            $writer->finalize();
100
101 2
            $reloadExport = $this->reloadExport($export);
102 2
            $reloadExport->markAsDone();
103
        } catch (Throwable $throwable) {
104
            $reloadExport = $this->reloadExport($export);
105
            $reloadExport->markAsErrored($throwable);
106
107
            throw $throwable;
108 2
        } finally {
109 2
            _em()->flush();
110
        }
111
112 2
        return $reloadExport;
113
    }
114
115 2
    private function reloadExport(Export $export): Export
116
    {
117 2
        User::reloadCurrentUser();
118
119 2
        return $this->exportRepository->findOneById($export->getId());
0 ignored issues
show
Bug introduced by
The method findOneById() does not exist on Application\Repository\ExportRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

119
        return $this->exportRepository->/** @scrutinizer ignore-call */ findOneById($export->getId());
Loading history...
120
    }
121
122
    /**
123
     * Export immediately and send an email to the export creator.
124
     *
125
     * This must only be used via CLI, because export might be very long (several minutes)
126
     * and the email is sent synchronously
127
     */
128
    public function exportAndSendMessage(int $id): void
129
    {
130
        /** @var null|Export $export */
131
        $export = $this->exportRepository->findOneById($id);
132
        if (!$export) {
133
            throw new InvalidArgumentException('Could not find export with ID: ' . $id);
134
        }
135
136
        $export = $this->export($export);
137
138
        $user = $export->getCreator();
139
        $message = $this->messageQueuer->queueExportDone($user, $export);
0 ignored issues
show
Bug introduced by
It seems like $user can also be of type null; however, parameter $user of Application\Service\Mess...euer::queueExportDone() does only seem to accept Application\Model\User, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

139
        $message = $this->messageQueuer->queueExportDone(/** @scrutinizer ignore-type */ $user, $export);
Loading history...
140
141
        $this->mailer->sendMessage($message);
142
    }
143
144 2
    private function getWriter(Export $export): Writer
145
    {
146 2
        switch ($export->getFormat()) {
147
            case ExportFormatType::ZIP:
148 2
                return $this->zip;
149
            case ExportFormatType::PPTX:
150 1
                return $this->pptx;
151
            case ExportFormatType::CSV:
152 1
                return $this->csv;
153
            default:
154
                throw new Exception('Invalid export format:' . $export->getFormat());
155
        }
156
    }
157
158
    /**
159
     * Write all cards in small batches to avoid exploding PHP memory
160
     */
161 2
    private function writeCards(Writer $writer, Export $export): void
162
    {
163 2
        $totalRecordsProcessed = 0;
164 2
        while (true) {
165 2
            $cards = $this->cardRepository->getExportCards($export, $totalRecordsProcessed);
166
167
            // If nothing to process anymore, stop the whole thing
168 2
            if (!$cards) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cards of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
169 2
                break;
170
            }
171
172 2
            foreach ($cards as $card) {
173 2
                $writer->write($card);
174
175 2
                ++$totalRecordsProcessed;
176
            }
177
178 2
            _em()->flush();
179 2
            _em()->clear();
180
        }
181 2
    }
182
}
183