AbstractConsumerService::getAllConsumers()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 9
c 0
b 0
f 0
dl 0
loc 13
ccs 9
cts 9
cp 1
rs 9.9666
cc 4
nc 3
nop 0
crap 4
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 Psr\Log\LoggerInterface;
14
use ZBateson\MailMimeParser\Header\IHeaderPart;
15
use ZBateson\MailMimeParser\Header\Part\HeaderPartFactory;
16
use ZBateson\MailMimeParser\Header\Part\MimeToken;
17
18
/**
19
 * Abstract base class for all header token consumers.
20
 *
21
 * Defines the base parser that loops over tokens, consuming them and creating
22
 * header parts.
23
 *
24
 * @author Zaahid Bateson
25
 */
26
abstract class AbstractConsumerService implements IConsumerService
27
{
28
    protected LoggerInterface $logger;
29
30
    /**
31
     * @var HeaderPartFactory used to construct IHeaderPart objects
32
     */
33
    protected HeaderPartFactory $partFactory;
34
35
    /**
36
     * @var AbstractConsumerService[] array of sub-consumers used by this
37
     *      consumer if any, or an empty array if none exist.
38
     */
39
    protected array $subConsumers = [];
40
41
    /**
42
     * @var ?string the generated token split pattern on first run, so it doesn't
43
     *      need to be regenerated every time.
44
     */
45
    private ?string $tokenSplitPattern = null;
46
47
    /**
48
     * @param AbstractConsumerService[] $subConsumers
49
     */
50 82
    public function __construct(LoggerInterface $logger, HeaderPartFactory $partFactory, array $subConsumers = [])
51
    {
52 82
        $this->logger = $logger;
53 82
        $this->partFactory = $partFactory;
54 82
        $this->subConsumers = $subConsumers;
55
    }
56
57 188
    public function __invoke(string $value) : array
58
    {
59 188
        $this->logger->debug('Starting {class} for "{value}"', ['class' => static::class, 'value' => $value]);
60 188
        if ($value !== '') {
61 187
            $parts = $this->parseRawValue($value);
62 187
            $this->logger->debug(
63 187
                'Ending {class} for "{value}": parsed into {cnt} header part objects',
64 187
                ['class' => static::class, 'value' => $value, 'cnt' => \count($parts)]
65 187
            );
66 187
            return $parts;
67
        }
68 1
        return [];
69
    }
70
71
    /**
72
     * Returns this consumer and all unique sub consumers.
73
     *
74
     * Loops into the sub-consumers (and their sub-consumers, etc...) finding
75
     * all unique consumers, and returns them in an array.
76
     *
77
     * @return AbstractConsumerService[] Array of unique consumers.
78
     */
79 83
    protected function getAllConsumers() : array
80
    {
81 83
        $found = [$this];
82
        do {
83 83
            $current = \current($found);
84 83
            $subConsumers = $current->subConsumers;
85 83
            foreach ($subConsumers as $consumer) {
86 73
                if (!\in_array($consumer, $found)) {
87 73
                    $found[] = $consumer;
88
                }
89
            }
90 83
        } while (\next($found) !== false);
91 83
        return $found;
92
    }
93
94
    /**
95
     * Parses the raw header value into header parts.
96
     *
97
     * Calls splitTokens to split the value into token part strings, then calls
98
     * parseParts to parse the returned array.
99
     *
100
     * @return \ZBateson\MailMimeParser\Header\IHeaderPart[] the array of parsed
101
     *         parts
102
     */
103 187
    private function parseRawValue(string $value) : array
104
    {
105 187
        $tokens = $this->splitRawValue($value);
106 187
        return $this->parseTokensIntoParts(new NoRewindIterator(new ArrayIterator($tokens)));
107
    }
108
109
    /**
110
     * Returns an array of regular expression separators specific to this
111
     * consumer.
112
     *
113
     * The returned patterns are used to split the header value into tokens for
114
     * the consumer to parse into parts.
115
     *
116
     * Each array element makes part of a generated regular expression that is
117
     * used in a call to preg_split().  RegEx patterns can be used, and care
118
     * should be taken to escape special characters.
119
     *
120
     * @return string[] Array of regex patterns.
121
     */
122
    abstract protected function getTokenSeparators() : array;
123
124
    /**
125
     * Returns a list of regular expression markers for this consumer and all
126
     * sub-consumers by calling getTokenSeparators().
127
     *
128
     * @return string[] Array of regular expression markers.
129
     */
130 83
    protected function getAllTokenSeparators() : array
131
    {
132 83
        $markers = $this->getTokenSeparators();
133 83
        $subConsumers = $this->getAllConsumers();
134 83
        foreach ($subConsumers as $consumer) {
135 83
            $markers = \array_merge($consumer->getTokenSeparators(), $markers);
136
        }
137 83
        return \array_unique($markers);
138
    }
139
140
    /**
141
     * Returns a regex pattern used to split the input header string.
142
     *
143
     * The default implementation calls
144
     * {@see AbstractConsumerService::getAllTokenSeparators()} and implodes the
145
     * returned array with the regex OR '|' character as its glue.
146
     *
147
     * @return string the regex pattern
148
     */
149 65
    protected function getTokenSplitPattern() : string
150
    {
151 65
        $sChars = \implode('|', $this->getAllTokenSeparators());
152 65
        $mimePartPattern = MimeToken::MIME_PART_PATTERN;
153 65
        return '~(' . $mimePartPattern . '|\\\\\r\n|\\\\.|' . $sChars . ')~ms';
154
    }
155
156
    /**
157
     * Returns an array of split tokens from the input string.
158
     *
159
     * The method calls preg_split using
160
     * {@see AbstractConsumerService::getTokenSplitPattern()}.  The split array
161
     * will not contain any empty parts and will contain the markers.
162
     *
163
     * @param string $rawValue the raw string
164
     * @return string[] the array of tokens
165
     */
166 187
    protected function splitRawValue($rawValue) : array
167
    {
168 187
        if ($this->tokenSplitPattern === null) {
169 83
            $this->tokenSplitPattern = $this->getTokenSplitPattern();
170 83
            $this->logger->debug(
171 83
                'Configuring {class} with token split pattern: {pattern}',
172 83
                ['class' => static::class, 'pattern' => $this->tokenSplitPattern]
173 83
            );
174
        }
175 187
        return \preg_split(
176 187
            $this->tokenSplitPattern,
177 187
            $rawValue,
178 187
            -1,
179 187
            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
180 187
        );
181
    }
182
183
    /**
184
     * Returns true if the passed string token marks the beginning marker for
185
     * the current consumer.
186
     *
187
     * @param string $token The current token
188
     */
189
    abstract protected function isStartToken(string $token) : bool;
190
191
    /**
192
     * Returns true if the passed string token marks the end marker for the
193
     * current consumer.
194
     *
195
     * @param string $token The current token
196
     */
197
    abstract protected function isEndToken(string $token) : bool;
198
199
    /**
200
     * Constructs and returns an IHeaderPart for the passed string token.
201
     *
202
     * If the token should be ignored, the function must return null.
203
     *
204
     * The default created part uses the instance's partFactory->newInstance
205
     * method.
206
     *
207
     * @param string $token the token
208
     * @param bool $isLiteral set to true if the token represents a literal -
209
     *        e.g. an escaped token
210
     * @return ?IHeaderPart The constructed header part or null if the token
211
     *         should be ignored.
212
     */
213 160
    protected function getPartForToken(string $token, bool $isLiteral) : ?IHeaderPart
214
    {
215 160
        if ($isLiteral) {
216 3
            return $this->partFactory->newToken($token, true);
217
        }
218
        // can be overridden with custom PartFactory
219 160
        return $this->partFactory->newInstance($token);
220
    }
221
222
    /**
223
     * Iterates through this consumer's sub-consumers checking if the current
224
     * token triggers a sub-consumer's start token and passes control onto that
225
     * sub-consumer's parseTokenIntoParts().
226
     *
227
     * If no sub-consumer is responsible for the current token, calls
228
     * {@see AbstractConsumerService::getPartForToken()} and returns it in an
229
     * array.
230
     *
231
     * @param Iterator<string> $tokens
232
     * @return IHeaderPart[]
233
     */
234 187
    protected function getConsumerTokenParts(Iterator $tokens) : array
235
    {
236 187
        $token = $tokens->current();
237 187
        $subConsumers = $this->subConsumers;
238 187
        foreach ($subConsumers as $consumer) {
239 177
            if ($consumer->isStartToken($token)) {
240 158
                $this->logger->debug(
241 158
                    'Token: "{value}" in {class} starting sub-consumer {consumer}',
242 158
                    ['value' => $token, 'class' => static::class, 'consumer' => \get_class($consumer)]
243 158
                );
244 158
                $this->advanceToNextToken($tokens, true);
245 158
                return $consumer->parseTokensIntoParts($tokens);
246
            }
247
        }
248 187
        $part = $this->getPartForToken($token, false);
249 187
        return ($part !== null) ? [$part] : [];
250
    }
251
252
    /**
253
     * Returns an array of IHeaderPart for the current token on the iterator.
254
     *
255
     * If the current token is a start token from a sub-consumer, the sub-
256
     * consumer's {@see AbstractConsumerService::parseTokensIntoParts()} method
257
     * is called.
258
     *
259
     * @param Iterator<string> $tokens The token iterator.
260
     * @return IHeaderPart[]
261
     */
262 185
    protected function getTokenParts(Iterator $tokens) : array
263
    {
264 185
        $token = $tokens->current();
265 185
        if ($token === "\\\r\n" || (\strlen($token) === 2 && $token[0] === '\\')) {
266 11
            $part = $this->getPartForToken(\substr($token, 1), true);
267 11
            return ($part !== null) ? [$part] : [];
268
        }
269 185
        return $this->getConsumerTokenParts($tokens);
270
    }
271
272
    /**
273
     * Determines if the iterator should be advanced to the next token after
274
     * reading tokens or finding a start token.
275
     *
276
     * The default implementation will advance for a start token, but not
277
     * advance on the end token of the current consumer, allowing the end token
278
     * to be passed up to a higher-level consumer.
279
     *
280
     * @param Iterator $tokens The token iterator.
281
     * @param bool $isStartToken true for the start token.
282
     */
283 184
    protected function advanceToNextToken(Iterator $tokens, bool $isStartToken) : static
284
    {
285 184
        $checkEndToken = (!$isStartToken && $tokens->valid());
286 184
        $isEndToken = ($checkEndToken && $this->isEndToken($tokens->current()));
287 184
        if (($isStartToken) || ($checkEndToken && !$isEndToken)) {
288 184
            $tokens->next();
289
        }
290 184
        return $this;
291
    }
292
293
    /**
294
     * Iterates over the passed token Iterator and returns an array of parsed
295
     * IHeaderPart objects.
296
     *
297
     * The method checks each token to see if the token matches a sub-consumer's
298
     * start token, or if it matches the current consumer's end token to stop
299
     * processing.
300
     *
301
     * If a sub-consumer's start token is matched, the sub-consumer is invoked
302
     * and its returned parts are merged to the current consumer's header parts.
303
     *
304
     * After all tokens are read and an array of Header\Parts are constructed,
305
     * the array is passed to {@see AbstractConsumerService::processParts} for
306
     * any final processing if there are any parts.
307
     *
308
     * @param Iterator<string> $tokens An iterator over a string of tokens
309
     * @return IHeaderPart[] An array of parsed parts
310
     */
311 187
    protected function parseTokensIntoParts(Iterator $tokens) : array
312
    {
313 187
        $parts = [];
314 187
        while ($tokens->valid() && !$this->isEndToken($tokens->current())) {
315 187
            $this->logger->debug('Parsing token: {token} in class: {consumer}', ['token' => $tokens->current(), 'consumer' => static::class]);
316 187
            $parts = \array_merge($parts, $this->getTokenParts($tokens));
317 187
            $this->advanceToNextToken($tokens, false);
318
        }
319 187
        return (empty($parts)) ? [] : $this->processParts($parts);
320
    }
321
322
    /**
323
     * Performs any final processing on the array of parsed parts before
324
     * returning it to the consumer client.  The passed $parts array is
325
     * guaranteed to not be empty.
326
     *
327
     * The default implementation simply returns the passed array after
328
     * filtering out null/empty parts.
329
     *
330
     * @param IHeaderPart[] $parts The parsed parts.
331
     * @return IHeaderPart[] Array of resulting final parts.
332
     */
333 110
    protected function processParts(array $parts) : array
334
    {
335 110
        $this->logger->debug('Processing parts array {parts} in {consumer}', ['parts' => $parts, 'consumer' => static::class]);
336 110
        return $parts;
337
    }
338
}
339