Completed
Push — master ( 48cc3d...5640fa )
by Craig
12:32 queued 05:19
created

RouteLoader::prependBundlePrefix()   D

Complexity

Conditions 9
Paths 18

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 19
c 0
b 0
f 0
nc 18
nop 2
dl 0
loc 31
rs 4.909
1
<?php
2
3
/*
4
 * This file is part of the Zikula package.
5
 *
6
 * Copyright Zikula Foundation - http://zikula.org/
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Zikula\RoutesModule\Routing;
13
14
use Doctrine\Common\Persistence\ObjectManager;
15
use Symfony\Component\Config\Loader\Loader;
16
use Symfony\Component\Routing\Route;
17
use Symfony\Component\Routing\RouteCollection;
18
use Zikula\Bundle\CoreBundle\HttpKernel\ZikulaHttpKernelInterface;
19
use Symfony\Component\Translation\TranslatorInterface;
20
use Zikula\Core\AbstractBundle;
21
use Zikula\Core\AbstractModule;
22
use Zikula\RoutesModule\Helper\SanitizeHelper;
23
use Zikula\ThemeModule\AbstractTheme;
24
25
/**
26
 * Custom loader following http://symfony.com/doc/current/routing/custom_route_loader.html
27
 */
28
class RouteLoader extends Loader
29
{
30
    /**
31
     * @var bool
32
     */
33
    private $loaded = false;
34
35
    /**
36
     * @var ObjectManager
37
     */
38
    private $objectManager;
39
40
    /**
41
     * @var TranslatorInterface
42
     */
43
    private $translator;
44
45
    /**
46
     * @var ZikulaHttpKernelInterface
47
     */
48
    private $kernel;
49
50
    /**
51
     * @var SanitizeHelper
52
     */
53
    private $sanitizeHelper;
54
55
    /**
56
     * @var string
57
     */
58
    private $locale;
59
60
    /**
61
     * RouteLoader constructor.
62
     *
63
     * @param ObjectManager             $objectManager  Doctrine object manager
64
     * @param TranslatorInterface       $translator     Translator
65
     * @param ZikulaHttpKernelInterface $kernel         Zikula kernel
66
     * @param SanitizeHelper            $sanitizeHelper Sanitize helper
67
     * @param string                    $locale
68
     */
69
    public function __construct(
70
        ObjectManager $objectManager,
71
        TranslatorInterface $translator,
72
        ZikulaHttpKernelInterface $kernel,
73
        SanitizeHelper $sanitizeHelper,
74
        $locale)
75
    {
76
        $this->objectManager = $objectManager;
77
        $this->kernel = $kernel;
78
        $this->translator = $translator;
79
        $this->sanitizeHelper = $sanitizeHelper;
80
        $this->locale = $locale;
81
    }
82
83
    /**
84
     * Finds all routes of all Zikula themes and modules.
85
     *
86
     * @return RouteCollection[]
87
     */
88
    private function findAll()
89
    {
90
        $modules = $this->kernel->getModules();
91
        $themes = $this->kernel->getThemes();
92
        $bundles = array_merge($modules, $themes);
93
94
        $topRouteCollection = new RouteCollection();
95
        $middleRouteCollection = new RouteCollection();
96
        $bottomRouteCollection = new RouteCollection();
97
        foreach ($bundles as $bundle) {
98
            list ($currentMiddleRouteCollection, $currentTopRouteCollection, $currentBottomRouteCollection) = $this->find($bundle);
99
            $middleRouteCollection->addCollection($currentMiddleRouteCollection);
100
            $topRouteCollection->addCollection($currentTopRouteCollection);
101
            $bottomRouteCollection->addCollection($currentBottomRouteCollection);
102
        }
103
104
        return [$middleRouteCollection, $topRouteCollection, $bottomRouteCollection];
105
    }
106
107
    /**
108
     * Load routes of the specified module from the module's configuration file.
109
     *
110
     * @param AbstractBundle $bundle
111
     *
112
     * @return RouteCollection[]
113
     */
114
    private function find(AbstractBundle $bundle)
115
    {
116
        try {
117
            $path = $this->kernel->locateResource($bundle->getRoutingConfig());
118
        } catch (\InvalidArgumentException $e) {
119
            // Routing file does not exist (e.g. because the bundle could not be located).
120
            return [new RouteCollection(), new RouteCollection(), new RouteCollection()];
121
        }
122
        $name = $bundle->getName();
123
124
        $topRouteCollection = new RouteCollection();
125
        $middleRouteCollection = new RouteCollection();
126
        $bottomRouteCollection = new RouteCollection();
127
128
        /**
129
         * These are all routes of the module, as loaded by Symfony.
130
         * @var RouteCollection $routeCollection
131
         */
132
        $routeCollection = $this->import($path);
133
134
        // Add all resources from the imported route collection to the middleRouteCollection.
135
        // The actual collection (top, middle, bottom) to add the resources too does not matter,
136
        // they just must be added to one of them, so that they don't get lost.
137
        foreach ($routeCollection->getResources() as $resource) {
138
            $middleRouteCollection->addResource($resource);
139
        }
140
        // It would be great to auto-reload routes here if the module version changes or a module is uninstalled.
141
        // @todo this seems to be possible in Symfony 2.8+ - check these out:
142
        // - https://github.com/symfony/symfony/issues/7176
143
        // - https://github.com/symfony/symfony/pull/15738
144
        // - https://github.com/symfony/symfony/pull/15692
145
        // $routeCollection->addResource(new ZikulaResource())
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% 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...
146
147
        /** @var Route $route */
148
        foreach ($routeCollection as $oldRouteName => $route) {
149
//          set break here with $oldRouteName == 'zikula_routesmodule_route_renew'
150
            $this->fixRequirements($route);
151
            $this->prependBundlePrefix($route, $bundle);
152
            list($type, $func) = $this->setZikulaDefaults($route, $bundle, $name);
153
            $routeName = $this->getRouteName($oldRouteName, $name, $type, $func);
154
155
            if ($route->hasOption('zkPosition')) {
156
                switch ($route->getOption('zkPosition')) {
157
                    case 'top':
158
                        $topRouteCollection->add($routeName, $route);
159
                        break;
160
                    case 'bottom':
161
                        $bottomRouteCollection->add($routeName, $route);
162
                        break;
163
                    default:
164
                        throw new \RuntimeException('Unknown route position. Got "' . $route->getOption('zkPosition') . '", expected "top" or "bottom"');
165
                }
166
            } else {
167
                $middleRouteCollection->add($routeName, $route);
168
            }
169
        }
170
171
        return [$middleRouteCollection, $topRouteCollection, $bottomRouteCollection];
172
    }
173
174
    public function load($resource, $type = null)
175
    {
176
        if (true === $this->loaded) {
177
            throw new \RuntimeException('Do not add the "zikularoutesmodule" loader twice');
178
        }
179
        unset($type);
180
181
        $routeCollection = new RouteCollection();
182
183
        list ($newRouteCollection, $topRouteCollection, $bottomRouteCollection) = $this->findAll();
184
185
        $routeCollection->addCollection($topRouteCollection);
186
187
//        try {
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% 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...
188
//            $routes = $this->objectManager->getRepository('ZikulaRoutesModule:RouteEntity')->findBy([], ['group' => 'ASC', 'sort' => 'ASC']);
189
//        } catch (DBALException $e) {
190
//            // It seems like the module is not yet installed. Fail silently.
191
//            return $routeCollection;
192
//        }
193
//
194
//        if (!empty($routes)) {
195
//            /**
196
//             * @var \Zikula\RoutesModule\Entity\RouteEntity $dbRoute
197
//             */
198
//            foreach ($routes as $dbRoute) {
199
//                // Add modname, type and func to the route's default values.
200
//                $defaults = $dbRoute->getDefaults();
201
//                $defaults['_zkModule'] = $dbRoute->getBundle();
202
//                list (, $type) = $this->sanitizeHelper->sanitizeController($dbRoute->getController());
203
//                list (, $func) = $this->sanitizeHelper->sanitizeAction($dbRoute->getAction());
204
//                $defaults['_zkType'] = $type;
205
//                $defaults['_zkFunc'] = $func;
206
//                // @todo @cmfcmf when reimplementing loading routews from DB, see #2593 (i.e. ucfirst problems)
207
//                $defaults['_controller'] = $dbRoute->getBundle() . ":" . ucfirst($type) . ":" . ucfirst($func);
208
//
209
//                // We have to prepend the bundle prefix (see detailed description in docblock of prependBundlePrefix() method).
210
//                $options = $dbRoute->getOptions();
211
//                $prependBundle = !isset($GLOBALS['translation_extract_routes']) && isset($options['i18n']) && !$options['i18n'];
212
//                if ($prependBundle) {
213
//                    $path = $dbRoute->getPathWithBundlePrefix();
214
//                } else {
215
//                    $path = $dbRoute->getPath();
216
//                }
217
//
218
//                $this->fixRequirements($dbRoute);
219
//
220
//                $route = new Route(
221
//                    $path,
222
//                    $defaults,
223
//                    $dbRoute->getRequirements(),
224
//                    $options,
225
//                    $dbRoute->getHost(),
226
//                    $dbRoute->getSchemes(),
227
//                    $dbRoute->getMethods(),
228
//                    $dbRoute->getCondition()
229
//                );
230
//
231
//                $routeCollection->add($dbRoute->getName(), $route);
232
//            }
233
//        }
234
        $routeCollection->addCollection($newRouteCollection);
235
        $routeCollection->addCollection($bottomRouteCollection);
236
237
        $this->loaded = true;
238
239
        return $routeCollection;
240
    }
241
242
    /**
243
     * Sets some Zikula-specific defaults for the routes.
244
     *
245
     * @param Route          $route The route instance
246
     * @param AbstractBundle $bundle The bundle
247
     * @param string         $bundleName The bundle's name
248
     *
249
     * @return array The legacy $type and $func parameters
250
     */
251
    private function setZikulaDefaults(Route $route, AbstractBundle $bundle, $bundleName)
252
    {
253
        $defaults = $route->getDefaults();
254
255
        $defaults['_zkBundle'] = $bundleName;
256
        if ($bundle instanceof AbstractModule) {
257
            $defaults['_zkModule'] = $bundleName;
258
        } else if ($bundle instanceof AbstractTheme) {
259
            $defaults['_zkTheme'] = $bundleName;
260
        }
261
262
        $controller = $this->sanitizeController($bundleName, $defaults['_controller']);
263
        $controller = explode(':', $controller);
264
        $defaults['_zkType'] = $type = lcfirst($controller[1]);
265
        $defaults['_zkFunc'] = $func = lcfirst($controller[2]);
266
        $defaults['_controller'] = $bundleName . ':' . $controller[1] . ':' . $func;
267
268
        $route->setDefaults($defaults);
269
270
        return [$type, $func];
271
    }
272
273
    /**
274
     * Removes some deprecated requirements which cause depreciation notices.
275
     *
276
     * @param Route $route
277
     *
278
     * @todo Remove when Symfony 3.0 is used.
279
     */
280
    private function fixRequirements(Route $route)
281
    {
282
        $requirements = $route->getRequirements();
283
        if (isset($requirements['_method'])) {
284
            unset($requirements['_method']);
285
        }
286
        if (isset($requirements['_scheme'])) {
287
            unset($requirements['_scheme']);
288
        }
289
        $route->setRequirements($requirements);
290
    }
291
292
    /**
293
     * Prepends the bundle prefix to the route.
294
     *
295
     * @param Route          $route
296
     * @param AbstractBundle $bundle
297
     *
298
     * We have to prepend the bundle prefix if
299
     * - routes are _not_ currently extracted via the command line and
300
     * - the route has i18n set to false.
301
     * This is because when extracting the routes, a bundle author only wants to translate the bare route
302
     * patterns, without a redundant and potentially customized bundle prefix in front of them.
303
     * If i18n is set to true, Zikula's customized pattern generation strategy will take care of it.
304
     * See Zikula\RoutesModule\Translation\ZikulaPatternGenerationStrategy
305
     */
306
    private function prependBundlePrefix(Route $route, AbstractBundle $bundle)
0 ignored issues
show
Coding Style introduced by
prependBundlePrefix uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
307
    {
308
        $prefix = '';
309
        $options = $route->getOptions();
310
        $prependBundle = !isset($GLOBALS['translation_extract_routes']) && isset($options['i18n']) && !$options['i18n'];
311
        if (!$prependBundle || (isset($options['zkNoBundlePrefix']) && $options['zkNoBundlePrefix'])) {
312
            return;
313
        }
314
315
        // get url from MetaData first. May be empty.
316
        $untranslatedPrefix = $bundle->getMetaData()->getUrl(false);
317
        if (empty($untranslatedPrefix)) {
318
            // @todo remove in 2.0
319
            try {
320
                // MetaData will be empty for extensions not Spec-2.0. Try to get from modinfo.
321
                // this calls the DB which is not available during install.
322
                $modinfo = \ModUtil::getInfoFromName($bundle->getName());
323
                $prefix = $modinfo['url'];
324
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
325
            }
326
        } else {
327
            if ($this->translator->getCatalogue($this->locale)->has($untranslatedPrefix, strtolower($bundle->getName()))) {
328
                $prefix = $this->translator->trans(/** @Ignore */$untranslatedPrefix, [], strtolower($bundle->getName()), $this->locale);
329
            } else {
330
                $prefix = $untranslatedPrefix;
331
            }
332
        }
333
334
        $path = '/' . $prefix . $route->getPath();
335
        $route->setPath($path);
336
    }
337
338
    /**
339
     * Converts the controller identifier into a unified form.
340
     *
341
     * @param string $bundleName The name of the bundle
342
     * @param string $controllerString The given controller identifier
343
     *
344
     * @return string The controller identifier in a Bundle:Type:func form
345
     */
346
    private function sanitizeController($bundleName, $controllerString)
347
    {
348
        if (0 === preg_match('#^(.*?\\\\Controller\\\\(.+)Controller)::(.+)Action$#', $controllerString, $match)) {
349
            return $controllerString;
350
        }
351
352
        // Bundle:controller:action
353
        return $bundleName . ':' . $match[2] . ':' . $match[3];
354
    }
355
356
    /**
357
     * Generates the route's new name.
358
     *
359
     * @param string $oldRouteName The old route name
360
     * @param string $bundleName   The bundle name
361
     * @param string $type         The legacy type
362
     * @param string $func         The legacy func
363
     *
364
     * @return string The route's new name
365
     */
366
    private function getRouteName($oldRouteName, $bundleName, $type, $func)
367
    {
368
        $suffix = '';
369
        $lastPart = substr($oldRouteName, strrpos($oldRouteName, '_'));
370
        if (is_numeric($lastPart)) {
371
            // If the last part of the old route name is numeric, also append it to the new route name.
372
            // This allows multiple routes for the same action.
373
            $suffix = '_' . $lastPart;
374
        }
375
        return strtolower($bundleName . '_' . $type . '_' . $func) . $suffix;
376
    }
377
378
    /**
379
     * Checks whether this route loader supports a given route type or not.
380
     *
381
     * @return boolean
382
     */
383
    public function supports($resource, $type = null)
384
    {
385
        return 'zikularoutesmodule' === $type;
386
    }
387
}
388