Passed
Push — master ( 9ed9fd...045e81 )
by Evgeny
04:22
created

Service   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 629
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 16

Test Coverage

Coverage 89.92%

Importance

Changes 0
Metric Value
dl 0
loc 629
ccs 214
cts 238
cp 0.8992
rs 2.2428
c 0
b 0
f 0
wmc 74
lcom 2
cbo 16

26 Methods

Rating   Name   Duplication   Size   Complexity  
F __construct() 0 54 16
A controller() 0 11 2
A name() 0 9 2
A version() 0 9 2
A initialize() 0 7 2
A parser() 0 9 2
A request() 0 10 2
A routes() 0 6 1
A _routesWrapper() 0 10 1
A resetRoutes() 0 4 1
A loadRoutes() 0 12 3
B routerDefaultOptions() 0 26 5
A routeUrl() 0 6 1
A routeReverse() 0 10 2
B dispatch() 0 20 6
B buildAction() 0 34 6
A parseRoute() 0 6 1
A baseUrl() 0 14 2
A parent() 0 9 2
A buildActionClass() 0 6 1
A _actionOptions() 0 13 1
A result() 0 11 3
A respond() 0 17 3
A response() 0 10 2
A renderer() 0 9 2
A mapAction() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like Service often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Service, and based on these observations, apply Extract Interface, too.

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\Controller\Controller;
23
use Cake\Core\App;
24
use Cake\Core\Configure;
25
use Cake\Datasource\Exception\RecordNotFoundException;
26
use Cake\Http\Client\Response;
27
use Cake\Routing\RouteBuilder;
28
use Cake\Utility\Hash;
29
use Cake\Utility\Inflector;
30
use Exception;
31
32
/**
33
 * Class Service
34
 */
