Completed
Branch master (19b0aa)
by Nate
07:03 queued 04:40
created

MethodBodyBuilder::setMultipartEncoded()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/*
3
 * Copyright (c) 2015 Nate Brunette.
4
 * Distributed under the MIT License (http://opensource.org/licenses/MIT)
5
 */
6
7
namespace Tebru\Retrofit\Generation\Builder;
8
9
use Tebru;
10
use Tebru\Dynamo\Model\Body;
11
12
/**
13
 * Class MethodBodyBuilder
14
 *
15
 * @author Nate Brunette <[email protected]>
16
 */
17
class MethodBodyBuilder
18
{
19
    /**
20
     * @var Body
21
     */
22
    private $methodBody;
23
    
24
    /**
25
     * @var string
26
     */
27
    private $baseUrl;
28
29
    /**
30
     * Request method
31
     *
32
     * @var string
33
     */
34
    private $requestMethod;
35
36
    /**
37
     * Request uri
38
     *
39
     * @var string
40
     */
41
    private $uri;
42
43
    /**
44
     * Array of query parameters
45
     *
46
     * @var array
47
     */
48
    private $queries = [];
49
50
    /**
51
     * Variable name of query map argument
52
     *
53
     * @var string
54
     */
55
    private $queryMap;
56
57
    /**
58
     * Request headers
59
     *
60
     * @var array
61
     */
62
    private $headers = [];
63
64
    /**
65
     * A string of php code to describe how to create the body
66
     *
67
     * @var string
68
     */
69
    private $body;
70
71
    /**
72
     * An array of body parts
73
     *
74
     * @var array
75
     */
76
    private $bodyParts = [];
77
78
    /**
79
     * True if the body is an object
80
     *
81
     * @var bool
82
     */
83
    private $bodyIsObject = false;
84
85
    /**
86
     * True if the body is an optional paramter
87
     *
88
     * @var bool
89
     */
90
    private $bodyIsOptional = false;
91
92
    /**
93
     * The default body value
94
     *
95
     * @var mixed
96
     */
97
    private $bodyDefaultValue;
98
99
    /**
100
     * True if the body implements \JsonSerializable
101
     *
102
     * @var boolean
103
     */
104
    private $bodyIsJsonSerializable;
105
106
    /**
107
     * True if the body is an array
108
     *
109
     * @var bool
110
     */
111
    private $bodyIsArray = false;
112
113
    /**
114
     * If we should json encode the body
115
     *
116
     * @var bool
117
     */
118
    private $jsonEncode = false;
119
120
    /**
121
     * If the body should be sent form urlencoded
122
     *
123
     * @var bool
124
     */
125
    private $formUrlEncoded = false;
126
127
    /**
128
     * If the body should be sent as multipart
129
     *
130
     * @var bool
131
     */
132
    private $multipartEncoded = false;
133
134
    /**
135
     * Method return type
136
     *
137
     * @var string
138
     */
139
    private $returnType = 'array';
140
141
    /**
142
     * JMS Serializer serialization context
143
     *
144
     * @var array
145
     */
146
    private $serializationContext = [];
147
148
    /**
149
     * JMS Serializer deserialization attributes
150
     *
151
     * @var array
152
     */
153
    private $deserializationContext = [];
154
155
    /**
156
     * Request callback variable name
157
     *
158
     * @var string
159
     */
160
    private $callback;
161
162
    /**
163
     * Async callback is optional
164
     *
165
     * @var bool
166
     */
167
    private $callbackOptional = false;
168
169
    /**
170
     * Boundary Id for multipart requests
171
     *
172
     * @var string
173
     */
174
    private $boundaryId;
175
176
    /**
177
     * Constructor
178
     */
179
    public function __construct()
180
    {
181
        $this->methodBody = new Body();
182
    }
183
184
    /**
185
     * @param string $baseUrl
186
     */
187
    public function setBaseUrl($baseUrl)
188
    {
189
        $this->baseUrl = $baseUrl;
190
    }
191
192
    /**
193
     * @param string $requestMethod
194
     */
195
    public function setRequestMethod($requestMethod)
196
    {
197
        $this->requestMethod = $requestMethod;
198
    }
199
200
    /**
201
     * @param string $uri
202
     */
203
    public function setUri($uri)
204
    {
205
        $this->uri = $uri;
206
    }
207
208
    /**
209
     * @param array $queries
210
     */
211
    public function setQueries(array $queries)
212
    {
213
        $this->queries = $queries;
214
    }
215
216
    /**
217
     * @param string $queryMap
218
     */
219
    public function setQueryMap($queryMap)
220
    {
221
        $this->queryMap = $queryMap;
222
    }
223
224
    /**
225
     * @param array $headers
226
     */
227
    public function setHeaders(array $headers)
228
    {
229
        $this->headers = $headers;
230
    }
231
232
    /**
233
     * @param string $body
234
     */
235
    public function setBody($body)
236
    {
237
        $this->body = $body;
238
    }
239
240
    /**
241
     * @param array $bodyParts
242
     */
243
    public function setBodyParts(array $bodyParts)
244
    {
245
        $this->bodyParts = $bodyParts;
246
    }
247
248
    /**
249
     * @param boolean $bodyIsObject
250
     */
251
    public function setBodyIsObject($bodyIsObject)
252
    {
253
        $this->bodyIsObject = $bodyIsObject;
254
    }
255
256
    /**
257
     * @param boolean $bodyIsOptional
258
     */
259
    public function setBodyIsOptional($bodyIsOptional)
260
    {
261
        $this->bodyIsOptional = $bodyIsOptional;
262
    }
263
264
    /**
265
     * @param mixed $bodyDefaultValue
266
     */
267
    public function setBodyDefaultValue($bodyDefaultValue)
268
    {
269
        $this->bodyDefaultValue = $bodyDefaultValue;
270
    }
271
272
    /**
273
     * @param boolean $bodyIsJsonSerializable
274
     */
275
    public function setBodyIsJsonSerializable($bodyIsJsonSerializable)
276
    {
277
        $this->bodyIsJsonSerializable = $bodyIsJsonSerializable;
278
    }
279
280
    /**
281
     * @param boolean $bodyIsArray
282
     */
283
    public function setBodyIsArray($bodyIsArray)
284
    {
285
        $this->bodyIsArray = $bodyIsArray;
286
    }
287
288
    /**
289
     * @param boolean $jsonEncode
290
     */
291
    public function setJsonEncode($jsonEncode)
292
    {
293
        $this->jsonEncode = $jsonEncode;
294
    }
295
296
    /**
297
     * @param boolean $formUrlEncoded
298
     */
299
    public function setFormUrlEncoded($formUrlEncoded)
300
    {
301
        $this->formUrlEncoded = $formUrlEncoded;
302
    }
303
304
    /**
305
     * @param boolean $multipartEncoded
306
     */
307
    public function setMultipartEncoded($multipartEncoded)
308
    {
309
        $this->multipartEncoded = $multipartEncoded;
310
    }
311
312
    /**
313
     * @param string $boundaryId
314
     */
315
    public function setBoundaryId($boundaryId)
316
    {
317
        $this->boundaryId = $boundaryId;
318
    }
319
320
    /**
321
     * @param string $returnType
322
     */
323
    public function setReturnType($returnType)
324
    {
325
        $this->returnType = $returnType;
326
    }
327
328
    /**
329
     * @param array $serializationContext
330
     */
331
    public function setSerializationContext(array $serializationContext)
332
    {
333
        $this->serializationContext = $serializationContext;
334
    }
335
336
    /**
337
     * @param array $deserializationContext
338
     */
339
    public function setDeserializationContext(array $deserializationContext)
340
    {
341
        $this->deserializationContext = $deserializationContext;
342
    }
343
344
    /**
345
     * @param string $callback
346
     */
347
    public function setCallback($callback)
348
    {
349
        $this->callback = $callback;
350
    }
351
352
    /**
353
     * @param boolean $callbackOptional
354
     */
355
    public function setCallbackOptional($callbackOptional)
356
    {
357
        $this->callbackOptional = $callbackOptional;
358
    }
359
360
    /**
361
     * Build the method body
362
     *
363
     * @return string
364
     */
365
    public function build()
366
    {
367
        Tebru\assertThat(null === $this->body || empty($this->bodyParts), 'Cannot have both @Body and @Part annotations');
368
369
        $this->createRequestUrl();
370
        $this->createHeaders();
371
        $this->createBody();
372
        $this->createResponse();
373
        $this->createReturns();
374
375
        return (string) $this->methodBody;
376
    }
377
378
    /**
379
     * Build the request url
380
     */
381
    private function createRequestUrl()
382
    {
383
        Tebru\assertNotNull($this->uri, 'Request annotation not found (e.g. @GET, @POST)');
384
385
        $baseUrl = (null !== $this->baseUrl) ? $this->baseUrl : '$this->baseUrl';
386
387
        // request request params using http_build_query if we have a query map
388
        if (null !== $this->queryMap) {
389
            // if we have regular queries, add them to the query builder
390
            if (!empty($this->queries)) {
391
                $queryArray = $this->arrayToString($this->queries);
392
                $this->methodBody->add('$queryArray = \Tebru\Retrofit\Generation\Manipulator\QueryManipulator::boolToString(%s + %s);', $queryArray, $this->queryMap);
393
                $this->methodBody->add('$queryString = http_build_query($queryArray);');
394
            } else {
395
                $this->methodBody->add('$queryArray = \Tebru\Retrofit\Generation\Manipulator\QueryManipulator::boolToString(%s);', $this->queryMap);
396
                $this->methodBody->add('$queryString = http_build_query($queryArray);');
397
            }
398
399
            $this->methodBody->add('$requestUrl = %s . "%s?" . $queryString;', $baseUrl, $this->uri);
400
401
            // if we have queries, add them to the request url
402
        } elseif (!empty($this->queries)) {
403
            $queryArray = $this->arrayToString($this->queries);
404
            $this->methodBody->add('$queryArray = \Tebru\Retrofit\Generation\Manipulator\QueryManipulator::boolToString(%s);', $queryArray);
405
            $this->methodBody->add('$queryString = http_build_query($queryArray);');
406
            $this->methodBody->add('$requestUrl = %s . "%s" . "?" . $queryString;', $baseUrl, $this->uri);
407
        } else {
408
            $this->methodBody->add('$requestUrl = %s . "%s";', $baseUrl, $this->uri);
409
        }
410
    }
411
412
    /**
413
     * Build the headers
414
     */
415
    private function createHeaders()
416
    {
417
        if (empty($this->headers)) {
418
            $this->methodBody->add('$headers = [];');
419
420
            return;
421
        }
422
423
        $this->methodBody->add('$headers = %s;', $this->arrayToString($this->headers));
424
    }
425
426
    /**
427
     * Build the request body
428
     */
429
    private function createBody()
430
    {
431
        // body should be null if there isn't a body or parts
432
        if (null === $this->body && empty($this->bodyParts)) {
433
            $this->methodBody->add('$body = null;');
434
435
            return;
436
        }
437
438
        // if body is optional and 'empty' it should be null
439
        if ($this->bodyIsOptional) {
440
            $this->methodBody->add('if (empty(%s)) { $body = null; } else {', $this->body);
441
        }
442
443
        $this->doCreateBody();
444
445
        if ($this->bodyIsOptional) {
446
            $this->methodBody->add('}');
447
        }
448
    }
449
450
    /**
451
     * Logic to actually create body
452
     */
453
    private function doCreateBody()
454
    {
455
        // check if body is a string, set variable if it doesn't already equal '$body'
456
        if (!$this->bodyIsArray && !$this->bodyIsObject && empty($this->bodyParts)) {
457
            if ('$body' !== $this->body) {
458
                $this->methodBody->add('$body = %s;', $this->body);
459
            }
460
461
            return;
462
        }
463
464
        // if we're json encoding, we don't need the body as an array first
465
        if ($this->jsonEncode) {
466
            $this->createBodyJson();
467
468
            return;
469
        }
470
471
        // otherwise, we do need to convert the body to an array
472
        $this->createBodyArray();
473
474
        if ($this->formUrlEncoded) {
475
            $this->methodBody->add('$bodyArray = \Tebru\Retrofit\Generation\Manipulator\QueryManipulator::boolToString($bodyArray);');
476
            $this->methodBody->add('$body = http_build_query($bodyArray);');
477
478
            return;
479
        }
480
481
        // body is multipart
482
        $this->methodBody->add('$bodyParts = [];');
483
        $this->methodBody->add('foreach ($bodyArray as $key => $value) {');
484
        $this->methodBody->add('$file = null;');
485
        $this->methodBody->add('if (is_resource($value)) { $file = $value; }');
486
        $this->methodBody->add('if (is_string($value)) { $file = fopen($value, "r"); }');
487
        $this->methodBody->add('if (!is_resource($file)) { throw new \LogicException("Expected resource or file path"); }');
488
        $this->methodBody->add('$bodyParts[] = ["name" => $key, "contents" => $file];');
489
        $this->methodBody->add('}');
490
491
        $this->methodBody->add('$body = new \GuzzleHttp\Psr7\MultipartStream($bodyParts, "%s");', $this->boundaryId);
492
    }
493
494
    /**
495
     * Create json body
496
     */
497 View Code Duplication
    private function createBodyJson()
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
498
    {
499
        // json encode arrays
500
        if ($this->bodyIsArray) {
501
            $this->methodBody->add('$body = json_encode(%s);', $this->body);
502
503
            return;
504
        }
505
506
        // if parts exist, json encode the parts
507
        if (!empty($this->bodyParts)) {
508
            $this->methodBody->add('$body = json_encode(%s);', $this->arrayToString($this->bodyParts));
509
510
            return;
511
        }
512
513
        // if it's an object, serialize it unless it implements \JsonSerializable
514
        if ($this->bodyIsObject) {
515
            if ($this->bodyIsJsonSerializable) {
516
                $this->methodBody->add('$body = json_encode(%s);', $this->body);
517
518
                return;
519
            }
520
521
            $this->serializeObject('$bodySerializationContext', '$body', $this->body);
522
        }
523
    }
524
525
    /**
526
     * Normalize body as array
527
     */
528 View Code Duplication
    private function createBodyArray()
1 ignored issue
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
529
    {
530
        // if it's already an array, set to variable
531
        if ($this->bodyIsArray) {
532
            $this->methodBody->add('$bodyArray = %s;', $this->body);
533
534
            return;
535
        }
536
537
        // if parts exist, set to variable
538
        if (!empty($this->bodyParts)) {
539
            $this->methodBody->add('$bodyArray = %s;', $this->arrayToString($this->bodyParts));
540
541
            return;
542
        }
543
544
        // if it implements \JsonSerializable, call jsonSerialize() to get array
545
        if ($this->bodyIsJsonSerializable) {
546
            $this->methodBody->add('$bodyArray = %s->jsonSerialize();', $this->body);
547
548
            return;
549
        }
550
551
        // otherwise, serialize and json_decode()
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
552
        $this->serializeObject('$bodySerializationContext', '$serializedBody', $this->body);
553
        $this->methodBody->add('$bodyArray = json_decode($serializedBody, true);');
554
    }
555
556
    /**
557
     * Helper method to serialize an object
558
     *
559
     * @param string $contextVar
560
     * @param string $bodyVar
561
     * @param string $object
562
     */
563
    private function serializeObject($contextVar, $bodyVar, $object)
564
    {
565
        $this->methodBody->add('%s = \JMS\Serializer\SerializationContext::create();', $contextVar);
566
        if (!empty($this->serializationContext)) {
567
            $this->createContext($contextVar, $this->serializationContext);
568
        }
569
570
        $this->methodBody->add('%s = $this->serializer->serialize(%s, "json", %s);', $bodyVar, $object, $contextVar);
571
    }
572
573
    /**
574
     * Build the response
575
     */
576
    private function createResponse()
577
    {
578
        $this->methodBody->add('$request = new \GuzzleHttp\Psr7\Request("%s", $requestUrl, $headers, $body);', strtoupper($this->requestMethod));
579
        $this->methodBody->add('$beforeSendEvent = new \Tebru\Retrofit\Event\BeforeSendEvent($request);');
580
        $this->methodBody->add('$this->eventDispatcher->dispatch("retrofit.beforeSend", $beforeSendEvent);');
581
        $this->methodBody->add('$request = $beforeSendEvent->getRequest();');
582
        $this->methodBody->add('try {');
583
584 View Code Duplication
        if ($this->callback !== null && $this->callbackOptional) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
585
            $this->methodBody->add('if (%s !== null) {', $this->callback);
586
            $this->methodBody->add('$response = $this->client->sendAsync($request, %s);', $this->callback);
587
            $this->methodBody->add('} else {');
588
            $this->methodBody->add('$response = $this->client->send($request);');
589
            $this->methodBody->add('}');
590
        } elseif ($this->callback !== null && !$this->callbackOptional) {
591
            $this->methodBody->add('$response = $this->client->sendAsync($request, %s);', $this->callback);
592
        } else {
593
            $this->methodBody->add('$response = $this->client->send($request);');
594
        }
595
596
        $this->methodBody->add('} catch (\Exception $exception) {');
597
        $this->methodBody->add('$apiExceptionEvent = new \Tebru\Retrofit\Event\ApiExceptionEvent($exception, $request);');
598
        $this->methodBody->add('$this->eventDispatcher->dispatch("retrofit.apiException", $apiExceptionEvent);');
599
        $this->methodBody->add('$exception = $apiExceptionEvent->getException();');
600
        $this->methodBody->add('throw new \Tebru\Retrofit\Exception\RetrofitApiException(get_class($this), $exception->getMessage(), $exception->getCode(), $exception);');
601
        $this->methodBody->add('}');
602
        $this->methodBody->add('$afterSendEvent = new \Tebru\Retrofit\Event\AfterSendEvent($request, $response);');
603
        $this->methodBody->add('$this->eventDispatcher->dispatch("retrofit.afterSend", $afterSendEvent);');
604
        $this->methodBody->add('$response = $afterSendEvent->getResponse();');
605
    }
