Passed
Push — master ( 93030a...89d176 )
by Smoren
02:41
created

Schemator::getPathDelimiter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Smoren\Schemator\Components;
6
7
use Smoren\Schemator\Exceptions\PathException;
8
use Smoren\Schemator\Exceptions\SchematorException;
9
use Smoren\Schemator\Factories\NestedAccessorFactory;
10
use Smoren\Schemator\Interfaces\BitmapInterface;
11
use Smoren\Schemator\Interfaces\NestedAccessorFactoryInterface;
12
use Smoren\Schemator\Interfaces\SchematorInterface;
13
use Smoren\Schemator\Structs\Bitmap;
14
use Smoren\Schemator\Structs\ErrorsLevelMask;
15
use Smoren\Schemator\Structs\FilterContext;
16
use Throwable;
17
use TypeError;
18
19
/**
20
 * Class for schematic data converting
21
 * @author Smoren <[email protected]>
22
 */
23
class Schemator implements SchematorInterface
24
{
25
    /**
26
     * @var array<string, callable> filters map
27
     */
28
    protected array $filterMap = [];
29
    /**
30
     * @var non-empty-string delimiter for multilevel paths
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
31
     */
32
    protected string $pathDelimiter;
33
    /**
34
     * @var BitmapInterface bitmap errors level mask
35
     */
36
    protected BitmapInterface $errorsLevelMask;
37
    /**
38
     * @var NestedAccessorFactoryInterface nested accessor factory
39
     */
40
    protected NestedAccessorFactoryInterface $nestedAccessorFactory;
41
42
    /**
43
     * Schemator constructor.
44
     * @param non-empty-string $pathDelimiter delimiter for multilevel paths
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
45
     * @param BitmapInterface|null $errorsLevelMask bitmap errors level mask
46
     * @param NestedAccessorFactoryInterface|null $nestedAccessorFactory nested accessor factory
47
     */
48
    public function __construct(
49
        string $pathDelimiter = '.',
50
        ?BitmapInterface $errorsLevelMask = null,
51
        NestedAccessorFactoryInterface $nestedAccessorFactory = null
52
    ) {
53
        $this->pathDelimiter = $pathDelimiter;
54
        $this->errorsLevelMask = $errorsLevelMask ?? ErrorsLevelMask::default();
55
        $this->nestedAccessorFactory = $nestedAccessorFactory ?? new NestedAccessorFactory();
56
    }
57
58
    /**
59
     * @inheritDoc
60
     * @throws SchematorException
61
     */
62
    public function convert($source, array $schema)
63
    {
64
        $result = [];
65
        $toAccessor = $this->nestedAccessorFactory->create($result, $this->pathDelimiter);
66
67
        foreach ($schema as $keyTo => $keyFrom) {
68
            $value = $this->getValue($source, $keyFrom);
69
            if ($keyTo === '') {
70
                return $value;
71
            }
72
            $toAccessor->set($keyTo, $value);
73
        }
74
75
        return $result;
76
    }
77
78
    /**
79
     * @inheritDoc
80
     * @throws SchematorException
81
     */
82
    public function getValue($source, $key)
83
    {
84
        if ($key === '' || $key === null) {
85
            return $source;
86
        }
87
88
        if (!is_array($source) && !is_object($source)) {
89
            return $this->getValueFromUnsupportedSource($source, $key);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getValueFromUnsup...edSource($source, $key) targeting Smoren\Schemator\Compone...FromUnsupportedSource() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
90
        }
91
92
        if (is_string($key)) {
93
            return $this->getValueByKey($source, $key);
94
        }
95
96
        if (is_array($key)) {
97
            return $this->getValueByFilters($source, $key);
98
        }
99
100
        return $this->getValueByUnsupportedKey($source, $key);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getValueByUnsupportedKey($source, $key) targeting Smoren\Schemator\Compone...ValueByUnsupportedKey() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
101
    }
102
103
    /**
104
     * @inheritDoc
105
     */
106
    public function setPathDelimiter(string $value): void
107
    {
108
        $this->pathDelimiter = $value;
109
    }
110
111
    /**
112
     * @inheritDoc
113
     */
114
    public function getPathDelimiter(): string
115
    {
116
        return $this->pathDelimiter;
117
    }
118
119
    /**
120
     * @inheritDoc
121
     */
122
    public function setErrorsLevelMask(BitmapInterface $value): void
123
    {
124
        $this->errorsLevelMask = $value;
125
    }
126
127
    /**
128
     * @inheritDoc
129
     */
130
    public function addFilter(string $filterName, callable $callback): self
131
    {
132
        $this->filterMap[$filterName] = $callback;
133
        return $this;
134
    }
135
136
    /**
137
     * Returns value got by string key
138
     * @param array<string, mixed>|object $source source to get value from
139
     * @param string $key nested path to get value by
140
     * @return array|mixed|null value
141
     * @throws SchematorException
142
     */
143
    protected function getValueByKey($source, string $key)
144
    {
145
        try {
146
            $fromAccessor = $this->nestedAccessorFactory->create($source, $this->pathDelimiter);
147
148
            return $fromAccessor->get(
149
                $key,
0 ignored issues
show
Bug introduced by
$key of type string is incompatible with the type Smoren\Schemator\Interfaces\TPath expected by parameter $path of Smoren\Schemator\Interfa...ccessorInterface::get(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

149
                /** @scrutinizer ignore-type */ $key,
Loading history...
150
                $this->needToThrow(SchematorException::CANNOT_GET_VALUE)
151
            );
152
        } catch (PathException $e) {
153
            throw SchematorException::createAsCannotGetValue($source, $key, $e);
154
        }
155
    }
156
157
    /**
158
     * Returns value got by filters key
159
     * @param array<string, mixed>|object $source source to get value from
160
     * @param array<int, string|array<int, mixed>|null> $filters filters config
161
     * @return mixed|null
162
     * @throws SchematorException
163
     */
164
    protected function getValueByFilters($source, array $filters)
165
    {
166
        $result = $source;
167
        foreach ($filters as $filterConfig) {
168
            if (is_string($filterConfig) || $filterConfig === null) {
169
                $result = $this->getValue($result, $filterConfig);
170
            } elseif (is_array($filterConfig)) {
171
                $result = $this->runFilter($filterConfig, $result, $source);
172
            } else {
173
                if ($this->needToThrow(SchematorException::UNSUPPORTED_FILTER_CONFIG_TYPE)) {
174
                    throw SchematorException::createAsUnsupportedFilterConfigType($filterConfig);
175
                }
176
                $result = null;
177
            }
178
        }
179
180
        return $result;
181
    }
182
183
    /**
184
     * Returns value got from unsupported source
185
     * @param mixed $source unsupported source
186
     * @param mixed $key path to get value by
187
     * @return null the only value we can get from unsupported source
188
     * @throws SchematorException
189
     */
190
    protected function getValueFromUnsupportedSource($source, $key)
191
    {
192
        if ($this->needToThrow(SchematorException::UNSUPPORTED_SOURCE_TYPE)) {
193
            throw SchematorException::createAsUnsupportedSourceType($source, $key);
194
        }
195
        return null;
196
    }
197
198
    /**
199
     * Returns value got by unsupported key
200
     * @param array<string, mixed>|object $source source to get value from
201
     * @param mixed $key unsupported key
202
     * @return null the only value we can get by unsupported key
203
     * @throws SchematorException
204
     */
205
    protected function getValueByUnsupportedKey($source, $key)
206
    {
207
        if ($this->needToThrow(SchematorException::UNSUPPORTED_KEY_TYPE)) {
208
            throw SchematorException::createAsUnsupportedKeyType($source, $key);
209
        }
210
        return null;
211
    }
212
213
    /**
214
     * Returns value from source by filter
215
     * @param array<int, mixed> $filterConfig filter config [filterName, ...args]
216
     * @param array<string, mixed>|object|mixed $source source to extract value from
217
     * @param array<string, mixed>|object $rootSource root source
218
     * @return mixed result value
219
     * @throws SchematorException
220
     */
221
    protected function runFilter(array $filterConfig, $source, $rootSource)
222
    {
223
        /** @var scalar $filterName */
224
        $filterName = array_shift($filterConfig);
225
        $filterName = strval($filterName);
226
227
        if (
228
            !isset($this->filterMap[$filterName])
229
            && $this->needToThrow(SchematorException::FILTER_NOT_FOUND)
230
        ) {
231
            throw SchematorException::createAsFilterNotFound($filterName);
232
        }
233
234
        $filterContext = new FilterContext($this, $source, $rootSource, $filterConfig, $filterName);
235
        try {
236
            return $this->filterMap[$filterName](
237
                $filterContext,
238
                ...$filterConfig
239
            );
240
        } catch (TypeError $e) {
241
            if ($this->needToThrow(SchematorException::BAD_FILTER_CONFIG)) {
242
                throw SchematorException::createAsBadFilterConfig($filterContext, $e);
243
            }
244
            return null;
245
        } catch (SchematorException $e) {
246
            if ($this->needToThrow($e->getCode())) {
247
                throw $e;
248
            }
249
            return null;
250
        } catch (Throwable $e) {
251
            if ($this->needToThrow(SchematorException::FILTER_ERROR)) {
252
                throw SchematorException::createAsFilterError($filterContext);
253
            }
254
            return null;
255
        }
256
    }
257
258
    /**
259
     * Returns true if there is given error code in errors level mask
260
     * @param int $errorCode
261
     * @return bool
262
     */
263
    protected function needToThrow(int $errorCode): bool
264
    {
265
        return $this->errorsLevelMask->intersectsWith(Bitmap::create([$errorCode]));
266
    }
267
}
268