Completed
Push — master ( ecdf18...6af8d0 )
by Kamil
08:48
created

normalizeContext()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the SymfonyExtension package.
7
 *
8
 * (c) Kamil Kokot <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace FriendsOfBehat\SymfonyExtension\Context\Environment\Handler;
15
16
use Behat\Behat\Context\Context;
17
use Behat\Behat\Context\Environment\ContextEnvironment;
18
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
19
use Behat\Testwork\Environment\Environment;
20
use Behat\Testwork\Environment\Exception\EnvironmentIsolationException;
21
use Behat\Testwork\Environment\Handler\EnvironmentHandler;
22
use Behat\Testwork\Suite\Exception\SuiteConfigurationException;
23
use Behat\Testwork\Suite\GenericSuite;
24
use Behat\Testwork\Suite\Suite;
25
use FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle;
26
use FriendsOfBehat\SymfonyExtension\Context\Environment\InitializedSymfonyExtensionEnvironment;
27
use FriendsOfBehat\SymfonyExtension\Context\Environment\UninitializedSymfonyExtensionEnvironment;
28
use Symfony\Component\DependencyInjection\ContainerInterface;
29
use Symfony\Component\HttpKernel\KernelInterface;
30
31
final class ContextServiceEnvironmentHandler implements EnvironmentHandler
32
{
33
    /** @var KernelInterface */
34
    private $symfonyKernel;
35
36
    /** @var EnvironmentHandler */
37
    private $decoratedEnvironmentHandler;
38
39
    public function __construct(KernelInterface $symfonyKernel, EnvironmentHandler $decoratedEnvironmentHandler)
40
    {
41
        $this->symfonyKernel = $symfonyKernel;
42
        $this->decoratedEnvironmentHandler = $decoratedEnvironmentHandler;
43
    }
44
45
    public function supportsSuite(Suite $suite): bool
46
    {
47
        return $suite->hasSetting('contexts');
48
    }
49
50
    public function buildEnvironment(Suite $suite): Environment
51
    {
52
        $symfonyContexts = [];
53
54
        foreach ($this->getSuiteContextsServices($suite) as $serviceId) {
55
            if (!$this->getContainer()->has($serviceId)) {
56
                continue;
57
            }
58
59
            $symfonyContexts[$serviceId] = get_class($this->getContainer()->get($serviceId));
60
        }
61
62
        $delegatedSuite = $this->cloneSuiteWithoutContexts($suite, array_keys($symfonyContexts));
63
64
        /** @var ContextEnvironment $delegatedEnvironment */
65
        $delegatedEnvironment = $this->decoratedEnvironmentHandler->buildEnvironment($delegatedSuite);
66
67
        return new UninitializedSymfonyExtensionEnvironment($suite, $symfonyContexts, $delegatedEnvironment);
68
    }
69
70
    public function supportsEnvironmentAndSubject(Environment $environment, $testSubject = null): bool
71
    {
72
        return $environment instanceof UninitializedSymfonyExtensionEnvironment;
73
    }
74
75
    /**
76
     * @param UninitializedSymfonyExtensionEnvironment $uninitializedEnvironment
77
     *
78
     * @throws EnvironmentIsolationException
79
     */
80
    public function isolateEnvironment(Environment $uninitializedEnvironment, $testSubject = null): Environment
81
    {
82
        $this->assertEnvironmentCanBeIsolated($uninitializedEnvironment, $testSubject);
83
84
        $environment = new InitializedSymfonyExtensionEnvironment($uninitializedEnvironment->getSuite());
85
86
        foreach ($uninitializedEnvironment->getServices() as $serviceId) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Testwork\Environment\Environment as the method getServices() does only exist in the following implementations of said interface: FriendsOfBehat\SymfonyEx...onyExtensionEnvironment.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
87
            /** @var Context $context */
88
            $context = $this->getContainer()->get($serviceId);
89
90
            $environment->registerContext($context);
91
        }
92
93
        /** @var InitializedContextEnvironment $delegatedEnvironment */
94
        $delegatedEnvironment = $this->decoratedEnvironmentHandler->isolateEnvironment($uninitializedEnvironment->getDelegatedEnvironment());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Behat\Testwork\Environment\Environment as the method getDelegatedEnvironment() does only exist in the following implementations of said interface: FriendsOfBehat\SymfonyEx...onyExtensionEnvironment.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
95
96
        foreach ($delegatedEnvironment->getContexts() as $context) {
97
            $environment->registerContext($context);
98
        }
99
100
        return $environment;
101
    }
102
103
    /**
104
     * @return string[]
105
     *
106
     * @throws SuiteConfigurationException If "contexts" setting is not an array
107
     */
108
    private function getSuiteContextsServices(Suite $suite): array
109
    {
110
        $contexts = $suite->getSetting('contexts');
111
112 View Code Duplication
        if (!is_array($contexts)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
113
            throw new SuiteConfigurationException(sprintf(
114
                '"contexts" setting of the "%s" suite is expected to be an array, %s given.',
115
                $suite->getName(),
116
                gettype($contexts)
117
            ), $suite->getName());
118
        }
119
120
        return array_map([$this, 'normalizeContext'], $contexts);
121
    }
122
123
    private function cloneSuiteWithoutContexts(Suite $suite, array $contextsToRemove): Suite
124
    {
125
        $contexts = $suite->getSetting('contexts');
126
127 View Code Duplication
        if (!is_array($contexts)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
128
            throw new SuiteConfigurationException(sprintf(
129
                '"contexts" setting of the "%s" suite is expected to be an array, %s given.',
130
                $suite->getName(),
131
                gettype($contexts)
132
            ), $suite->getName());
133
        }
134
135
        $contexts = array_filter($contexts, function ($context) use ($contextsToRemove): bool {
136
            return !in_array($this->normalizeContext($context), $contextsToRemove, true);
137
        });
138
139
        return new GenericSuite($suite->getName(), array_merge($suite->getSettings(), ['contexts' => $contexts]));
140
    }
141
142
    private function normalizeContext($context): string
143
    {
144
        if (is_array($context)) {
145
            return current(array_keys($context));
146
        }
147
148
        if (is_string($context)) {
149
            return $context;
150
        }
151
152
        throw new \Exception();
153
    }
154
155
    /**
156
     * @throws EnvironmentIsolationException
157
     */
158
    private function assertEnvironmentCanBeIsolated(Environment $uninitializedEnvironment, $testSubject): void
159
    {
160
        if (!$this->supportsEnvironmentAndSubject($uninitializedEnvironment, $testSubject)) {
161
            throw new EnvironmentIsolationException(sprintf(
162
                '"%s" does not support isolation of "%s" environment.',
163
                static::class,
164
                get_class($uninitializedEnvironment)
165
            ), $uninitializedEnvironment);
166
        }
167
    }
168
169
    private function getContainer(): ContainerInterface
170
    {
171
        try {
172
            $this->symfonyKernel->getBundle('FriendsOfBehatSymfonyExtensionBundle');
173
        } catch (\InvalidArgumentException $exception) {
174
            throw new \RuntimeException(sprintf(
175
                'Kernel "%s" used by Behat with "%s" environment and debug %s needs to have "%s" bundle registered.',
176
                get_class($this->symfonyKernel),
177
                $this->symfonyKernel->getEnvironment(),
178
                $this->symfonyKernel->isDebug() ? 'enabled' : 'disabled',
179
                FriendsOfBehatSymfonyExtensionBundle::class
180
            ));
181
        }
182
183
        return $this->symfonyKernel->getContainer();
184
    }
185
}
186