Passed
Push — master ( 46ed75...ca2387 )
by Zaahid
03:33
created

AbstractConsumerService   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 32
eloc 76
c 1
b 0
f 0
dl 0
loc 311
ccs 87
cts 87
cp 1
rs 9.84

13 Methods

Rating   Name   Duplication   Size   Complexity  
A parseTokensIntoParts() 0 9 3
A getTokenSplitPattern() 0 5 1
A advanceToNextToken() 0 8 6
A getTokenParts() 0 7 3
A getConsumerTokenParts() 0 15 3
A __construct() 0 5 1
A __invoke() 0 12 2
A getAllConsumers() 0 13 4
A splitRawValue() 0 14 2
A parseRawValue() 0 4 1
A getPartForToken() 0 9 3
A getAllTokenSeparators() 0 8 2
A processParts() 0 3 1
1
<?php
2
/**
3
 * This file is part of the ZBateson\MailMimeParser project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
8
namespace ZBateson\MailMimeParser\Header\Consumer;
9
10
use ArrayIterator;
11
use Iterator;
12
use NoRewindIterator;
13
use ZBateson\MailMimeParser\Header\IHeaderPart;
14
use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory;
15
use ZBateson\MailMimeParser\Header\Part\MimeLiteralPart;
16
use Psr\Log\LoggerInterface;
17
use Psr\Log\NullLogger;
18
19
/**
20
 * Abstract base class for all header token consumers.
21
 *
22
 * Defines the base parser that loops over tokens, consuming them and creating
23
 * header parts.
24
 *
25
 * @author Zaahid Bateson
26
 */
