Passed
Pull Request — master (#9)
by Pavel
11:27
created

RpcControllerNameParser   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 164
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 3.61%

Importance

Changes 0
Metric Value
dl 0
loc 164
c 0
b 0
f 0
wmc 21
lcom 1
cbo 2
ccs 3
cts 83
cp 0.0361
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B parse() 0 60 7
B build() 0 23 4
A guessControllerClassName() 0 4 1
A guessActionString() 0 4 1
A getDefaultControllerPattern() 0 4 1
B findAlternative() 0 25 6
1
<?php
2
3
namespace Bankiru\Api\Rpc\Controller;
4
5
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
6
use Symfony\Component\HttpKernel\KernelInterface;
7
8
/**
9
 * ControllerNameParser converts controller from the short notation a:b:c
10
 * (BlogBundle:Post:index) to a fully-qualified class::method string
11
 * (Bundle\BlogBundle\Rpc\PostController::indexAction).
12
 *
13
 * @author Fabien Potencier <[email protected]>
14
 * @author Pavel Batanov <[email protected]>
15
 */
16
class RpcControllerNameParser implements ControllerNameParser
17
{
18
    protected $kernel;
19
20
    /**
21
     * Constructor.
22
     *
23
     * @param KernelInterface $kernel A KernelInterface instance
24
     */
25 8
    public function __construct(KernelInterface $kernel)
26
    {
27 8
        $this->kernel = $kernel;
28 8
    }
29
30
    /** {@inheritdoc} */
31
    public function parse($controller)
32
    {
33
        $originalController = $controller;
34
        if (3 !== count($parts = explode(':', $controller))) {
35
            throw new \InvalidArgumentException(
36
                sprintf('The "%s" controller is not a valid "a:b:c" controller string.', $controller)
37
            );
38
        }
39
40
        list($bundle, $controller, $action) = $parts;
41
        $controller = str_replace('/', '\\', $controller);
42
        $bundles    = [];
43
44
        try {
45
            // this throws an exception if there is no such bundle
46
            $allBundles = $this->kernel->getBundle($bundle, false);
47
        } catch (\InvalidArgumentException $e) {
48
            $message = sprintf(
49
                'The "%s" (from the _controller value "%s") does not exist or is not enabled in your kernel!',
50
                $bundle,
51
                $originalController
52
            );
53
54
            if ($alternative = $this->findAlternative($bundle)) {
55
                $message .= sprintf(' Did you mean "%s:%s:%s"?', $alternative, $controller, $action);
56
            }
57
58
            throw new \InvalidArgumentException($message, 0, $e);
59
        }
60
61
        foreach ($allBundles as $b) {
0 ignored issues
show
Bug introduced by
The expression $allBundles of type object<Symfony\Component...undle\BundleInterface>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
62
            $try = $this->guessControllerClassName($b, $controller);
63
            if (class_exists($try)) {
64
                return $this->guessActionString($try, $action);
65
            }
66
67
            $bundles[] = $b->getName();
68
            $msg       =
69
                sprintf(
70
                    'The _controller value "%s:%s:%s" maps to a "%s" class, but this class was not found. ' .
71
                    'Create this class or check the spelling of the class and its namespace.',
72
                    $bundle,
73
                    $controller,
74
                    $action,
75
                    $try
76
                );
77
        }
78
79
        if (count($bundles) > 1) {
80
            $msg =
81
                sprintf(
82
                    'Unable to find controller "%s:%s" in bundles %s.',
83
                    $bundle,
84
                    $controller,
85
                    implode(', ', $bundles)
86
                );
87
        }
88
89
        throw new \InvalidArgumentException($msg);
90
    }
91
92
    /** {@inheritdoc} */
93
    public function build($controller)
94
    {
95
        if (0 === preg_match($this->getDefaultControllerPattern(), $controller, $match)) {
96
            throw new \InvalidArgumentException(
97
                sprintf('The "%s" controller is not a valid "class::method" string.', $controller)
98
            );
99
        }
100
101
        $className      = $match[1];
102
        $controllerName = $match[2];
103
        $actionName     = $match[3];
104
        foreach ($this->kernel->getBundles() as $name => $bundle) {
105
            if (0 !== strpos($className, $bundle->getNamespace())) {
106
                continue;
107
            }
108
109
            return sprintf('%s:%s:%s', $name, $controllerName, $actionName);
110
        }
111
112
        throw new \InvalidArgumentException(
113
            sprintf('Unable to find a bundle that defines controller "%s".', $controller)
114
        );
115
    }
116
117
    /**
118
     * @param BundleInterface $bundle
119
     * @param string          $controller
120
     *
121
     * @return string Guessed controller FQCN
122
     */
123
    protected function guessControllerClassName(BundleInterface $bundle, $controller)
124
    {
125
        return $bundle->getNamespace() . '\\Controller\\' . $controller . 'Controller';
126
    }
127
128
    /**
129
     * @param string $controller
130
     * @param string $action
131
     *
132
     * @return string guessed FQCN::function string
133
     */
134
    protected function guessActionString($controller, $action)
135
    {
136
        return $controller . '::' . $action . 'Action';
137
    }
138
139
    /**
140
     * @return string
141
     */
142
    protected function getDefaultControllerPattern()
143
    {
144
        return '#^(.*?\\\\Rpc\\\\(.+)Controller)::(.+)Action$#';
145
    }
146
147
    /**
148
     * Attempts to find a bundle that is *similar* to the given bundle name.
149
     *
150
     * @param string $nonExistentBundleName
151
     *
152
     * @return string
153
     */
154
    private function findAlternative($nonExistentBundleName)
155
    {
156
        $bundleNames = array_map(
157
            function (BundleInterface $b) {
158
                return $b->getName();
159
            },
160
            $this->kernel->getBundles()
161
        );
162
163
        $alternative = null;
164
        $shortest    = null;
165
        foreach ($bundleNames as $bundleName) {
166
            // if there's a partial match, return it immediately
167
            if (false !== strpos($bundleName, $nonExistentBundleName)) {
168
                return $bundleName;
169
            }
170
171
            $lev = levenshtein($nonExistentBundleName, $bundleName);
172
            if ($lev <= strlen($nonExistentBundleName) / 3 && ($alternative === null || $lev < $shortest)) {
173
                $alternative = $bundleName;
174
            }
175
        }
176
177
        return $alternative;
178
    }
179
}
180