606
607
    /**
608
     * Build the return
609
     */
610
    private function createReturns()
611
    {
612 View Code Duplication
        if ($this->callback !== null && $this->callbackOptional) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
613
            $this->methodBody->add('if (%s !== null) {', $this->callback);
614
            $this->methodBody->add('$returnEvent = new \Tebru\Retrofit\Event\ReturnEvent(null);');
615
            $this->methodBody->add('$this->eventDispatcher->dispatch("retrofit.return", $returnEvent);');
616
            $this->methodBody->add('return $returnEvent->getReturn();');
617
            $this->methodBody->add('}');
618
        } elseif ($this->callback !== null && !$this->callbackOptional) {
619
            $this->methodBody->add('$returnEvent = new \Tebru\Retrofit\Event\ReturnEvent(null);');
620
            $this->methodBody->add('$this->eventDispatcher->dispatch("retrofit.return", $returnEvent);');
621
            $this->methodBody->add('return $returnEvent->getReturn();');
622
623
            return;
624
        }
625
626
        $matches = [];
627
        $returnType = $this->returnType;
628
        $responseReturn = false;
629
        preg_match('/^Response<(.+)>$/', $returnType, $matches);
630
631
        if (isset($matches[1])) {
632
            $returnType = $matches[1];
633
            $responseReturn = true;
634
        }