35
abstract class Service
36
{
37
38
    /**
39
     * Actions routes description map, indexed by action name.
40
     *
41
     * @var array
42
     */
43
    protected $_actions = [];
44
45
    /**
46
     * Actions classes map, indexed by action name.
47
     *
48
     * @var array
49
     */
50
    protected $_actionsClassMap = [];
51
52
    /**
53
     * Service url acceptable extensions list.
54
     *
55
     * @var array
56
     */
57
    protected $_extensions = ['json'];
58
59
    /**
60
     *
61
     *
62
     * @var string
63
     */
64
    protected $_routePrefix = '';
65
66
    /**
67
     * Service name
68
     *
69
     * @var string
70
     */
71
    protected $_name = null;
72
73
    /**
74
     * Controller instance.
75
     *
76
     * @var Controller
77
     */
78
    protected $_controller = null;
79
80
    /**
81
     * Service version.
82
     *
83
     * @var int
84
     */
85
    protected $_version;
86
87
88
    /**
89
     * Parser class to process the HTTP request.
90
     *
91
     * @var BaseParser
92
     */
93
    protected $_parser;
94
95
    /**
96
     * Renderer class to build the HTTP response.
97
     *
98
     * @var BaseRenderer
99
     */
100
    protected $_renderer;
101
102
    /**
103
     * The parser class.
104
     *
105
     * @var string
106
     */
107
    protected $_parserClass = null;
108
109
    /**
110
     * The Renderer class.
111
     *
112
     * @var string
113
     */
114
    protected $_rendererClass = null;
115
116
    /**
117
     * Dependent services names list
118
     *
119
     * @var array<string>
120
     */
121
    protected $_innerServices = [];
122
123
    /**
124
     * Parent service instance.
125
     *
126
     * @var Service
127
     */
128
    protected $_parentService;
129
130
    /**
131
     * Service Action Result object.
132
     *
133
     * @var Result
134
     */
135
    protected $_result;
136
    
137
	protected $_baseUrl;
138
    protected $_request;
139
    protected $_response;
140
141
    protected $_corsSuffix = '_cors';
142
143
//    protected $_identified = false;
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% 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...
144
145
    /**
146
     * Service constructor.
147
     *
148
     * @param array $config Service configuration.
149
     */
150 118
    public function __construct(array $config = [])
151
    {
152 118
        if (isset($config['controller'])) {
153 28
            $this->controller($config['controller']);
154 28
        }
155 118
        if (isset($config['request'])) {
156 88
            $this->request($config['request']);
157 88
        }
158 118
        if (isset($config['response'])) {
159 88
            $this->response($config['response']);
160 88
        }
161 118
        if (isset($config['baseUrl'])) {
162 60
            $this->_baseUrl = $config['baseUrl'];
163 60
        }
164 118
        if (isset($config['service'])) {
165 94
            $this->name($config['service']);
166 94
        }
167 118
        if (isset($config['version'])) {
168
            $this->version($config['version']);
169
        }
170 118
        if (isset($config['parserClass'])) {
171
            $this->_parserClass = $config['parserClass'];
172
        }
173 118
        $parserClass = Configure::read('Api.parser');
174 118
        if (empty($this->_parserClass) && !empty($parserClass)) {
175 118
            $this->_parserClass = $parserClass;
176 118
        }
177
178 118
        if (isset($config['rendererClass'])) {
179 13
            $this->_rendererClass = $config['rendererClass'];
180 13
        }
181 118
        $rendererClass = Configure::read('Api.renderer');
182 118
        if (empty($this->_rendererClass) && !empty($rendererClass)) {
183 105
            $this->_rendererClass = $rendererClass;
184 105
        }
185
186 118
        if (isset($config['classMap'])) {
187 1
            $this->_actionsClassMap = Hash::merge($this->_actionsClassMap, $config['classMap']);
188 1
        }
189
190 118
        $this->initialize();
191
192 118
        $class = App::className($this->_parserClass, 'Service/RequestParser', 'Parser');
193 118
        if (!class_exists($class)) {
194
            throw new MissingParserException(['class' => $this->_parserClass]);
195
        }
196 118
        $this->_parser = new $class($this);
197
198 118
        $class = App::className($this->_rendererClass, 'Service/Renderer', 'Renderer');
199 118
        if (!class_exists($class)) {
200
            throw new MissingRendererException(['class' => $this->_rendererClass]);
201
        }
202 118
        $this->_renderer = new $class($this);
203 118
    }
204
205
    /**
206
     * Get and set controller associated with service,
207
     *
208
     * @param \Cake\Controller\Controller $controller Controller.
209
     * @return \Cake\Controller\Controller
210
     */
211 28
    public function controller(Controller $controller = null)
212
    {
213 28
        if ($controller === null) {
214 1
            return $this->_controller;
215
        }
216 28
        $this->_controller = $controller;
217 28
		$this->_request = $controller->request;
218 28
		$this->_response = $controller->response;
219
220 28
        return $this->_controller;
221
    }
222
223
    /**
224
     * Get and set service name.
225
     *
226
     * @param string $name Service name.
227
     * @return string
228
     */
229 118
    public function name($name = null)
230
    {
231 118
        if ($name === null) {
232 117
            return $this->_name;
233
        }
234 118
        $this->_name = $name;
235
236 118
        return $this->_name;
237
    }
238
239
    /**
240
     * Get and set service version.
241
     *
242
     * @param int $version Version number.
243
     * @return int
244
     */
245 80
    public function version($version = null)
246
    {
247 80
        if ($version === null) {
248 80
            return $this->_version;
249
        }
250
        $this->_version = $version;
251
252
        return $this->_version;
253
    }
254
255
    /**
256
     * Initialize method
257
     *
258
     * @return void
259
     */
260 118
    public function initialize()
261
    {
262 118
        if ($this->_name === null) {
263 41
            $className = (new \ReflectionClass($this))->getShortName();
264 41
            $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...
265 41
        }
266 118
    }
267
268
    /**
269
     * Service parser configuration method.
270
     *
271
     * @param BaseParser $parser A Parser instance.
272
     * @return BaseParser
273
     */
274 51
    public function parser(BaseParser $parser = null)
275
    {
276 51
        if ($parser === null) {
277 51
            return $this->_parser;
278
        }
279
        $this->_parser = $parser;
280
281
        return $this->_parser;
282
    }
283
284
    /**
285
     * @return \Cake\Network\Request
286
     */
287 102
    public function request($request = null)
288
    {
289 102
        if ($request === null) {
290 101
            return $this->_request;
291
        }
292
293 88
        $this->_request = $request;
294
295 88
        return $this->_request;
296
    }
297
298
    /**
299
     * Get the service route scopes and their connected routes.
300
     *
301
     * @return array
302
     */
303 3
    public function routes()
304
    {
305
        return $this->_routesWrapper(function () {
306 3
            return ApiRouter::routes();
307 3
        });
308
    }
309
310
    /**
311
     * @param callable $callable Wrapped router instance.
312
     * @return mixed
313
     */
314 64
    protected function _routesWrapper(callable $callable)
315
    {
316 64
        $this->resetRoutes();
317 64
        $this->loadRoutes();
318 64
        ApiRouter::$initialized = true;
319 64
        $result = $callable();
320 63
        $this->resetRoutes();
321
322 63
        return $result;
323
    }
324
325
    /**
326
     * Reset to default application routes.
327
     *
328
     * @return void
329
     */
330 64
    public function resetRoutes()
331
    {
332 64
        ApiRouter::reload();
333 64
    }
334
335
    /**
336
     * Initialize service level routes
337
     *
338
     * @return void
339
     */
340 8
    public function loadRoutes()
341
    {
342 8
        $defaultOptions = $this->routerDefaultOptions();
343
        ApiRouter::scope('/', $defaultOptions, function (RouteBuilder $routes) use ($defaultOptions) {
344 8
            if (is_array($this->_extensions)) {
345 8
                $routes->extensions($this->_extensions);
346 8
            }
347 8
            if (!empty($defaultOptions['map'])) {
348 8
                $routes->resources($this->name(), $defaultOptions);
349 8
            }
350 8
        });
351 8
    }
352
353
    /**
354
     * Build router settings.
355
     * This implementation build action map for resource routes based on Service actions.
356
     *
357
     * @return array
358
     */
359 63
    public function routerDefaultOptions()
360
    {
361 63
        $mapList = [];
362 63
        foreach ($this->_actions as $alias => $map) {
363 42
            if (is_numeric($alias)) {
364
                $alias = $map;
365
                $map = [];
366
            }
367 42
            $mapCors = false;
368 42
            if (!empty($map['mapCors'])) {
369 8
                $mapCors = $map['mapCors'];
370 8
                unset($map['mapCors']);
371 8
            }
372 42
            $mapList[$alias] = $map;
373 42
            $mapList[$alias] += ['method' => 'GET', 'path' => '', 'action' => $alias];
374 42
            if ($mapCors) {
375 8
                $map['method'] = 'OPTIONS';
376 8
                $map += ['path' => '', 'action' => $alias . $this->_corsSuffix];
377 8
                $mapList[$alias . $this->_corsSuffix] = $map;
378 8
            }
379 63
        }
380
381
        return [
382
            'map' => $mapList
383 63
        ];
384
    }
385
386
    /**
387
     * Finds URL for specified action.
388
     *
389
     * Returns an URL pointing to a combination of controller and action.
390
     *
391
     * @param string|array|null $route An array specifying any of the following:
392
     *   'controller', 'action', 'plugin' additionally, you can provide routed
393
     *   elements or query string parameters. If string it can be name any valid url
394
     *   string.
395
     * @return string Full translated URL with base path.
396
     * @throws \Cake\Core\Exception\Exception When the route name is not found
397
     */
398
    public function routeUrl($route)
399
    {
400
        return $this->_routesWrapper(function () use ($route) {
401
            return ApiRouter::url($route);
402
        });
403
    }
404
405
    /**
406
     * Reverses a parsed parameter array into a string.
407
     *
408
     * @param \Cake\Network\Request|array $params The params array or
409
     *     Cake\Network\Request object that needs to be reversed.
410
     * @return string The string that is the reversed result of the array
411
     */
412 13
    public function routeReverse($params)
413
    {
414
        return $this->_routesWrapper(function () use ($params) {
415
            try {
416 13
                return ApiRouter::reverse($params);
417 1
            } catch (Exception $e) {
418 1
                return null;
419
            }
420 13
        });
421
    }
422
423
    /**
424
     * Dispatch service call.
425
     *
426
     * @return \CakeDC\Api\Service\Action\Result
427
     */
428 56
    public function dispatch()
429
    {
430
        try {
431 56
            $action = $this->buildAction();
432 56
            $result = $action->process();
433 52
            $this->result()->data($result);
434 52
            $this->result()->code(200);
435 56
        } catch (RecordNotFoundException $e) {
436 6
            $this->result()->code(404);
437 6
            $this->result()->exception($e);
438 8
        } catch (Exception $e) {
439 2
            $code = $e->getCode();
440 2
            if (!is_int($code) || $code < 100 || $code >= 600) {
441
                $this->result()->code(500);
442
            }
443 2
            $this->result()->exception($e);
444
        }
445
446 56
        return $this->result();
447
    }
448
449
    /**
450
     * Build action instance
451
     *
452
     * @return \CakeDC\Api\Service\Action\Action
453
     * @throws Exception
454
     */
455 63
    public function buildAction()
456
    {
457 63
        $route = $this->parseRoute($this->baseUrl());
458 62
        if (empty($route)) {
459
            throw new MissingActionException('Invalid Action Route:' . $this->baseUrl()); // InvalidActionException
460
        }
461 62
        $service = null;
462 62
        $serviceName = Inflector::underscore($route['controller']);
463 62
        if ($serviceName == $this->name()) {
464 53
            $service = $this;
465 53
        }
466 62
        if (in_array($serviceName, $this->_innerServices)) {
467
            $options = [
468 9
                'version' => $this->version(),
469 9
                'request' => $this->request(),
470 9
                'response' => $this->response(),
471 9
            ];
472 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 462 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...
473 9
            $service->parent($this);
474 9
        }
475 62
        $action = $route['action'];
476 62
        list($namespace, $serviceClass) = namespaceSplit(get_class($service));
477 62
        $actionPrefix = substr($serviceClass, 0, -7);
478 62
        $actionClass = $namespace . '\\Action\\' . $actionPrefix . Inflector::camelize($action) . 'Action';
479 62
        if (class_exists($actionClass)) {
480 2
            return $service->buildActionClass($actionClass, $route);
481
        }
482 60
        if (array_key_exists($action, $this->_actionsClassMap)) {
483 60
            $actionClass = $this->_actionsClassMap[$action];
484
485 60
            return $service->buildActionClass($actionClass, $route);
486
        }
487
        throw new MissingActionException(['class' => $actionClass]);
488
    }
489
490
    /**
491
     * Parses given URL string. Returns 'routing' parameters for that URL.
492
     *
493
     * @param string $url URL to be parsed
494
     * @return array Parsed elements from URL
495
     * @throws \Cake\Routing\Exception\MissingRouteException When a route cannot be handled
496
     */
497
    public function parseRoute($url)
498
    {
499 63
        return $this->_routesWrapper(function () use ($url) {
500 63
            return ApiRouter::parse($url);
501 63
        });
502
    }
503
504
    /**
505
     * Build base url
506
     *
507
     * @return string
508
     */
509 64
    public function baseUrl()
510
    {
511 64
		if (!empty($this->_baseUrl)) {
512 60
			return $this->_baseUrl;
513
		}
514
		
515
        // $passed = $this->controller()->request->params['pass'];
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% 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...
516 4
        $result = '/' . $this->name();
517
        // if (!empty($passed)) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
75% 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...
518
            // $result .= '/' . join('/', $passed);
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% 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...
519
        // }
520
521 4
        return $result;
522
    }
523
524
    /**
525
     * Parent service get and set methods
526
     *
527
     * @param Service $service Parent Service instance.
528
     * @return Service
529
     */
530 54
    public function parent(Service $service = null)
531
    {
532 54
        if ($service === null) {
533 54
            return $this->_parentService;
534
        }
535 9
        $this->_parentService = $service;
536
537 9
        return $this->_parentService;
538
    }
539
540
    /**
541
     * Build action class
542
     *
543
     * @param string $class Class name.
544
     * @param array $route Activated route.
545
     * @return mixed
546
     */
547 63
    public function buildActionClass($class, $route)
548
    {
549 63
        $instance = new $class($this->_actionOptions($route));
550
551 63
        return $instance;
552
    }
553
554
    /**
555
     * Action constructor options.
556
     *
557
     * @param array $route Activated route.
558
     * @return array
559
     */
560 63
    protected function _actionOptions($route)
561
    {
562 63
        $actionName = $route['action'];
563
564
        $options = [
565 63
            'name' => $actionName,
566 63
            'service' => $this,
567 63
            'route' => $route,
568 63
        ];
569 63
        $options += (new ConfigReader())->actionOptions($this->name(), $actionName, $this->version());
570
571 63
        return $options;
572
    }
573
574
    /**
575
     * @return \CakeDC\Api\Service\Action\Result
576
     */
577 56
    public function result()
578
    {
579 56
        if ($this->_parentService !== null) {
580 2
            return $this->_parentService->result();
581
        }
582 56
        if ($this->_result === null) {
583 56
            $this->_result = new Result();
584 56
        }
585
586 56
        return $this->_result;
587
    }
588
589
    /**
590
     * Fill up response and stop execution.
591
     *
592
     * @param Result $result A Result instance.
593
     * @return Response
594
     */
595 56
    public function respond($result = null)
596
    {
597 56
        if ($result === null) {
598
            $result = $this->result();
599
        }
600 56
        $this->response()
601 56
             ->statusCode($result->code());
602 56
        if ($result->exception() !== null) {
603 8
            $this->renderer()
604 8
                 ->error($result->exception());
605 8
        } else {
606 52
            $this->renderer()
607 52
                 ->response($result);
608
        }
609
610 56
        return $this->response();
611
    }
612
613
    /**
614
     * @return \Cake\Network\Response
615
     */
616 110
    public function response($response = null)
617
    {
618 110
        if ($response === null) {
619 109
            return $this->_response;
620
        }
621
622 88
        $this->_response = $response;
623
624 88
        return $this->_response;
625
    }
626
627
    /**
628
     * Service renderer configuration method.
629
     *
630
     * @param BaseRenderer $renderer A Renderer instance
631
     * @return BaseRenderer
632
     */
633 68
    public function renderer(BaseRenderer $renderer = null)
634
    {
635 68
        if ($renderer === null) {
636 68
            return $this->_renderer;
637
        }
638
        $this->_renderer = $renderer;
639
640
        return $this->_renderer;
641
    }
642
643
    /**
644
     * Define action config.
645
     *
646
     * @param string $actionName Action name.
647
     * @param string $className Class name.
648
     * @param array $route Route config.
649
     * @return void
650
     */
651 8
    public function mapAction($actionName, $className, $route)
652
    {
653 8
        $route += ['mapCors' => false];
654 8
        $this->_actionsClassMap[$actionName] = $className;
655 8
        if ($route['mapCors']) {
656 8
            $this->_actionsClassMap[$actionName . $this->_corsSuffix] = DummyAction::class;
657 8
        }
658 8
        if (!isset($route['path'])) {
659 8
            $route['path'] = $actionName;
660 8
        }
661 8
        $this->_actions[$actionName] = $route;
662 8
    }
663
}
664