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