635
636
        $this->methodBody->add(
637
            '$retrofitResponse = new \Tebru\Retrofit\Http\Response($response, "%s", $this->serializer, %s);',
638
            $returnType,
639
            $this->arrayToString($this->deserializationContext)
640
        );
641
642
        if ($responseReturn) {
643
            $this->methodBody->add('$return = $retrofitResponse;');
644
        } else {
645
            $this->methodBody->add('$return = $retrofitResponse->body();');
646
        }
647
648
        $this->methodBody->add('$returnEvent = new \Tebru\Retrofit\Event\ReturnEvent($return);');
649
        $this->methodBody->add('$this->eventDispatcher->dispatch("retrofit.return", $returnEvent);');
650
        $this->methodBody->add('return $returnEvent->getReturn();');
651
    }
652
653
    /**
654
     * Build the serialization context
655
     *
656
     * @param $contextVar
657
     * @param $context
658
     */
659
    private function createContext($contextVar, &$context)
660
    {
661
        if (!empty($context['groups'])) {
662
            $this->methodBody->add('%s->setGroups(%s);', $contextVar, $this->arrayToString($context['groups']));
663
        }
664
665
        if (!empty($context['version'])) {
666
            $this->methodBody->add('%s->setVersion(%d);', $contextVar, (int) $context['version']);
667
        }
668
669
        if (!empty($context['serializeNull'])) {
670
            $this->methodBody->add('%s->setSerializeNull(%d);', $contextVar, (bool) $context['serializeNull']);
671
        }
672
673
        if (!empty($context['enableMaxDepthChecks'])) {
674
            $this->methodBody->add('%s->enableMaxDepthChecks();', $contextVar);
675
        }
676
677 View Code Duplication
        if (!empty($context['attributes'])) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
678
            foreach ($context['attributes'] as $key => $value) {
679
                $this->methodBody->add('%s->setAttribute("%s", "%s");', $contextVar, $key, $value);
680
            }
681
        }
682
    }
683
684
    /**
685
     * Create a string representation of an array
686
     *
687
     * @param array $array
688
     * @return string
689
     */
690
    private function arrayToString(array $array)
691
    {
692
        $string = var_export($array, true);
693
        $string = preg_replace('/\'\$(.+)\'/', '$' . '\\1', $string);
694
695
        return $string;
696
    }
697
}
698