Completed
Push — master ( 38f1ea...fd6c7c )
by Matze
04:05
created

TestCreateCommand::getShortClassName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 2
Metric Value
c 4
b 0
f 2
dl 0
loc 11
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 31 and the first side effect is on line 25.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace BrainExe\Core\Console;
4
5
use BrainExe\Annotations\Annotations\Inject;
6
use BrainExe\Core\Console\TestGenerator\HandleExistingFile;
7
use BrainExe\Core\Console\TestGenerator\MethodCodeGenerator;
8
use BrainExe\Core\Console\TestGenerator\ProcessMethod;
9
use BrainExe\Core\Console\TestGenerator\TestData;
10
use BrainExe\Core\DependencyInjection\Rebuild;
11
use PHPUnit_Framework_MockObject_MockObject;
12
use PHPUnit_Framework_TestCase;
13
use ReflectionClass;
14
use ReflectionMethod;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\DependencyInjection\ContainerBuilder;
21
use Symfony\Component\DependencyInjection\Definition;
22
use Symfony\Component\DependencyInjection\Reference;
23
use BrainExe\Core\Annotations\Command as CommandAnnotation;
24
25
require_once "TestGenerator/TestData.php";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal TestGenerator/TestData.php does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
26
27
/**
28
 * @CommandAnnotation
29
 * @codeCoverageIgnore
30
 */
