Completed
Push — master ( 930ec3...37c441 )
by Evgeny
03:09
created

Service::_initializeParser()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.4425

Importance

Changes 0
Metric Value
cc 6
eloc 10
nc 8
nop 1
dl 0
loc 16
ccs 10
cts 13
cp 0.7692
crap 6.4425
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * Copyright 2016, Cake Development Corporation (http://cakedc.com)
4
 *
5
 * Licensed under The MIT License
6
 * Redistributions of files must retain the above copyright notice.
7
 *
8
 * @copyright Copyright 2016, Cake Development Corporation (http://cakedc.com)
9
 * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
10
 */
11
12
namespace CakeDC\Api\Service;
13
14
use CakeDC\Api\Routing\ApiRouter;
15
use CakeDC\Api\Service\Action\DummyAction;
16
use CakeDC\Api\Service\Action\Result;
17
use CakeDC\Api\Service\Exception\MissingActionException;
18
use CakeDC\Api\Service\Exception\MissingParserException;
19
use CakeDC\Api\Service\Exception\MissingRendererException;
20
use CakeDC\Api\Service\Renderer\BaseRenderer;
21
use CakeDC\Api\Service\RequestParser\BaseParser;
22
use Cake\Core\App;
23
use Cake\Core\Configure;
24
use Cake\Datasource\Exception\RecordNotFoundException;
25
use Cake\Http\Client\Response;
26
use Cake\Routing\RouteBuilder;
27
use Cake\Utility\Hash;
28
use Cake\Utility\Inflector;
29
use Exception;
30
31
/**
32
 * Class Service
33
 */
34
abstract class Service
35
{
36
37
    /**
38
     * Actions routes description map, indexed by action name.
39
     *
40
     * @var array
41
     */
42
    protected $_actions = [];
43
44
    /**
45
     * Actions classes map, indexed by action name.
46
     *
47
     * @var array
48
     */
49
    protected $_actionsClassMap = [];
50
51
    /**
52
     * Service url acceptable extensions list.
53
     *
54
     * @var array
55
     */
56
    protected $_extensions = ['json'];
57
58
    /**
59
     *
60
     *
61
     * @var string
62
     */
63
    protected $_routePrefix = '';
64
65
    /**
66
     * Service name
67
     *
68
     * @var string
69
     */
70
    protected $_name = null;
71
72
    /**
73
     * Service version.
74
     *
75
     * @var int
76
     */
77
    protected $_version;
78
79
80
    /**
81
     * Parser class to process the HTTP request.
82
     *
83
     * @var BaseParser
84
     */
85
    protected $_parser;
86
87
    /**
88
     * Renderer class to build the HTTP response.
89
     *
90
     * @var BaseRenderer
91
     */
92
    protected $_renderer;
93
94
    /**
95
     * The parser class.
96
     *
97
     * @var string
98
     */
99
    protected $_parserClass = null;
100
101
    /**
102
     * The Renderer class.
103
     *
104
     * @var string
105
     */
106
    protected $_rendererClass = null;
107
108
    /**
109
     * Dependent services names list
110
     *
111
     * @var array<string>
112
     */
113
    protected $_innerServices = [];
114
115
    /**
116
     * Parent service instance.
117
     *
118
     * @var Service
119
     */
120
    protected $_parentService;
121
122
    /**
123
     * Service Action Result object.
124
     *
125
     * @var Result
126
     */
127
    protected $_result;
128
129
    /**
130
     * Base url for service.
131
     *
132
     * @var string
133
     */
134
    protected $_baseUrl;
135
136
    /**
137
     * Request
138
     *
139
     * @var \Cake\Network\Request
140
     */
141
    protected $_request;
142
143
    /**
144
     * Request
145
     *
146
     * @var \Cake\Network\Response
147
     */
148
    protected $_response;
149
150
    /**
151
     * @var string
152
     */
153
    protected $_corsSuffix = '_cors';
154
155
    /**
156
     * Service constructor.
157
     *
158
     * @param array $config Service configuration.
159
     */
160 118
    public function __construct(array $config = [])
161
    {
162 118
        if (isset($config['request'])) {
163 118
            $this->request($config['request']);
164 118
        }
165 118
        if (isset($config['response'])) {
166 118
            $this->response($config['response']);
167 118
        }
168 118
        if (isset($config['baseUrl'])) {
169 78
            $this->_baseUrl = $config['baseUrl'];
170 78
        }
171 118
        if (isset($config['service'])) {
172 96
            $this->name($config['service']);
173 96
        }
174 118
        if (isset($config['version'])) {
175
            $this->version($config['version']);
176
        }
177 118
        if (isset($config['classMap'])) {
178 1
            $this->_actionsClassMap = Hash::merge($this->_actionsClassMap, $config['classMap']);
179 1
        }
180 118
        $this->initialize();
181 118
        $this->_initializeParser($config);
182 118
        $this->_initializeRenderer($config);
183 118
    }
184
185
    /**
186
     * Get and set service name.
187
     *
188
     * @param string $name Service name.
189
     * @return string
190
     */
191 118
    public function name($name = null)
192
    {
193 118
        if ($name === null) {
194 117
            return $this->_name;
195
        }
196 118
        $this->_name = $name;
197
198 118
        return $this->_name;
199
    }
200
201
    /**
202
     * Get and set service version.
203
     *
204
     * @param int $version Version number.
205
     * @return int
206
     */
207 80
    public function version($version = null)
208
    {
209 80
        if ($version === null) {
210 80
            return $this->_version;
211
        }
212
        $this->_version = $version;
213
214
        return $this->_version;
215
    }
216
217
    /**
218
     * Initialize method
219
     *
220
     * @return void
221
     */
222 118
    public function initialize()
223
    {
224 118
        if ($this->_name === null) {
225 39
            $className = (new \ReflectionClass($this))->getShortName();
226 39
            $this->name(Inflector::underscore(str_replace('Service', '', $className)));
0 ignored issues
show
Bug introduced by
It seems like \Cake\Utility\Inflector:...vice', '', $className)) targeting Cake\Utility\Inflector::underscore() can also be of type boolean; however, CakeDC\Api\Service\Service::name() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
227 39
        }
228 118
    }
229
230
    /**
231
     * Service parser configuration method.
232
     *
233
     * @param BaseParser $parser A Parser instance.
234
     * @return BaseParser
235
     */
236 51
    public function parser(BaseParser $parser = null)
237
    {
238 51
        if ($parser === null) {
239 51
            return $this->_parser;
240
        }
241
        $this->_parser = $parser;
242
243
        return $this->_parser;
244
    }
245
246
    /**
247
     * Get and set request.
248
     *
249
     * @param \Cake\Network\Request $request A request object.
250
     * @return \Cake\Network\Request
251
     */
252 118
    public function request($request = null)
253
    {
254 118
        if ($request === null) {
255 101
            return $this->_request;
256
        }
257
258 118
        $this->_request = $request;
259
260 118
        return $this->_request;
261
    }
262
263
    /**
264
     * Get the service route scopes and their connected routes.
265
     *
266
     * @return array
267
     */
268 3
    public function routes()
269
    {
270
        return $this->_routesWrapper(function () {
271 3
            return ApiRouter::routes();
272 3
        });
273
    }
274
275
    /**
276
     * @param callable $callable Wrapped router instance.
277
     * @return mixed
278
     */
279 64
    protected function _routesWrapper(callable $callable)
280
    {
281 64
        $this->resetRoutes();
282 64
        $this->loadRoutes();
283 64
        ApiRouter::$initialized = true;
284 64
        $result = $callable();
285 63
        $this->resetRoutes();
286
287 63
        return $result;
288
    }
289
290
    /**
291
     * Reset to default application routes.
292
     *
293
     * @return void
294
     */
295 64
    public function resetRoutes()
296
    {
297 64
        ApiRouter::reload();
298 64
    }
299
300
    /**
301
     * Initialize service level routes
302
     *
303
     * @return void
304
     */
305 8
    public function loadRoutes()
306
    {
307 8
        $defaultOptions = $this->routerDefaultOptions();
308
        ApiRouter::scope('/', $defaultOptions, function (RouteBuilder $routes) use ($defaultOptions) {
309 8
            if (is_array($this->_extensions)) {
310 8
                $routes->extensions($this->_extensions);
311 8
            }
312 8
            if (!empty($defaultOptions['map'])) {
313 8
                $routes->resources($this->name(), $defaultOptions);
314 8
            }
315 8
        });
316 8
    }
317
318
    /**
319
     * Build router settings.
320
     * This implementation build action map for resource routes based on Service actions.
321
     *
322
     * @return array
323
     */
324 63
    public function routerDefaultOptions()
325
    {
326 63
        $mapList = [];
327 63
        foreach ($this->_actions as $alias => $map) {
328 42
            if (is_numeric($alias)) {
329
                $alias = $map;
330
                $map = [];
331
            }
332 42
            $mapCors = false;
333 42
            if (!empty($map['mapCors'])) {
334 8
                $mapCors = $map['mapCors'];
335 8
                unset($map['mapCors']);
336 8
            }
337 42
            $mapList[$alias] = $map;
338 42
            $mapList[$alias] += ['method' => 'GET', 'path' => '', 'action' => $alias];
339 42
            if ($mapCors) {
340 8
                $map['method'] = 'OPTIONS';
341 8
                $map += ['path' => '', 'action' => $alias . $this->_corsSuffix];
342 8
                $mapList[$alias . $this->_corsSuffix] = $map;
343 8
            }
344 63
        }
345
346
        return [
347
            'map' => $mapList
348 63
        ];
349
    }
350
351
    /**
352
     * Finds URL for specified action.
353
     *
354
     * Returns an URL pointing to a combination of controller and action.
355
     *
356
     * @param string|array|null $route An array specifying any of the following:
357
     *   'controller', 'action', 'plugin' additionally, you can provide routed
358
     *   elements or query string parameters. If string it can be name any valid url
359
     *   string.
360
     * @return string Full translated URL with base path.
361
     * @throws \Cake\Core\Exception\Exception When the route name is not found
362
     */
363
    public function routeUrl($route)
364
    {
365
        return $this->_routesWrapper(function () use ($route) {
366
            return ApiRouter::url($route);
367
        });
368
    }
369
370
    /**
371
     * Reverses a parsed parameter array into a string.
372
     *
373
     * @param \Cake\Network\Request|array $params The params array or
374
     *     Cake\Network\Request object that needs to be reversed.
375
     * @return string The string that is the reversed result of the array
376
     */
377 13
    public function routeReverse($params)
378
    {
379
        return $this->_routesWrapper(function () use ($params) {
380
            try {
381 13
                return ApiRouter::reverse($params);
382 1
            } catch (Exception $e) {
383 1
                return null;
384
            }
385 13
        });
386
    }
387
388
    /**
389
     * Dispatch service call.
390
     *
391
     * @return \CakeDC\Api\Service\Action\Result
392
     */
393 56
    public function dispatch()
394
    {
395
        try {
396 56
            $action = $this->buildAction();
397 56
            $result = $action->process();
398
399 52
            if ($result instanceof Result) {
400
                $this->result($result);
401
            }  else {
402 52
                $this->result()->data($result);
403 52
                $this->result()->code(200);
404
            }
405 56
        } catch (RecordNotFoundException $e) {
406 6
            $this->result()->code(404);
407 6
            $this->result()->exception($e);
408 8
        } catch (Exception $e) {
409 2
            $code = $e->getCode();
410 2
            if (!is_int($code) || $code < 100 || $code >= 600) {
411
                $this->result()->code(500);
412
            }
413 2
            $this->result()->exception($e);
414
        }
415
416 56
        return $this->result();
417
    }
418
419
    /**
420
     * Build action instance
421
     *
422
     * @return \CakeDC\Api\Service\Action\Action
423
     * @throws Exception
424
     */
425 63
    public function buildAction()
426
    {
427 63
        $route = $this->parseRoute($this->baseUrl());
428 62
        if (empty($route)) {
429
            throw new MissingActionException('Invalid Action Route:' . $this->baseUrl()); // InvalidActionException
430
        }
431 62
        $service = null;
432 62
        $serviceName = Inflector::underscore($route['controller']);
433 62
        if ($serviceName == $this->name()) {
434 53
            $service = $this;
435 53
        }
436 62
        if (in_array($serviceName, $this->_innerServices)) {
437
            $options = [
438 9
                'version' => $this->version(),
439 9
                'request' => $this->request(),
440 9
                'response' => $this->response(),
441 9
            ];
442 9
            $service = ServiceRegistry::get($serviceName, $options);
0 ignored issues
show
Bug introduced by
It seems like $serviceName defined by \Cake\Utility\Inflector:...e($route['controller']) on line 432 can also be of type boolean; however, CakeDC\Api\Service\ServiceRegistry::get() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
443 9
            $service->parent($this);
444 9
        }
445 62
        $action = $route['action'];
446 62
        list($namespace, $serviceClass) = namespaceSplit(get_class($service));
447 62
        $actionPrefix = substr($serviceClass, 0, -7);
448 62
        $actionClass = $namespace . '\\Action\\' . $actionPrefix . Inflector::camelize($action) . 'Action';
449 62
        if (class_exists($actionClass)) {
450 2
            return $service->buildActionClass($actionClass, $route);
451
        }
452 60
        if (array_key_exists($action, $this->_actionsClassMap)) {
453 60
            $actionClass = $this->_actionsClassMap[$action];
454
455 60
            return $service->buildActionClass($actionClass, $route);
456
        }
457
        throw new MissingActionException(['class' => $actionClass]);
458
    }
459
460
    /**
461
     * Parses given URL string. Returns 'routing' parameters for that URL.
462
     *
463
     * @param string $url URL to be parsed
464
     * @return array Parsed elements from URL
465
     * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
466
     */
467
    public function parseRoute($url)
468
    {
469 63
        return $this->_routesWrapper(function () use ($url) {
470 63
            return ApiRouter::parse($url);
471 63
        });
472
    }
473
474
    /**
475
     * Build base url
476
     *
477
     * @return string
478
     */
479 64
    public function baseUrl()
480
    {
481 64
        if (!empty($this->_baseUrl)) {
482 64
            return $this->_baseUrl;
483
        }
484
485
        $result = '/' . $this->name();
486
487
        return $result;
488
    }
489
490
    /**
491
     * Parent service get and set methods
492
     *
493
     * @param Service $service Parent Service instance.
494
     * @return Service
495
     */
496 54
    public function parent(Service $service = null)
497
    {
498 54
        if ($service === null) {
499 54
            return $this->_parentService;
500
        }
501 9
        $this->_parentService = $service;
502
503 9
        return $this->_parentService;
504
    }
505
506
    /**
507
     * Build action class
508
     *
509
     * @param string $class Class name.
510
     * @param array $route Activated route.
511
     * @return mixed
512
     */
513 63
    public function buildActionClass($class, $route)
514
    {
515 63
        $instance = new $class($this->_actionOptions($route));
516
517 63
        return $instance;
518
    }
519
520
    /**
521
     * Action constructor options.
522
     *
523
     * @param array $route Activated route.
524
     * @return array
525
     */
526 63
    protected function _actionOptions($route)
527
    {
528 63
        $actionName = $route['action'];
529
530
        $options = [
531 63
            'name' => $actionName,
532 63
            'service' => $this,
533 63
            'route' => $route,
534 63
        ];
535 63
        $options += (new ConfigReader())->actionOptions($this->name(), $actionName, $this->version());
536
537 63
        return $options;
538
    }
539
540
    /**
541
     * @return \CakeDC\Api\Service\Action\Result
542
     */
543 56
    public function result($value = null)
544
    {
545 56
        if ($this->_parentService !== null) {
546 2
            return $this->_parentService->result($value);
547
        }
548 56
        if ($value instanceof Result) {
549
            $this->_result = $value;
550
        }
551 56
        if ($this->_result === null) {
552 56
            $this->_result = new Result();
553 56
        }
554
555 56
        return $this->_result;
556
    }
557
558
    /**
559
     * Fill up response and stop execution.
560
     *
561
     * @param Result $result A Result instance.
562
     * @return Response
563
     */
564 56
    public function respond($result = null)
565
    {
566 56
        if ($result === null) {
567
            $result = $this->result();
568
        }
569 56
        $this->response()
570 56
             ->statusCode($result->code());
571 56
        if ($result->exception() !== null) {
572 8
            $this->renderer()
573 8
                 ->error($result->exception());
574 8
        } else {
575 52
            $this->renderer()
576 52
                 ->response($result);
577
        }
578
579 56
        return $this->response();
580
    }
581
582
    /**
583
     * Get and set response.
584
     *
585
     * @param \Cake\Network\Response $response  A Response object.
586
     * @return \Cake\Network\Response
587
     */
588 118
    public function response($response = null)
589
    {
590 118
        if ($response === null) {
591 109
            return $this->_response;
592
        }
593
594 118
        $this->_response = $response;
595
596 118
        return $this->_response;
597
    }
598
599
    /**
600
     * Service renderer configuration method.
601
     *
602
     * @param BaseRenderer $renderer A Renderer instance.
603
     * @return BaseRenderer
604
     */
605 68
    public function renderer(BaseRenderer $renderer = null)
606
    {
607 68
        if ($renderer === null) {
608 68
            return $this->_renderer;
609
        }
610
        $this->_renderer = $renderer;
611
612
        return $this->_renderer;
613
    }
614
615
    /**
616
     * Define action config.
617
     *
618
     * @param string $actionName Action name.
619
     * @param string $className Class name.
620
     * @param array $route Route config.
621
     * @return void
622
     */
623 8
    public function mapAction($actionName, $className, $route)
624
    {
625 8
        $route += ['mapCors' => false];
626 8
        $this->_actionsClassMap[$actionName] = $className;
627 8
        if ($route['mapCors']) {
628 8
            $this->_actionsClassMap[$actionName . $this->_corsSuffix] = DummyAction::class;
629 8
        }
630 8
        if (!isset($route['path'])) {
631 8
            $route['path'] = $actionName;
632 8
        }
633 8
        $this->_actions[$actionName] = $route;
634 8
    }
635
636
    /**
637
     * Initialize parser.
638
     *
639
     * @param array $config Service options
640
     * @return void
641
     */
642 118
    protected function _initializeParser(array $config)
643
    {
644 118
        if (empty($this->_parserClass) && isset($config['parserClass'])) {
645
            $this->_parserClass = $config['parserClass'];
646
        }
647 118
        $parserClass = Configure::read('Api.parser');
648 118
        if (empty($this->_parserClass) && !empty($parserClass)) {
649 118
            $this->_parserClass = $parserClass;
650 118
        }
651
652 118
        $class = App::className($this->_parserClass, 'Service/RequestParser', 'Parser');
653 118
        if (!class_exists($class)) {
654
            throw new MissingParserException(['class' => $this->_parserClass]);
655
        }
656 118
        $this->_parser = new $class($this);
657 118
    }
658
659
    /**
660
     * Initialize renderer.
661
     *
662
     * @param array $config Service options.
663
     * @return void
664
     */
665 118
    protected function _initializeRenderer(array $config)
666
    {
667 118
        if (empty($this->_rendererClass) && isset($config['rendererClass'])) {
668 13
            $this->_rendererClass = $config['rendererClass'];
669 13
        }
670 118
        $rendererClass = Configure::read('Api.renderer');
671 118
        if (empty($this->_rendererClass) && !empty($rendererClass)) {
672 105
            $this->_rendererClass = $rendererClass;
673 105
        }
674
675 118
        $class = App::className($this->_rendererClass, 'Service/Renderer', 'Renderer');
676 118
        if (!class_exists($class)) {
677
            throw new MissingRendererException(['class' => $this->_rendererClass]);
678
        }
679 118
        $this->_renderer = new $class($this);
680 118
    }
681
}
682