Completed
Push — master ( 055f1b...f72a4b )
by Pavel
52s
created

RpcControllerNameParser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * User: scaytrase
4
 * Created: 2016-02-14 12:37
5
 */
6
7
namespace Bankiru\Api\Rpc\Controller;
8
9
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
10
use Symfony\Component\HttpKernel\KernelInterface;
11
12
/**
13
 * ControllerNameParser converts controller from the short notation a:b:c
14
 * (BlogBundle:Post:index) to a fully-qualified class::method string
15
 * (Bundle\BlogBundle\Controller\PostController::indexAction).
16
 *
17
 * @author Fabien Potencier <[email protected]>
18
 * @author Pavel Batanov <[email protected]>
19
 */
20
class RpcControllerNameParser implements ControllerNameParser
21
{
22
    protected $kernel;
23
24
    /**
25
     * Constructor.
26
     *
27
     * @param KernelInterface $kernel A KernelInterface instance
28
     */
29 7
    public function __construct(KernelInterface $kernel)
30
    {
31 7
        $this->kernel = $kernel;
32 7
    }
33
34
    /** {@inheritdoc} */
35
    public function parse($controller)
36
    {
37
        $originalController = $controller;
38
        if (3 !== count($parts = explode(':', $controller))) {
39
            throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "a:b:c" controller string.', $controller));
40
        }
41
42
        list($bundle, $controller, $action) = $parts;
43
        $controller = str_replace('/', '\\', $controller);
44
        $bundles = array();
45
46
        try {
47
            // this throws an exception if there is no such bundle
48
            $allBundles = $this->kernel->getBundle($bundle, false);
49
        } catch (\InvalidArgumentException $e) {
50
            $message = sprintf(
51
                'The "%s" (from the _controller value "%s") does not exist or is not enabled in your kernel!',
52
                $bundle,
53
                $originalController
54
            );
55
56
            if ($alternative = $this->findAlternative($bundle)) {
57
                $message .= sprintf(' Did you mean "%s:%s:%s"?', $alternative, $controller, $action);
58
            }
59
60
            throw new \InvalidArgumentException($message, 0, $e);
61
        }
62
63
        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...
64
            $try = $this->guessControllerClassName($b, $controller);
65
            if (class_exists($try)) {
66
                return $this->guessActionString($try, $action);
67
            }
68
69
            $bundles[] = $b->getName();
70
            $msg = sprintf('The _controller value "%s:%s:%s" maps to a "%s" class, but this class was not found. Create this class or check the spelling of the class and its namespace.', $bundle, $controller, $action, $try);
71
        }
72
73
        if (count($bundles) > 1) {
74
            $msg = sprintf('Unable to find controller "%s:%s" in bundles %s.', $bundle, $controller, implode(', ', $bundles));
75
        }
76
77
        throw new \InvalidArgumentException($msg);
78
    }
79
80
    /** {@inheritdoc} */
81
    public function build($controller)
82
    {
83
        if (0 === preg_match('#^(.*?\\\\Controller\\\\(.+)Controller)::(.+)Action$#', $controller, $match)) {
84
            throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "class::method" string.', $controller));
85
        }
86
87
        $className = $match[1];
88
        $controllerName = $match[2];
89
        $actionName = $match[3];
90
        foreach ($this->kernel->getBundles() as $name => $bundle) {
91
            if (0 !== strpos($className, $bundle->getNamespace())) {
92
                continue;
93
            }
94
95
            return sprintf('%s:%s:%s', $name, $controllerName, $actionName);
96
        }
97
98
        throw new \InvalidArgumentException(sprintf('Unable to find a bundle that defines controller "%s".', $controller));
99
    }
100
101
    /**
102
     * Attempts to find a bundle that is *similar* to the given bundle name.
103
     *
104
     * @param string $nonExistentBundleName
105
     *
106
     * @return string
107
     */
108
    private function findAlternative($nonExistentBundleName)
109
    {
110
        $bundleNames = array_map(function (BundleInterface $b) {
111
            return $b->getName();
112
        }, $this->kernel->getBundles());
113
114
        $alternative = null;
115
        $shortest = null;
116
        foreach ($bundleNames as $bundleName) {
117
            // if there's a partial match, return it immediately
118
            if (false !== strpos($bundleName, $nonExistentBundleName)) {
119
                return $bundleName;
120
            }
121
122
            $lev = levenshtein($nonExistentBundleName, $bundleName);
123
            if ($lev <= strlen($nonExistentBundleName) / 3 && ($alternative === null || $lev < $shortest)) {
124
                $alternative = $bundleName;
125
            }
126
        }
127
128
        return $alternative;
129
    }
130
131
    /**
132
     * @param BundleInterface $bundle
133
     * @param string $controller
134
     * @return string Guessed controller FQCN
135
     */
136
    protected function guessControllerClassName(BundleInterface $bundle, $controller)
137
    {
138
        return $bundle->getNamespace().'\\Controller\\'.$controller.'Controller';
139
    }
140
141
    /**
142
     * @param string $controller
143
     * @param string $action
144
     * @return string guessed FQCN::function string
145
     */
146
    protected function guessActionString($controller, $action)
147
    {
148
        return $controller.'::'.$action.'Action';
149
    }
150
}
151