31
class TestCreateCommand extends Command
32
{
33
34
    /**
35
     * Cached container builder
36
     * @var ContainerBuilder|null
37
     */
38
    public $container = null;
39
40
    /**
41
     * @var Rebuild
42
     */
43
    private $rebuild;
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    protected function configure()
49
    {
50
        $this->setName('test:create')
51
            ->addArgument('service', InputArgument::REQUIRED, 'service id, e.g. IndexController')
52
            ->addOption('dry', 'd', InputOption::VALUE_NONE, 'only display the generated test');
53
    }
54
55
    /**
56
     * @Inject("@Core.Rebuild")
57
     * @param Rebuild $rebuild
58
     */
59
    public function __construct(Rebuild $rebuild)
60
    {
61
        $this->rebuild = $rebuild;
62
63
        parent::__construct();
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    protected function execute(InputInterface $input, OutputInterface $output)
70
    {
71
        $this->initContainerBuilder();
72
73
        $serviceId            = $input->getArgument('service');
74
        $serviceObject        = $this->getService($serviceId);
75
        $serviceDefinition    = $this->getServiceDefinition($serviceId);
76
        $serviceReflection    = new ReflectionClass($serviceObject);
77
        $serviceFullClassName = $serviceReflection->getName();
78
        $serviceClassName     = $this->getShortClassName($serviceFullClassName);
79
80
        $testData = new TestData();
81
82
        $testData->addUse(PHPUnit_Framework_TestCase::class, 'TestCase');
83
        $testData->addUse(PHPUnit_Framework_MockObject_MockObject::class, 'MockObject');
84
        $testData->addUse($serviceFullClassName);
85
86
        $this->generateMethods($serviceReflection, $testData, $serviceFullClassName);
87
        $this->setupConstructor($serviceDefinition, $testData);
88
89
        $methodProcessor = new ProcessMethod($this);
90
        foreach ($serviceDefinition->getMethodCalls() as $methodCall) {
91
            $methodProcessor->processMethod($methodCall, $testData);
92
        }
93
94
        $template = file_get_contents(__DIR__ . '/../../scripts/phpunit_template.php.tpl');
95
        $template = str_replace(
96
            '%test_namespace%',
97
            $this->getTestNamespace($serviceReflection->getNamespaceName()),
98
            $template
99
        );
100
        $template = str_replace('%service_namespace%', $serviceFullClassName, $template);
101
        $template = str_replace('%class_name%', $serviceClassName, $template);
102
        $template = str_replace('%setters%', implode("\n", $testData->setterCalls), $template);
103
        $template = str_replace('%default_tests%', implode("\n", $testData->defaultTests), $template);
104
        $template = str_replace('%mock_properties%', implode("\n", $testData->mockProperties), $template);
105
        $template = str_replace('%use_statements%', $testData->renderUse(), $template);
106
        $template = str_replace('%local_mocks%', implode("\n", $testData->localMocks), $template);
107
        $template = str_replace('%constructor_arguments%', implode(', ', $testData->constructorArguments), $template);
108
        $template = str_replace("\t", '    ', $template);
109
110
        $testFileName = $this->getTestFileName($serviceFullClassName);
111
112
        if ($input->getOption('dry')) {
113
            $output->writeln($template);
114
            return;
115
        } elseif (file_exists($testFileName)) {
116
            // handle already existing test file
117
            $fileHandler = new HandleExistingFile();
118
            $handler = $this->getHelper('question');
119
            $template = $fileHandler->handleExistingFile(
120
                $input,
121
                $output,
122
                $handler,
0 ignored issues
show
Compatibility introduced by
$handler of type object<Symfony\Component...Helper\HelperInterface> is not a sub-type of object<Symfony\Component...\Helper\QuestionHelper>. It seems like you assume a concrete implementation of the interface Symfony\Component\Console\Helper\HelperInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
123
                $serviceId,
124
                $testFileName,
125
                $template
126
            );
127
128
            if ($template === false) {
129
                return;
130
            }
131
        }
132
133
        $testDir = dirname($testFileName);
134
135
        if (!is_dir($testDir)) {
136
            mkdir($testDir, 0777, true);
137
        }
138
139
        file_put_contents($testFileName, $template);
140
141
        $output->writeln(
142
            sprintf("Created Test for '<info>%s</info>' in <info>%s</info>", $serviceId, $testFileName)
143
        );
144
    }
145
146
    /**
147
     * @param string $serviceNamespace
148
     * @return string
149
     */
150
    private function getTestFileName($serviceNamespace)
151
    {
152
        $path = str_replace('\\', DIRECTORY_SEPARATOR, $serviceNamespace);
153
154
        return sprintf('%sTests/%sTest.php', ROOT, $path);
155
    }
156
157
    /**
158
     * @param string $serviceId
159
     * @return Definition
160
     */
161
    public function getServiceDefinition($serviceId)
162
    {
163
        return $this->container->getDefinition($serviceId);
164
    }
165
166
    /**
167
     * @param string $serviceId
168
     * @return object
169
     */
170
    public function getService($serviceId)
171
    {
172
        return $this->container->get($serviceId);
173
    }
174
175
    /**
176
     * @param string $serviceNamespace
177
     * @return string
178
     */
179
    private function getTestNamespace($serviceNamespace)
180
    {
181
        return "Tests\\" . $serviceNamespace;
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal Tests\\ does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
182
    }
183
184
    /**
185
     * @param ReflectionClass $serviceReflection
186
     * @param TestData $testData
187
     * @param string $serviceFullClassName
188
     */
189
    protected function generateMethods(ReflectionClass $serviceReflection, TestData $testData, $serviceFullClassName)
190
    {
191
        $blacklistedMethods = $this->getBlacklistedMethods($serviceReflection);
192
193
        $methodCodeGenerator = new MethodCodeGenerator();
194
        $methods             = $serviceReflection->getMethods(ReflectionMethod::IS_PUBLIC);
195
        foreach ($methods as $method) {
196
            $methodName = $method->getName();
197
198
            if (!in_array($methodName, $blacklistedMethods)) {
199
                if ($method->getDeclaringClass() == $serviceReflection) {
200
                    $testData->defaultTests[] = $methodCodeGenerator->getDummyTestCode(
201
                        $testData,
202
                        $method,
203
                        $serviceFullClassName
204
                    );
205
                }
206
            }
207
        }
208
    }
209
210
    private function initContainerBuilder()
211
    {
212
        if ($this->container !== null) {
213
            return;
214
        }
215
216
        $this->container = $this->rebuild->rebuildDIC(false);
217
    }
218
219
    /**
220
     * @param string $fullClassName
221
     * @return string
222
     */
223
    public function getShortClassName($fullClassName)
224
    {
225
        // Strip off namespace
226
        $lastBackslashPos = strrpos($fullClassName, '\\');
227
228
        if (!$lastBackslashPos) {
229
            return $fullClassName;
230
        }
231
232
        return substr($fullClassName, $lastBackslashPos + 1);
233
    }
234
235
    /**
236
     * @param ReflectionClass $serviceReflection
237
     * @return string[]
238
     */
239
    private function getBlacklistedMethods(ReflectionClass $serviceReflection)
240
    {
241
        $blacklistedMethods = [];
242
243
        foreach ($serviceReflection->getTraitNames() as $trait) {
244
            $reflection = new ReflectionClass($trait);
245
            foreach ($reflection->getMethods() as $method) {
246
                $blacklistedMethods[] = $method->getName();
247
            }
248
        }
249
250
        $blacklistedMethods[] = '__construct';
251
252
        return $blacklistedMethods;
253
    }
254
255
    /**
256
     * @param Definition $referenceService
257
     * @param TestData $testData
258
     * @param string $mockName
259
     */
260
    public function addMock(Definition $referenceService, TestData $testData, $mockName)
261
    {
262
        $class = $referenceService->getClass();
263
        $testData->addUse($class);
264
265
        $mock = sprintf(
266
            "\t\t\$this->%s = \$this->createMock(%s::class);",
267
            lcfirst($mockName),
268
            $mockName
269
        );
270
271
        $testData->localMocks[]     = $mock;
272
        $testData->mockProperties[] = sprintf(
273
            "\t/**\n\t * @var %s|MockObject\n\t */\n\tprivate \$%s;\n",
274
            $mockName,
275
            lcfirst($mockName)
276
        );
277
    }
278
279
    /**
280
     * @param Definition $serviceDefinition
281
     * @param TestData $data
282
     */
283
    private function setupConstructor(Definition $serviceDefinition, TestData $data)
284
    {
285
        foreach ($serviceDefinition->getArguments() as $reference) {
286
            if ($reference instanceof Definition) {
287
                $definition = $reference;
288
                $mockName = $this->getShortClassName($definition->getClass());
289
            } elseif ($reference instanceof Reference) {
290
                // add setter for model mock
291
                $definition = $this->getServiceDefinition((string)$reference);
292
                $mockName = $this->getShortClassName($definition->getClass());
293
            } else {
294
                $data->constructorArguments[] = var_export($reference, true);
295
                continue;
296
            }
297
298
            $data->constructorArguments[] = sprintf('$this->%s', lcfirst($mockName));
299
300
            $this->addMock($definition, $data, $mockName);
301
        }
302
    }
303
}
304