27
abstract class AbstractConsumerService implements IConsumerService
28
{
29
    #[Inject]
30
    protected LoggerInterface $logger;
31
32
    /**
33
     * @var used to construct IHeaderPart objects
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\Header\Consumer\used was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
34
     */
35
    protected HeaderPartFactory $partFactory;
36
37
    /**
38
     * @var the generated token split pattern on first run, so it doesn't
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\Header\Consumer\the was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
39
     *      need to be regenerated every time.
40
     */
41
    private ?string $tokenSplitPattern = null;
42
43
    /**
44
     * @var AbstractConsumerService[] array of sub-consumers used by this
45
     *      consumer if any, or an empty array if none exist.
46
     */
47
    protected array $subConsumers = [];
48
49
    /**
50
     * @param HeaderPartFactory $partFactory
51
     * @param AbstractConsumerService[] $subConsumers
52
     */
53 58
    public function __construct(HeaderPartFactory $partFactory, array $subConsumers = [])
54
    {
55 58
        $this->logger = new NullLogger();
56 58
        $this->partFactory = $partFactory;
0 ignored issues
show
Documentation Bug introduced by
It seems like $partFactory of type ZBateson\MailMimeParser\...\Part\HeaderPartFactory is incompatible with the declared type ZBateson\MailMimeParser\Header\Consumer\used of property $partFactory.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
57 58
        $this->subConsumers = $subConsumers;
58
    }
59
60 163
    public function __invoke(string $value) : array
61
    {
62 163
        $this->logger->debug('Starting ${class} for "${value}"', [ 'class' => static::class, 'value' => $value ]);
63 163
        if ($value !== '') {
64 162
            $parts = $this->parseRawValue($value);
65 162
            $this->logger->debug(
66 162
                'Ending ${class} for "${value}": parsed into ${cnt} header part objects',
67 162
                [ 'class' => static::class, 'value' => $value, 'cnt' => count($parts) ]
68 162
            );
69 162
            return $parts;
70
        }
71 1
        return [];
72
    }
73
74
    /**
75
     * Returns this consumer and all unique sub consumers.
76
     *
77
     * Loops into the sub-consumers (and their sub-consumers, etc...) finding
78
     * all unique consumers, and returns them in an array.
79
     *
80
     * @return AbstractConsumerService[] Array of unique consumers.
81
     */
82 59
    protected function getAllConsumers() : array
83
    {
84 59
        $found = [$this];
85
        do {
86 59
            $current = \current($found);
87 59
            $subConsumers = $current->subConsumers;
88 59
            foreach ($subConsumers as $consumer) {
89 54
                if (!\in_array($consumer, $found)) {
90 54
                    $found[] = $consumer;
91
                }
92
            }
93 59
        } while (\next($found) !== false);
94 59
        return $found;
95
    }
96
97
    /**
98
     * Parses the raw header value into header parts.
99
     *
100
     * Calls splitTokens to split the value into token part strings, then calls
101
     * parseParts to parse the returned array.
102
     *
103
     * @return \ZBateson\MailMimeParser\Header\IHeaderPart[] the array of parsed
104
     *         parts
105
     */
106 162
    private function parseRawValue(string $value) : array
107
    {
108 162
        $tokens = $this->splitRawValue($value);
109 162
        return $this->parseTokensIntoParts(new NoRewindIterator(new ArrayIterator($tokens)));
110
    }
111
112
    /**
113
     * Returns an array of regular expression separators specific to this
114
     * consumer.
115
     *
116
     * The returned patterns are used to split the header value into tokens for
117
     * the consumer to parse into parts.
118
     *
119
     * Each array element makes part of a generated regular expression that is
120
     * used in a call to preg_split().  RegEx patterns can be used, and care
121
     * should be taken to escape special characters.
122
     *
123
     * @return string[] Array of regex patterns.
124
     */
125
    abstract protected function getTokenSeparators() : array;
126
127
    /**
128
     * Returns a list of regular expression markers for this consumer and all
129
     * sub-consumers by calling getTokenSeparators().
130
     *
131
     * @return string[] Array of regular expression markers.
132
     */
133 59
    protected function getAllTokenSeparators() : array
134
    {
135 59
        $markers = $this->getTokenSeparators();
136 59
        $subConsumers = $this->getAllConsumers();
137 59
        foreach ($subConsumers as $consumer) {
138 59
            $markers = \array_merge($consumer->getTokenSeparators(), $markers);
139
        }
140 59
        return \array_unique($markers);
141
    }
142
143
    /**
144
     * Returns a regex pattern used to split the input header string.
145
     *
146
     * The default implementation calls
147
     * {@see AbstractConsumerService::getAllTokenSeparators()} and implodes the
148
     * returned array with the regex OR '|' character as its glue.
149
     *
150
     * @return string the regex pattern
151
     */
152 40
    protected function getTokenSplitPattern() : string
153
    {
154 40
        $sChars = \implode('|', $this->getAllTokenSeparators());
155 40
        $mimePartPattern = MimeLiteralPart::MIME_PART_PATTERN;
156 40
        return '~(' . $mimePartPattern . '|\\\\.|' . $sChars . ')~';
157
    }
158
159
    /**
160
     * Returns an array of split tokens from the input string.
161
     *
162
     * The method calls preg_split using
163
     * {@see AbstractConsumerService::getTokenSplitPattern()}.  The split array
164
     * will not contain any empty parts and will contain the markers.
165
     *
166
     * @param string $rawValue the raw string
167
     * @return string[] the array of tokens
168
     */
169 162
    protected function splitRawValue($rawValue) : array
170
    {
171 162
        if ($this->tokenSplitPattern === null) {
172 59
            $this->tokenSplitPattern = $this->getTokenSplitPattern();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getTokenSplitPattern() of type string is incompatible with the declared type ZBateson\MailMimeParser\Header\Consumer\the of property $tokenSplitPattern.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
173 59
            $this->logger->debug(
174 59
                'Configuring ${class} with token split pattern: ${pattern}',
175 59
                [ 'class' => static::class, 'pattern' => $this->tokenSplitPattern]
176 59
            );
177
        }
178 162
        return \preg_split(
179 162
            $this->tokenSplitPattern,
180 162
            $rawValue,
181 162
            -1,
182 162
            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
183 162
        );
184
    }
185
186
    /**
187
     * Returns true if the passed string token marks the beginning marker for
188
     * the current consumer.
189
     *
190
     * @param string $token The current token
191
     */
192
    abstract protected function isStartToken(string $token) : bool;
193
194
    /**
195
     * Returns true if the passed string token marks the end marker for the
196
     * current consumer.
197
     *
198
     * @param string $token The current token
199
     */
200
    abstract protected function isEndToken(string $token) : bool;
201
202
    /**
203
     * Constructs and returns an IHeaderPart for the passed string token.
204
     *
205
     * If the token should be ignored, the function must return null.
206
     *
207
     * The default created part uses the instance's partFactory->newInstance
208
     * method.
209
     *
210
     * @param string $token the token
211
     * @param bool $isLiteral set to true if the token represents a literal -
212
     *        e.g. an escaped token
213
     * @return ?IHeaderPart The constructed header part or null if the token
214
     *         should be ignored.
215
     */
216 121
    protected function getPartForToken(string $token, bool $isLiteral) : ?IHeaderPart
217
    {
218 121
        if ($isLiteral) {
219 2
            return $this->partFactory->newLiteralPart($token);
220 121
        } elseif (\preg_match('/^\s+$/', $token)) {
221 107
            return $this->partFactory->newToken(' ');
222
        }
223
        // can be overridden with custom PartFactory
224 121
        return $this->partFactory->newInstance($token);
225
    }
226
227
    /**
228
     * Iterates through this consumer's sub-consumers checking if the current
229
     * token triggers a sub-consumer's start token and passes control onto that
230
     * sub-consumer's parseTokenIntoParts().
231
     *
232
     * If no sub-consumer is responsible for the current token, calls
233
     * {@see AbstractConsumerService::getPartForToken()} and returns it in an
234
     * array.
235
     *
236
     * @param Iterator<string> $tokens
237
     * @return IHeaderPart[]
238
     */
239 162
    protected function getConsumerTokenParts(Iterator $tokens) : array
240
    {
241 162
        $token = $tokens->current();
242 162
        $subConsumers = $this->subConsumers;
243 162
        foreach ($subConsumers as $consumer) {
244 157
            if ($consumer->isStartToken($token)) {
245 135
                $this->logger->debug(
246 135
                    'Token: "${value}" in ${class} starting sub-consumer ${consumer}',
247 135
                    [ 'value' => $token, 'class' => static::class, 'consumer' => get_class($consumer) ]
248 135
                );
249 135
                $this->advanceToNextToken($tokens, true);
250 135
                return $consumer->parseTokensIntoParts($tokens);
251
            }
252
        }
253 162
        return [$this->getPartForToken($token, false)];
254
    }
255
256
    /**
257
     * Returns an array of IHeaderPart for the current token on the iterator.
258
     *
259
     * If the current token is a start token from a sub-consumer, the sub-
260
     * consumer's {@see AbstractConsumerService::parseTokensIntoParts()} method
261
     * is called.
262
     *
263
     * @param Iterator<string> $tokens The token iterator.
264
     * @return IHeaderPart[]
265
     */
266 160
    protected function getTokenParts(Iterator $tokens) : array
267
    {
268 160
        $token = $tokens->current();
269 160
        if (\strlen($token) === 2 && $token[0] === '\\') {
270 9
            return [$this->getPartForToken(\substr($token, 1), true)];
271
        }
272 160
        return $this->getConsumerTokenParts($tokens);
273
    }
274
275
    /**
276
     * Determines if the iterator should be advanced to the next token after
277
     * reading tokens or finding a start token.
278
     *
279
     * The default implementation will advance for a start token, but not
280
     * advance on the end token of the current consumer, allowing the end token
281
     * to be passed up to a higher-level consumer.
282
     *
283
     * @param Iterator $tokens The token iterator.
284
     * @param bool $isStartToken true for the start token.
285
     */
286 159
    protected function advanceToNextToken(Iterator $tokens, bool $isStartToken) : AbstractConsumerService
287
    {
288 159
        $checkEndToken = (!$isStartToken && $tokens->valid());
289 159
        $isEndToken = ($checkEndToken && $this->isEndToken($tokens->current()));
290 159
        if (($isStartToken) || ($checkEndToken && !$isEndToken)) {
291 159
            $tokens->next();
292
        }
293 159
        return $this;
294
    }
295
296
    /**
297
     * Iterates over the passed token Iterator and returns an array of parsed
298
     * IHeaderPart objects.
299
     *
300
     * The method checks each token to see if the token matches a sub-consumer's
301
     * start token, or if it matches the current consumer's end token to stop
302
     * processing.
303
     *
304
     * If a sub-consumer's start token is matched, the sub-consumer is invoked
305
     * and its returned parts are merged to the current consumer's header parts.
306
     *
307
     * After all tokens are read and an array of Header\Parts are constructed,
308
     * the array is passed to {@see AbstractConsumerService::processParts} for
309
     * any final processing.
310
     *
311
     * @param Iterator<string> $tokens An iterator over a string of tokens
312
     * @return IHeaderPart[] An array of parsed parts
313
     */
314 162
    protected function parseTokensIntoParts(Iterator $tokens) : array
315
    {
316 162
        $parts = [];
317 162
        while ($tokens->valid() && !$this->isEndToken($tokens->current())) {
318 162
            $this->logger->debug('Parsing token: ${token} in consumer: ${consumer}');
319 162
            $parts = \array_merge($parts, $this->getTokenParts($tokens));
320 162
            $this->advanceToNextToken($tokens, false);
321
        }
322 162
        return $this->processParts($parts);
323
    }
324
325
    /**
326
     * Performs any final processing on the array of parsed parts before
327
     * returning it to the consumer client.
328
     *
329
     * The default implementation simply returns the passed array after
330
     * filtering out null/empty parts.
331
     *
332
     * @param IHeaderPart[] $parts The parsed parts.
333
     * @return IHeaderPart[] Array of resulting final parts.
334
     */
335 109
    protected function processParts(array $parts) : array
336
    {
337 109
        return \array_values(\array_filter($parts));
338
    }
339
}
340