Completed
Push — master ( decfed...00ae50 )
by Narcotic
61:33
created

MainController::indexAction()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 12
cp 0
rs 9.3142
c 0
b 0
f 0
cc 1
eloc 11
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * controller for start page
4
 */
5
6
namespace Graviton\CoreBundle\Controller;
7
8
use Graviton\CoreBundle\Event\HomepageRenderEvent;
9
use Graviton\ProxyBundle\Service\ApiDefinitionLoader;
10
use Graviton\RestBundle\HttpFoundation\LinkHeader;
11
use Graviton\RestBundle\HttpFoundation\LinkHeaderItem;
12
use Graviton\RestBundle\Service\RestUtilsInterface;
13
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\Routing\Router;
17
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
18
19
/**
20
 * MainController
21
 *
22
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
23
 * @license  https://opensource.org/licenses/MIT MIT License
24
 * @link     http://swisscom.ch
25
 */
26
class MainController
27
{
28
    /**
29
     * @var Router
30
     */
31
    private $router;
32
33
    /**
34
     * @var Response
35
     */
36
    private $response;
37
38
    /**
39
     * @var RestUtilsInterface
40
     */
41
    private $restUtils;
42
43
    /**
44
     * @var EventDispatcherInterface
45
     */
46
    private $eventDispatcher;
47
48
    /**
49
     * @var ApiDefinitionLoader
50
     */
51
    private $apiLoader;
52
53
    /**
54
     * @var array
55
     */
56
    private $addditionalRoutes;
57
58
    /**
59
     * @var array
60
     */
61
    private $pathWhitelist;
62
63
    /**
64
     * @var array
65
     */
66
    private $proxySourceConfiguration;
67
68
    /**
69
     * @param Router                   $router                   router
70
     * @param Response                 $response                 prepared response
71
     * @param RestUtilsInterface       $restUtils                rest-utils from GravitonRestBundle
72
     * @param EventDispatcherInterface $eventDispatcher          event dispatcher
73
     * @param ApiDefinitionLoader      $apiLoader                loader for third party api definition
74
     * @param array                    $additionalRoutes         custom routes
75
     * @param array                    $pathWhitelist            serviec path that always get aded to the main page
76
     * @param array                    $proxySourceConfiguration Set of sources to be recognized by the controller
77
     */
78
    public function __construct(
79
        Router $router,
80
        Response $response,
81
        RestUtilsInterface $restUtils,
82
        EventDispatcherInterface $eventDispatcher,
83
        ApiDefinitionLoader $apiLoader,
84
        $additionalRoutes = [],
85
        $pathWhitelist = [],
86
        array $proxySourceConfiguration = []
87
    ) {
88
        $this->router = $router;
89
        $this->response = $response;
90
        $this->restUtils = $restUtils;
91
        $this->eventDispatcher = $eventDispatcher;
92
        $this->apiLoader = $apiLoader;
93
        $this->addditionalRoutes = $additionalRoutes;
94
        $this->pathWhitelist = $pathWhitelist;
95
        $this->proxySourceConfiguration = $proxySourceConfiguration;
96
    }
97
98
    /**
99
     * create simple start page.
100
     *
101
     * @return Response $response Response with result or error
102
     */
103
    public function indexAction()
104
    {
105
        $response = $this->response;
106
107
        $mainPage = new \stdClass();
108
        $mainPage->services = $this->determineServices(
109
            $this->restUtils->getOptionRoutes()
110
        );
111
112
        $mainPage->thirdparty = $this->registerThirdPartyServices();
113
114
        $response->setContent(json_encode($mainPage));
115
        $response->setStatusCode(Response::HTTP_OK);
116
        $response->headers->set('Link', $this->prepareLinkHeader());
117
118
        // todo: make sure, that the correct content type is set.
119
        // todo: this should be covered by a kernel.response event listener?
120
        $response->headers->set('Content-Type', 'application/json');
121
122
        return $response;
123
    }
124
125
    /**
126
     * Determines what service endpoints are available.
127
     *
128
     * @param array $optionRoutes List of routing options.
129
     *
130
     * @return array
131
     */
132
    protected function determineServices(array $optionRoutes)
133
    {
134
        $router = $this->router;
135
        foreach ($this->addditionalRoutes as $route) {
136
            $optionRoutes[$route] = null;
137
        }
138
139
        $services = array_map(
140
            function ($routeName) use ($router) {
141
                $routeParts = explode('.', $routeName);
142
                if (count($routeParts) > 3) {
143
                    list($app, $bundle, $rest, $document) = $routeParts;
144
145
                    $schemaRoute = implode('.', [$app, $bundle, $rest, $document, 'canonicalSchema']);
146
147
                    return [
148
                        '$ref' => $router->generate($routeName, [], UrlGeneratorInterface::ABSOLUTE_URL),
149
                        'profile' => $router->generate($schemaRoute, [], UrlGeneratorInterface::ABSOLUTE_URL),
150
                    ];
151
                }
152
            },
153
            array_keys($optionRoutes)
154
        );
155
156
        $sortArr = [];
157
        foreach ($services as $key => $val) {
158
            if ($this->isRelevantForMainPage($val) && !in_array($val['$ref'], $sortArr)) {
159
                $sortArr[$key] = $val['$ref'];
160
            } else {
161
                unset($services[$key]);
162
            }
163
        }
164
165
        // get additional routes
166
        $additionalRoutes = $this->getAdditionalRoutes($sortArr);
167
168
        $services = array_merge($services, $additionalRoutes);
169
170
        array_multisort($sortArr, SORT_ASC, $services);
171
        return $services;
172
    }
173
174
    /**
175
     * gets the additional routes that can be injected by listeners/subscribers
176
     *
177
     * @param array $sortArr array needed for sorting
178
     *
179
     * @return array additional routes
180
     */
181
    private function getAdditionalRoutes(array &$sortArr)
182
    {
183
        $additionalRoutes = [];
184
        $event = new HomepageRenderEvent();
185
        $routes = $this->eventDispatcher->dispatch(HomepageRenderEvent::EVENT_NAME, $event)->getRoutes();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\EventDispatcher\Event as the method getRoutes() does only exist in the following sub-classes of Symfony\Component\EventDispatcher\Event: Graviton\CoreBundle\Event\HomepageRenderEvent. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
186
187
        if (!empty($routes)) {
188
            $baseRoute = $this->router->match("/");
189
            $baseUrl = $this->router->generate($baseRoute['_route'], [], UrlGeneratorInterface::ABSOLUTE_URL);
190
            foreach ($routes as $route) {
191
                $thisUrl = $baseUrl.$route['$ref'];
192
                $additionalRoutes[] = [
193
                    '$ref' => $thisUrl,
194
                    'profile' => $baseUrl.$route['profile']
195
                ];
196
                $sortArr[$thisUrl] = $thisUrl;
197
            }
198
        }
199
200
        return $additionalRoutes;
201
    }
202
203
    /**
204
     * Prepares the header field containing information about pagination.
205
     *
206
     * @return string
207
     */
208
    protected function prepareLinkHeader()
209
    {
210
        $links = new LinkHeader(array());
211
        $links->add(
212
            new LinkHeaderItem(
213
                $this->router->generate('graviton.core.rest.app.all', array (), UrlGeneratorInterface::ABSOLUTE_URL),
214
                array ('rel' => 'apps', 'type' => 'application/json')
215
            )
216
        );
217
218
        return (string) $links;
219
    }
220
221
    /**
222
     * tells if a service is relevant for the mainpage
223
     *
224
     * @param array $val value of service spec
225
     *
226
     * @return boolean
227
     */
228
    private function isRelevantForMainPage($val)
229
    {
230
        return (substr($val['$ref'], -1) === '/')
231
            || in_array(parse_url($val['$ref'], PHP_URL_PATH), $this->pathWhitelist);
232
    }
233
234
    /**
235
     * Resolves all third party routes and add schema info
236
     *
237
     * @param array $thirdApiRoutes list of all routes from an API
238
     *
239
     * @return array
240
     */
241
    protected function determineThirdPartyServices(array $thirdApiRoutes)
242
    {
243
        $definition = $this->apiLoader;
244
        $mainRoute = $this->router->generate(
245
            'graviton.core.static.main.all',
246
            [],
247
            UrlGeneratorInterface::ABSOLUTE_URL
248
        );
249
        $services = array_map(
250
            function ($apiRoute) use ($mainRoute, $definition) {
251
252
                return array (
253
                    '$ref' => $mainRoute.$apiRoute,
254
                    'profile' => $mainRoute."schema/".$apiRoute."/item",
255
                );
256
            },
257
            $thirdApiRoutes
258
        );
259
260
        return $services;
261
    }
262
263
    /**
264
     * Finds configured external apis to be exposed via G2.
265
     *
266
     * @return array
267
     */
268
    private function registerThirdPartyServices()
269
    {
270
        $services = [];
271
272
        // getenv()... it's a workaround for run all tests on travis! will be removed!
273
        if (getenv('USER') !== 'travis' && getenv('HAS_JOSH_K_SEAL_OF_APPROVAL') !== true) {
274
            foreach (array_keys($this->proxySourceConfiguration) as $source) {
275
                foreach ($this->proxySourceConfiguration[$source] as $thirdparty => $option) {
276
                    $this->apiLoader->resetDefinitionLoader();
277
                    $this->apiLoader->setOption($option);
278
                    $this->apiLoader->addOptions($this->decideApiAndEndpoint($option));
279
                    $services[$thirdparty] = $this->determineThirdPartyServices(
280
                        $this->apiLoader->getAllEndpoints(false, true)
281
                    );
282
                }
283
            }
284
        }
285
286
        return $services;
287
    }
288
289
    /**
290
     * get API name and endpoint from the url (third party API)
291
     *
292
     * @param array $config Configuration information ['prefix', 'serviceEndpoint']
293
     *
294
     * @return array
295
     */
296
    protected function decideApiAndEndpoint(array $config)
297
    {
298
        if (array_key_exists('serviceEndpoint', $config)) {
299
            return array (
300
                "apiName" => $config['prefix'],
301
                "endpoint" => $config['serviceEndpoint'],
302
            );
303
        }
304
305
        return [];
306
    }
307
308
    /**
309
     * Return OPTIONS results.
310
     *
311
     * @param Request $request Current http request
312
     *
313
     * @return Response $response Result of the action
314
     */
315
    public function optionsAction(Request $request)
316
    {
317
        $response = $this->response;
318
        $response->setStatusCode(Response::HTTP_NO_CONTENT);
319
        $request->attributes->set('corsMethods', 'GET, OPTIONS');
320
321
        return $response;
322
    }
323
}
324