VCardParser::parse()   F
last analyzed

Complexity

Conditions 38
Paths 438

Size

Total Lines 143
Code Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 38
eloc 107
nc 438
nop 0
dl 0
loc 143
rs 0.6244
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace JeroenDesloovere\VCard;
4
5
/*
6
 * This file is part of the VCard PHP Class from Jeroen Desloovere.
7
 *
8
 * For the full copyright and license information, please view the license
9
 * file that was distributed with this source code.
10
 */
11
12
use Iterator;
13
14
/**
15
 * VCard PHP Class to parse .vcard files.
16
 *
17
 * This class is heavily based on the Zendvcard project (seemingly abandoned),
18
 * which is licensed under the Apache 2.0 license.
19
 * More information can be found at https://code.google.com/archive/p/zendvcard/
20
 */
21
class VCardParser implements Iterator
22
{
23
    /**
24
     * The raw VCard content.
25
    *
26
     * @var string
27
     */
28
    protected $content;
29
30
    /**
31
     * The VCard data objects.
32
     *
33
     * @var array
34
     */
35
    protected $vcardObjects;
36
37
    /**
38
     * The iterator position.
39
     *
40
     * @var int
41
     */
42
    protected $position;
43
44
    /**
45
     * Helper function to parse a file directly.
46
     *
47
     * @param string $filename
48
     * @return self
49
     */
50
    public static function parseFromFile($filename)
51
    {
52
        if (file_exists($filename) && is_readable($filename)) {
53
            return new self(file_get_contents($filename));
54
        } else {
55
            throw new \RuntimeException(sprintf("File %s is not readable, or doesn't exist.", $filename));
56
        }
57
    }
58
59
    public function __construct($content)
60
    {
61
        $this->content = $content;
62
        $this->vcardObjects = [];
63
        $this->rewind();
64
        $this->parse();
65
    }
66
67
    public function rewind()
68
    {
69
        $this->position = 0;
70
    }
71
72
    public function current()
73
    {
74
        if ($this->valid()) {
75
            return $this->getCardAtIndex($this->position);
76
        }
77
    }
78
79
    public function key()
80
    {
81
        return $this->position;
82
    }
83
84
    public function next()
85
    {
86
        $this->position++;
87
    }
88
89
    public function valid()
90
    {
91
        return !empty($this->vcardObjects[$this->position]);
92
    }
93
94
    /**
95
     * Fetch all the imported VCards.
96
     *
97
     * @return array
98
     *    A list of VCard card data objects.
99
     */
100
    public function getCards()
101
    {
102
        return $this->vcardObjects;
103
    }
104
105
    /**
106
     * Fetch the imported VCard at the specified index.
107
     *
108
     * @throws OutOfBoundsException
109
     *
110
     * @param int $i
111
     *
112
     * @return stdClass
0 ignored issues
show
Bug introduced by
The type JeroenDesloovere\VCard\stdClass was not found. Did you mean stdClass? If so, make sure to prefix the type with \.
Loading history...
113
     *    The card data object.
114
     */
115
    public function getCardAtIndex($i)
116
    {
117
        if (isset($this->vcardObjects[$i])) {
118
            return $this->vcardObjects[$i];
119
        }
120
        throw new \OutOfBoundsException();
121
    }
122
123
    /**
124
     * Start the parsing process.
125
     *
126
     * This method will populate the data object.
127
     */
128
    protected function parse()
129
    {
130
        // Normalize new lines.
131
        $this->content = str_replace(["\r\n", "\r"], "\n", $this->content);
132
133
        // RFC2425 5.8.1. Line delimiting and folding
134
        // Unfolding is accomplished by regarding CRLF immediately followed by
135
        // a white space character (namely HTAB ASCII decimal 9 or. SPACE ASCII
136
        // decimal 32) as equivalent to no characters at all (i.e., the CRLF
137
        // and single white space character are removed).
138
        $this->content = preg_replace("/\n(?:[ \t])/", "", $this->content);
139
        $lines = explode("\n", $this->content);
140
141
        // Parse the VCard, line by line.
142
        foreach ($lines as $line) {
143
            $line = trim($line);
144
145
            if (strtoupper($line) == "BEGIN:VCARD") {
146
                $cardData = new \stdClass();
147
            } elseif (strtoupper($line) == "END:VCARD") {
148
                $this->vcardObjects[] = $cardData;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cardData seems to be defined later in this foreach loop on line 146. Are you sure it is defined here?
Loading history...
149
            } elseif (!empty($line)) {
150
                // Strip grouping information. We don't use the group names. We
151
                // simply use a list for entries that have multiple values.
152
                // As per RFC, group names are alphanumerical, and end with a
153
                // period (.).
154
                $line = preg_replace('/^\w+\./', '', $line);
155
156
                $type = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $type is dead and can be removed.
Loading history...
157
                $value = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
158
                @list($type, $value) = explode(':', $line, 2);
159
160
                $types = explode(';', $type);
161
                $element = strtoupper($types[0]);
162
163
                array_shift($types);
164
165
                // Normalize types. A type can either be a type-param directly,
166
                // or can be prefixed with "type=". E.g.: "INTERNET" or
167
                // "type=INTERNET".
168
                if (!empty($types)) {
169
                    $types = array_map(function($type) {
170
                        return preg_replace('/^type=/i', '', $type);
171
                    }, $types);
172
                }
173
174
                $i = 0;
175
                $rawValue = false;
176
                foreach ($types as $type) {
177
                    if (preg_match('/base64/', strtolower($type))) {
178
                        $value = base64_decode($value);
179
                        unset($types[$i]);
180
                        $rawValue = true;
181
                    } elseif (preg_match('/encoding=b/', strtolower($type))) {
182
                        $value = base64_decode($value);
183
                        unset($types[$i]);
184
                        $rawValue = true;
185
                    } elseif (preg_match('/quoted-printable/', strtolower($type))) {
186
                        $value = quoted_printable_decode($value);
187
                        unset($types[$i]);
188
                        $rawValue = true;
189
                    } elseif (strpos(strtolower($type), 'charset=') === 0) {
190
                        try {
191
                            $value = mb_convert_encoding($value, "UTF-8", substr($type, 8));
192
                        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
193
                        }
194
                        unset($types[$i]);
195
                    }
196
                    $i++;
197
                }
198
199
                switch (strtoupper($element)) {
200
                    case 'FN':
201
                        $cardData->fullname = $value;
202
                        break;
203
                    case 'N':
204
                        foreach ($this->parseName($value) as $key => $val) {
205
                            $cardData->{$key} = $val;
206
                        }
207
                        break;
208
                    case 'BDAY':
209
                        $cardData->birthday = $this->parseBirthday($value);
210
                        break;
211
                    case 'ADR':
212
                        if (!isset($cardData->address)) {
213
                            $cardData->address = [];
214
                        }
215
                        $key = !empty($types) ? implode(';', $types) : 'WORK;POSTAL';
216
                        $cardData->address[$key][] = $this->parseAddress($value);
217
                        break;
218
                    case 'TEL':
219
                        if (!isset($cardData->phone)) {
220
                            $cardData->phone = [];
221
                        }
222
                        $key = !empty($types) ? implode(';', $types) : 'default';
223
                        $cardData->phone[$key][] = $value;
224
                        break;
225
                    case 'EMAIL':
226
                        if (!isset($cardData->email)) {
227
                            $cardData->email = [];
228
                        }
229
                        $key = !empty($types) ? implode(';', $types) : 'default';
230
                        $cardData->email[$key][] = $value;
231
                        break;
232
                    case 'REV':
233
                        $cardData->revision = $value;
234
                        break;
235
                    case 'VERSION':
236
                        $cardData->version = $value;
237
                        break;
238
                    case 'ORG':
239
                        $cardData->organization = $value;
240
                        break;
241
                    case 'URL':
242
                        if (!isset($cardData->url)) {
243
                            $cardData->url = [];
244
                        }
245
                        $key = !empty($types) ? implode(';', $types) : 'default';
246
                        $cardData->url[$key][] = $value;
247
                        break;
248
                    case 'TITLE':
249
                        $cardData->title = $value;
250
                        break;
251
                    case 'PHOTO':
252
                        if ($rawValue) {
253
                            $cardData->rawPhoto = $value;
254
                        } else {
255
                            $cardData->photo = $value;
256
                        }
257
                        break;
258
                    case 'LOGO':
259
                        if ($rawValue) {
260
                            $cardData->rawLogo = $value;
261
                        } else {
262
                            $cardData->logo = $value;
263
                        }
264
                        break;
265
                    case 'NOTE':
266
                        $cardData->note = $this->unescape($value);
267
                        break;
268
                    case 'CATEGORIES':
269
                        $cardData->categories = array_map('trim', explode(',', $value));
270
                        break;
271
                }
272
            }
273
        }
274
    }
275
276
    protected function parseName($value)
277
    {
278
        @list(
279
            $lastname,
280
            $firstname,
281
            $additional,
282
            $prefix,
283
            $suffix
284
        ) = explode(';', $value);
285
        return (object) [
286
            'lastname' => $lastname,
287
            'firstname' => $firstname,
288
            'additional' => $additional,
289
            'prefix' => $prefix,
290
            'suffix' => $suffix,
291
        ];
292
    }
293
294
    protected function parseBirthday($value)
295
    {
296
        return new \DateTime($value);
297
    }
298
299
    protected function parseAddress($value)
300
    {
301
        @list(
302
            $name,
303
            $extended,
304
            $street,
305
            $city,
306
            $region,
307
            $zip,
308
            $country,
309
        ) = explode(';', $value);
310
        return (object) [
311
            'name' => $name,
312
            'extended' => $extended,
313
            'street' => $street,
314
            'city' => $city,
315
            'region' => $region,
316
            'zip' => $zip,
317
            'country' => $country,
318
        ];
319
    }
320
321
    /**
322
     * Unescape newline characters according to RFC2425 section 5.8.4.
323
     * This function will replace escaped line breaks with PHP_EOL.
324
     *
325
     * @link http://tools.ietf.org/html/rfc2425#section-5.8.4
326
     * @param  string $text
327
     * @return string
328
     */
329
    protected function unescape($text)
330
    {
331
        return str_replace("\\n", PHP_EOL, $text);
332
    }
333
}
334