Passed
Push — master ( 038ad1...258dec )
by Michiel
21:14
created

VisualizerTask::generatePumlDiagram()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 1
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19
20
declare(strict_types=1);
21
22
use function Jawira\PlantUml\encodep;
23
24
/**
25
 * Class VisualizerTask
26
 *
27
 * VisualizerTask creates diagrams using buildfiles, these diagrams represents calls and depends among targets.
28
 *
29
 * @author Jawira Portugal
30
 */
31
class VisualizerTask extends HttpTask
32
{
33
    public const FORMAT_EPS = 'eps';
34
    public const FORMAT_PNG = 'png';
35
    public const FORMAT_PUML = 'puml';
36
    public const FORMAT_SVG = 'svg';
37
    public const SERVER = 'http://www.plantuml.com/plantuml';
38
    public const STATUS_OK = 200;
39
    public const XSL_CALLS = __DIR__ . '/calls.xsl';
40
    public const XSL_FOOTER = __DIR__ . '/footer.xsl';
41
    public const XSL_HEADER = __DIR__ . '/header.xsl';
42
    public const XSL_TARGETS = __DIR__ . '/targets.xsl';
43
44
    /**
45
     * @var string Diagram format
46
     */
47
    protected $format;
48
49
    /**
50
     * @var string Location in disk where diagram is saved
51
     */
52
    protected $destination;
53
54
    /**
55
     * @var string PlantUml server
56
     */
57
    protected $server;
58
59
    /**
60
     * Setting some default values and checking requirements
61
     */
62 6
    public function init(): void
63
    {
64 6
        parent::init();
65 6
        if (!function_exists(\Jawira\PlantUml\encodep::class)) {
0 ignored issues
show
Bug introduced by
The type Jawira\PlantUml\encodep was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
66
            $exceptionMessage = get_class($this) . ' requires "jawira/plantuml-encoding" library';
67
        }
68 6
        if (!class_exists(XSLTProcessor::class)) {
69
            $exceptionMessage = get_class($this) . ' requires XSL extension';
70
        }
71 6
        if (!class_exists(SimpleXMLElement::class)) {
72
            $exceptionMessage = get_class($this) . ' requires SimpleXML extension';
73
        }
74 6
        if (isset($exceptionMessage)) {
75
            $this->log($exceptionMessage, Project::MSG_ERR);
76
            throw new BuildException($exceptionMessage);
77
        }
78 6
        $this->setFormat(VisualizerTask::FORMAT_PNG);
79 6
        $this->setServer(VisualizerTask::SERVER);
80 6
    }
81
82
    /**
83
     * The main entry point method.
84
     *
85
     * @throws \GuzzleHttp\Exception\GuzzleException
86
     * @throws \IOException
87
     * @throws \NullPointerException
88
     */
89 5
    public function main(): void
90
    {
91 5
        $pumlDiagram = $this->generatePumlDiagram();
92 5
        $destination = $this->resolveImageDestination();
93 4
        $format = $this->getFormat();
94 4
        $image = $this->generateImage($pumlDiagram, $format);
95 3
        $this->saveToFile($image, $destination);
96 3
    }
97
98
    /**
99
     * Retrieves loaded buildfiles and generates a PlantUML diagram
100
     *
101
     * @return string
102
     */
103 5
    protected function generatePumlDiagram(): string
104
    {
105
        /**
106
         * @var \PhingXMLContext $xmlContext
107
         */
108 5
        $xmlContext = $this->getProject()
109 5
            ->getReference("phing.parsing.context");
110 5
        $importStack = $xmlContext->getImportStack();
111 5
        $pumlDiagram = $this->generatePuml($importStack);
112
113 5
        return $pumlDiagram;
114
    }
115
116
    /**
117
     * Read through provided buildfiles and generates a PlantUML diagram
118
     *
119
     * @param \PhingFile[] $buildFiles
120
     *
121
     * @return string
122
     */
123 5
    protected function generatePuml(array $buildFiles): string
124
    {
125 5
        $puml = $this->transformToPuml(reset($buildFiles), VisualizerTask::XSL_HEADER);
126
127
        /**
128
         * @var \PhingFile $buildFile
129
         */
130 5
        foreach ($buildFiles as $buildFile) {
131 5
            $puml .= $this->transformToPuml($buildFile, VisualizerTask::XSL_TARGETS);
132
        }
133
134 5
        foreach ($buildFiles as $buildFile) {
135 5
            $puml .= $this->transformToPuml($buildFile, VisualizerTask::XSL_CALLS);
136
        }
137
138 5
        $puml .= $this->transformToPuml(reset($buildFiles), VisualizerTask::XSL_FOOTER);
139
140 5
        return $puml;
141
    }
142
143
    /**
144
     * Transforms buildfile using provided xsl file
145
     *
146
     * @param \PhingFile $buildfile Path to buildfile
147
     * @param string $xslFile XSLT file
148
     *
149
     * @return string
150
     */
151 5
    protected function transformToPuml(PhingFile $buildfile, string $xslFile): string
152
    {
153 5
        $xml = $this->loadXmlFile($buildfile->getPath());
154 5
        $xsl = $this->loadXmlFile($xslFile);
155
156 5
        $processor = new XSLTProcessor();
157 5
        $processor->importStylesheet($xsl);
158
159 5
        return $processor->transformToXml($xml) . PHP_EOL;
160
    }
161
162
    /**
163
     * Load XML content from a file
164
     *
165
     * @param string $xmlFile XML or XSLT file
166
     *
167
     * @return \SimpleXMLElement
168
     */
169 5
    protected function loadXmlFile(string $xmlFile): SimpleXMLElement
170
    {
171 5
        $xmlContent = (new FileReader($xmlFile))->read();
172 5
        $xml = simplexml_load_string($xmlContent);
173
174 5
        if (!($xml instanceof SimpleXMLElement)) {
175
            $message = "Error loading XML file: $xmlFile";
176
            $this->log($message, Project::MSG_ERR);
177
            throw new BuildException($message);
178
        }
179
180 5
        return $xml;
181
    }
182
183
    /**
184
     * Get the image's final location
185
     *
186
     * @return \PhingFile
187
     * @throws \IOException
188
     * @throws \NullPointerException
189
     */
190 5
    protected function resolveImageDestination(): PhingFile
191
    {
192 5
        $phingFile = $this->getProject()->getProperty('phing.file');
193 5
        $format = $this->getFormat();
194 5
        $candidate = $this->getDestination();
195 5
        $path = $this->resolveDestination($phingFile, $format, $candidate);
196
197 4
        return new PhingFile($path);
198
    }
199
200
    /**
201
     * @return string
202
     */
203 5
    public function getFormat(): string
204
    {
205 5
        return $this->format;
206
    }
207
208
    /**
209
     * Sets and validates diagram's format
210
     *
211
     * @param string $format
212
     *
213
     * @return VisualizerTask
214
     */
215 6
    public function setFormat(string $format): VisualizerTask
216
    {
217 6
        switch ($format) {
218
            case VisualizerTask::FORMAT_PUML:
219
            case VisualizerTask::FORMAT_PNG:
220
            case VisualizerTask::FORMAT_EPS:
221
            case VisualizerTask::FORMAT_SVG:
222 6
                $this->format = $format;
223 6
                break;
224
            default:
225 1
                $message = "'$format' is not a valid format";
226 1
                $this->log($message, Project::MSG_ERR);
227 1
                throw new BuildException($message);
228
                break;
229
        }
230
231 6
        return $this;
232
    }
233
234
    /**
235
     * @return null|string
236
     */
237 5
    public function getDestination(): ?string
238
    {
239 5
        return $this->destination;
240
    }
241
242
    /**
243
     * @param string $destination
244
     *
245
     * @return VisualizerTask
246
     */
247 3
    public function setDestination(?string $destination): VisualizerTask
248
    {
249 3
        $this->destination = $destination;
250
251 3
        return $this;
252
    }
253
254
    /**
255
     * Figure diagram's file path
256
     *
257
     * @param string $buildfilePath Path to main buildfile
258
     * @param string $format Extension to use
259
     * @param null|string $destination Desired destination provided by user
260
     *
261
     * @return string
262
     */
263 5
    protected function resolveDestination(string $buildfilePath, string $format, ?string $destination): string
264
    {
265 5
        $buildfileInfo = pathinfo($buildfilePath);
266
267
        // Fallback
268 5
        if (empty($destination)) {
269 2
            $destination = $buildfileInfo['dirname'];
270
        }
271
272
        // Adding filename if necessary
273 5
        if (is_dir($destination)) {
274 3
            $destination .= DIRECTORY_SEPARATOR . $buildfileInfo['filename'] . '.' . $format;
275
        }
276
277
        // Check if path is available
278 5
        if (!is_dir(dirname($destination))) {
279 1
            $message = "Directory '$destination' is invalid";
280 1
            $this->log($message, Project::MSG_ERR);
281 1
            throw new BuildException(sprintf($message, $destination));
282
        }
283
284 4
        return $destination;
285
    }
286
287
    /**
288
     * Generates an actual image using PlantUML code
289
     *
290
     * @param string $pumlDiagram
291
     * @param string $format
292
     *
293
     * @return string
294
     * @throws \GuzzleHttp\Exception\GuzzleException
295
     */
296 4
    protected function generateImage(string $pumlDiagram, string $format): string
297
    {
298 4
        if ($format === VisualizerTask::FORMAT_PUML) {
299 3
            $this->log('Bypassing, no need to call server', Project::MSG_DEBUG);
300
301 3
            return $pumlDiagram;
302
        }
303
304 1
        $format = $this->getFormat();
305 1
        $encodedPuml = encodep($pumlDiagram);
306 1
        $this->prepareImageUrl($format, $encodedPuml);
307
308
        $response = $this->request();
309
        $this->processResponse($response); // used for status validation
310
311
        return $response->getBody()->getContents();
312
    }
313
314
    /**
315
     * Prepares URL from where image will be downloaded
316
     *
317
     * @param string $format
318
     * @param string $encodedPuml
319
     */
320 1
    protected function prepareImageUrl(string $format, string $encodedPuml): void
321
    {
322 1
        $server = $this->getServer();
323 1
        $this->log("Server: $server", Project::MSG_VERBOSE);
324
325 1
        $server = filter_var($server, FILTER_VALIDATE_URL);
326 1
        if ($server === false) {
327 1
            $message = 'Invalid PlantUml server';
328 1
            $this->log($message, Project::MSG_ERR);
329 1
            throw new BuildException($message);
330
        }
331
332
        $imageUrl = sprintf('%s/%s/%s', rtrim($server, '/'), $format, $encodedPuml);
333
        $this->log($imageUrl, Project::MSG_DEBUG);
334
        $this->setUrl($imageUrl);
335
    }
336
337
    /**
338
     * @return string
339
     */
340 1
    public function getServer(): string
341
    {
342 1
        return $this->server;
343
    }
344
345
    /**
346
     * @param string $server
347
     *
348
     * @return VisualizerTask
349
     */
350 6
    public function setServer(string $server): VisualizerTask
351
    {
352 6
        $this->server = $server;
353
354 6
        return $this;
355
    }
356
357
    /**
358
     * Receive server's response
359
     *
360
     * This method validates `$response`'s status
361
     *
362
     * @param \Psr\Http\Message\ResponseInterface $response Response from server
363
     *
364
     * @return void
365
     */
366
    protected function processResponse(\Psr\Http\Message\ResponseInterface $response): void
367
    {
368
        $status = $response->getStatusCode();
369
        $reasonPhrase = $response->getReasonPhrase();
370
        $this->log("Response status: $status", Project::MSG_DEBUG);
371
        $this->log("Response reason: $reasonPhrase", Project::MSG_DEBUG);
372
373
        if ($status !== VisualizerTask::STATUS_OK) {
374
            $message = "Request unsuccessful. Response from server: $status $reasonPhrase";
375
            $this->log($message, Project::MSG_ERR);
376
            throw new BuildException($message);
377
        }
378
    }
379
380
    /**
381
     * Save provided $content string into $destination file
382
     *
383
     * @param string $content Content to save
384
     * @param \PhingFile $destination Location where $content is saved
385
     *
386
     * @return void
387
     */
388 3
    protected function saveToFile(string $content, PhingFile $destination): void
389
    {
390 3
        $path = $destination->getPath();
391 3
        $this->log("Writing: $path", Project::MSG_INFO);
392
393 3
        (new FileWriter($destination))->write($content);
394 3
    }
395
}
396