Completed
Push — master ( 62946e...cfb4fd )
by
unknown
07:23
created

Service::_actionOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

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