Completed
Push — master ( 296743...12eab9 )
by Kirill
36:17
created

ArgumentValidator::validateNullDefaultValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
/**
3
 * This file is part of Railt package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace Railt\SDL\Reflection\Validation\Definitions;
11
12
use Railt\SDL\Contracts\Behavior\Inputable;
13
use Railt\SDL\Contracts\Definitions\Definition;
14
use Railt\SDL\Contracts\Dependent\ArgumentDefinition;
15
use Railt\SDL\Exceptions\TypeConflictException;
16
17
/**
18
 * Class ArgumentValidator
19
 */
20
class ArgumentValidator extends BaseDefinitionValidator
21
{
22
    /**
23
     * @param Definition $definition
24
     * @return bool
25
     */
26 6551
    public function match(Definition $definition): bool
27
    {
28 6551
        return $definition instanceof ArgumentDefinition;
29
    }
30
31
    /**
32
     * @param Definition|ArgumentDefinition $type
33
     * @return void
34
     * @throws \Railt\SDL\Exceptions\TypeConflictException
35
     */
36 5545
    public function validate(Definition $type): void
37
    {
38 5545
        $definition = $type->getTypeDefinition();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Railt\SDL\Contracts\Definitions\Definition as the method getTypeDefinition() does only exist in the following implementations of said interface: Railt\SDL\Base\BaseDocument, Railt\SDL\Base\Dependent\BaseArgument, Railt\SDL\Base\Dependent\BaseField, Railt\SDL\Base\Invocations\BaseDirectiveInvocation, Railt\SDL\Base\Invocations\BaseInputInvocation, Railt\SDL\Reflection\Bui...pendent\ArgumentBuilder, Railt\SDL\Reflection\Bui...\Dependent\FieldBuilder, Railt\SDL\Reflection\Builder\DocumentBuilder, Railt\SDL\Reflection\Bui...ectiveInvocationBuilder, Railt\SDL\Reflection\Bui...\InputInvocationBuilder, Railt\SDL\Standard\Directives\Deprecation\Reason, Railt\SDL\Standard\GraphQLDocument.

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...
39
40 5545 View Code Duplication
        if (! ($definition instanceof Inputable)) {
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...
41
            $error = \sprintf('%s must be type of Scalar, Enum or Input', $type);
42
            throw new TypeConflictException($error, $this->getCallStack());
43
        }
44
45 5545
        if ($type->hasDefaultValue()) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Railt\SDL\Contracts\Definitions\Definition as the method hasDefaultValue() does only exist in the following implementations of said interface: Railt\SDL\Base\Dependent\BaseArgument, Railt\SDL\Reflection\Bui...pendent\ArgumentBuilder, Railt\SDL\Standard\Directives\Deprecation\Reason.

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...
46 5537
            $this->validateDefaultValue($type, $definition);
0 ignored issues
show
Compatibility introduced by
$type of type object<Railt\SDL\Contrac...Definitions\Definition> is not a sub-type of object<Railt\SDL\Contrac...ent\ArgumentDefinition>. It seems like you assume a child interface of the interface Railt\SDL\Contracts\Definitions\Definition 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...
47
        }
48 5542
    }
49
50
    /**
51
     * @param ArgumentDefinition $type
52
     * @param Inputable $definition
53
     * @return void
54
     * @throws TypeConflictException
55
     */
56 5537
    private function validateDefaultValue(ArgumentDefinition $type, Inputable $definition): void
57
    {
58 5537
        $default = $type->getDefaultValue();
59
60 5537
        if ($default === null) {
61 5513
            $this->validateNullDefaultValue($type);
62
63 5512
            return;
64
        }
65
66 5334
        if (\is_array($default)) {
67 3036
            $this->validateArrayDefaultValue($type, $default);
68
69 1534
            $this->validateDefaultListType($type, $definition, $default);
70
71 1534
            return;
72
        }
73
74 5310
        $this->validateDefaultType($type, $definition, $default);
75 5310
    }
76
77
    /**
78
     * @param ArgumentDefinition $type
79
     * @return void
80
     * @throws TypeConflictException
81
     */
82 5513
    private function validateNullDefaultValue(ArgumentDefinition $type): void
83
    {
84 5513
        if ($type->isNonNull()) {
85 301
            $error = \sprintf('%s can not be initialized by default value NULL', $type);
86
87 301
            throw new TypeConflictException($error, $this->getCallStack());
88
        }
89 5512
    }
90
91
    /**
92
     * @param ArgumentDefinition $type
93
     * @param array $defaults
94
     * @return void
95
     * @throws TypeConflictException
96
     */
97 3036
    private function validateArrayDefaultValue(ArgumentDefinition $type, array $defaults): void
98
    {
99 3036 View Code Duplication
        if (! $type->isList()) {
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...
100 301
            $error = \sprintf('%s can not be initialized by List %s',
101 301
                $type, $this->valueToString($defaults)
102
            );
103 301
            throw new TypeConflictException($error, $this->getCallStack());
104
        }
105
106
107 2735
        if ($type->isList() && $type->isListOfNonNulls()) {
108 1827
            foreach ($defaults as $value) {
109 1815 View Code Duplication
                if ($value === null) {
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...
110 1201
                    $error = \sprintf('%s can not be initialized by list %s with NULL value',
111 1201
                        $type, $this->valueToString($defaults)
112
                    );
113
114 1815
                    throw new TypeConflictException($error, $this->getCallStack());
115
                }
116
            }
117
        }
118 1534
    }
119
120
    /**
121
     * @param ArgumentDefinition $type
122
     * @param Inputable $definition
123
     * @param array $values
124
     * @return void
125
     * @throws TypeConflictException
126
     */
127 1534
    private function validateDefaultListType(ArgumentDefinition $type, Inputable $definition, array $values): void
128
    {
129 1534
        $isNullable = ! $type->isListOfNonNulls();
130
131 1534
        foreach ($values as $value) {
132 1516
            $isNull = $isNullable && $value === null;
133
134 1516
            if (! $isNull && ! $definition->isCompatible($value)) {
135
                $error = \sprintf('%s defined by %s can not be initialized by %s',
136
                    $type,
137
                    $this->typeIndicatorToString($type),
138
                    $this->valueToString($values)
139
                );
140
141 1516
                throw new TypeConflictException($error, $this->getCallStack());
142
            }
143
        }
144 1534
    }
145
146
    /**
147
     * @param ArgumentDefinition $type
148
     * @param Inputable $definition
149
     * @param $value
150
     * @return void
151
     * @throws TypeConflictException
152
     */
153 5310
    private function validateDefaultType(ArgumentDefinition $type, Inputable $definition, $value): void
154
    {
155 5310
        if (! $definition->isCompatible($value)) {
156 408
            $error = \sprintf('%s contain non compatible default value %s', $type, $this->valueToString($value));
157 408
            throw new TypeConflictException($error, $this->getCallStack());
158
        }
159 5310
    }
160
}
161