1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace JeroenDesloovere\VCard\Service; |
4
|
|
|
|
5
|
|
|
use Behat\Transliterator\Transliterator; |
6
|
|
|
use JeroenDesloovere\VCard\Exception\ElementAlreadyExistsException; |
7
|
|
|
use JeroenDesloovere\VCard\Model\VCard; |
8
|
|
|
use JeroenDesloovere\VCard\Model\VCardAddress; |
9
|
|
|
use JeroenDesloovere\VCard\Model\VCardMedia; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Class PropertyService |
13
|
|
|
* |
14
|
|
|
* @package JeroenDesloovere\VCard\Util |
15
|
|
|
*/ |
16
|
|
|
class PropertyService |
17
|
|
|
{ |
18
|
|
|
/** |
19
|
|
|
* definedElements |
20
|
|
|
* |
21
|
|
|
* @var array |
22
|
|
|
*/ |
23
|
|
|
private $definedElements; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* FileName |
27
|
|
|
* |
28
|
|
|
* @var string|null |
29
|
|
|
*/ |
30
|
|
|
private $fileName; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Multiple properties for element allowed |
34
|
|
|
* |
35
|
|
|
* @var array |
36
|
|
|
*/ |
37
|
|
|
private static $multiplePropertiesForElementAllowed = [ |
38
|
|
|
'email', |
39
|
|
|
'address', |
40
|
|
|
'phoneNumber', |
41
|
|
|
'url', |
42
|
|
|
]; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Properties |
46
|
|
|
* |
47
|
|
|
* @var array |
48
|
|
|
*/ |
49
|
|
|
private $properties = []; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* Default Charset |
53
|
|
|
* |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
private $charset; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @var VCard[] |
60
|
|
|
*/ |
61
|
|
|
private $vCards; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* PropertyService constructor. |
65
|
|
|
* |
66
|
|
|
* @param VCard|VCard[] $vCard |
67
|
|
|
* @param string $charset |
68
|
|
|
* |
69
|
|
|
* @throws ElementAlreadyExistsException |
70
|
|
|
*/ |
71
|
|
|
public function __construct($vCard, $charset = 'utf-8') |
72
|
|
|
{ |
73
|
|
|
$this->vCards = $vCard; |
|
|
|
|
74
|
|
|
if (!\is_array($vCard)) { |
75
|
|
|
$this->vCards = [$vCard]; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
$this->charset = $charset; |
79
|
|
|
|
80
|
|
|
$this->parseVCarts(); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Get filename |
85
|
|
|
* |
86
|
|
|
* @return string|null |
87
|
|
|
*/ |
88
|
|
|
public function getFileName(): ?string |
89
|
|
|
{ |
90
|
|
|
return $this->fileName; |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Get properties |
95
|
|
|
* |
96
|
|
|
* @return array |
97
|
|
|
*/ |
98
|
|
|
public function getProperties(): array |
99
|
|
|
{ |
100
|
|
|
return $this->properties; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Get charset string |
105
|
|
|
* |
106
|
|
|
* @return string |
107
|
|
|
*/ |
108
|
|
|
private function getCharsetString(): string |
109
|
|
|
{ |
110
|
|
|
$charsetString = ''; |
111
|
|
|
|
112
|
|
|
if ($this->charset === 'utf-8') { |
113
|
|
|
$charsetString = ';CHARSET=UTF-8'; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
return $charsetString; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Set filename |
121
|
|
|
* |
122
|
|
|
* @param string|array $value |
123
|
|
|
* @param bool $overwrite [optional] Default overwrite is true |
124
|
|
|
* @param string $separator [optional] Default separator is an underscore '_' |
125
|
|
|
* @return void |
126
|
|
|
*/ |
127
|
|
|
private function setFileName($value, $overwrite = true, $separator = '_'): void |
128
|
|
|
{ |
129
|
|
|
// recast to string if $value is array |
130
|
|
|
if (\is_array($value)) { |
131
|
|
|
$value = implode($separator, $value); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
// trim unneeded values |
135
|
|
|
$value = trim($value, $separator); |
136
|
|
|
|
137
|
|
|
// remove all spaces |
138
|
|
|
$value = preg_replace('/\s+/', $separator, $value); |
139
|
|
|
|
140
|
|
|
// if value is empty, stop here |
141
|
|
|
if (empty($value)) { |
142
|
|
|
return; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
// decode value |
146
|
|
|
$value = Transliterator::transliterate($value); |
147
|
|
|
|
148
|
|
|
// lowercase the string |
149
|
|
|
$value = strtolower($value); |
150
|
|
|
|
151
|
|
|
// urlize this part |
152
|
|
|
$value = Transliterator::urlize($value); |
153
|
|
|
|
154
|
|
|
// overwrite filename or add to filename using a prefix in between |
155
|
|
|
$this->fileName = $overwrite ? |
156
|
|
|
$value : $this->fileName.$separator.$value; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Has property |
161
|
|
|
* |
162
|
|
|
* @param string $key |
163
|
|
|
* @return bool |
164
|
|
|
*/ |
165
|
|
|
private function hasProperty(string $key): bool |
166
|
|
|
{ |
167
|
|
|
$properties = $this->getProperties(); |
168
|
|
|
|
169
|
|
|
foreach ($properties as $property) { |
170
|
|
|
if ($property['key'] === $key && $property['value'] !== '') { |
171
|
|
|
return true; |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
return false; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* @throws ElementAlreadyExistsException |
180
|
|
|
*/ |
181
|
|
|
private function parseVCarts(): void |
182
|
|
|
{ |
183
|
|
|
foreach ($this->vCards as $vCard) { |
184
|
|
|
$this->parseVCart($vCard); |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* @param VCard $vCard |
190
|
|
|
* |
191
|
|
|
* @throws ElementAlreadyExistsException |
192
|
|
|
*/ |
193
|
|
|
private function parseVCart(VCard $vCard): void |
194
|
|
|
{ |
195
|
|
|
$this->addAddress($vCard->getAddresses()); |
196
|
|
|
$this->addBirthday($vCard->getBirthday()); |
197
|
|
|
$this->addOrganization($vCard->getOrganization()); |
198
|
|
|
$this->setArrayProperty('email', 'EMAIL;INTERNET', $vCard->getEmails()); |
199
|
|
|
$this->setStringProperty('title', 'TITLE', $vCard->getTitle()); |
200
|
|
|
$this->setStringProperty('role', 'ROLE', null); // TODO add Role to \JeroenDesloovere\VCard\Model\VCard |
201
|
|
|
$this->addName($vCard->getLastName(), $vCard->getFirstName(), $vCard->getAdditional(), $vCard->getPrefix(), $vCard->getSuffix()); |
202
|
|
|
$this->setStringProperty('note', 'NOTE', $vCard->getNote()); |
203
|
|
|
$this->addCategories($vCard->getCategories()); |
204
|
|
|
$this->setArrayProperty('phoneNumber', 'TEL', $vCard->getPhones()); |
205
|
|
|
$this->setMedia('logo', 'LOGO', $vCard->getLogo()); |
206
|
|
|
$this->setMedia('photo', 'PHOTO', $vCard->getPhoto()); |
207
|
|
|
$this->setArrayProperty('url', 'URL', $vCard->getUrls()); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* @param VCardAddress[][]|null $addresses |
212
|
|
|
* |
213
|
|
|
* @throws ElementAlreadyExistsException |
214
|
|
|
*/ |
215
|
|
|
private function addAddress($addresses): void |
216
|
|
|
{ |
217
|
|
|
if ($addresses !== null) { |
218
|
|
|
foreach ($addresses as $type => $sub) { |
219
|
|
|
foreach ($sub as $address) { |
220
|
|
|
$this->setProperty( |
221
|
|
|
'address', |
222
|
|
|
'ADR'.(($type !== '') ? ';'.$type : '').$this->getCharsetString(), |
|
|
|
|
223
|
|
|
$address->getAddress() |
224
|
|
|
); |
225
|
|
|
} |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Add birthday |
232
|
|
|
* |
233
|
|
|
* @param \DateTime|null $date Format is YYYY-MM-DD |
234
|
|
|
* |
235
|
|
|
* @throws ElementAlreadyExistsException |
236
|
|
|
*/ |
237
|
|
|
private function addBirthday(?\DateTime $date): void |
238
|
|
|
{ |
239
|
|
|
if ($date !== null) { |
240
|
|
|
$this->setProperty( |
241
|
|
|
'birthday', |
242
|
|
|
'BDAY'.$this->getCharsetString(), |
243
|
|
|
$date->format('Y-m-d') |
244
|
|
|
); |
245
|
|
|
} |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Add company |
250
|
|
|
* |
251
|
|
|
* @param null|string $company |
252
|
|
|
* @param string $department |
253
|
|
|
* |
254
|
|
|
* @throws ElementAlreadyExistsException |
255
|
|
|
*/ |
256
|
|
|
private function addOrganization(?string $company, string $department = ''): void |
257
|
|
|
{ |
258
|
|
|
if ($company !== null) { |
259
|
|
|
$this->setProperty( |
260
|
|
|
'organization', |
261
|
|
|
'ORG'.$this->getCharsetString(), |
262
|
|
|
$company.($department !== '' ? ';'.$department : '') |
263
|
|
|
); |
264
|
|
|
|
265
|
|
|
// if filename is empty, add to filename |
266
|
|
|
if ($this->fileName === null) { |
267
|
|
|
$this->setFileName($company); |
268
|
|
|
} |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Add name |
274
|
|
|
* |
275
|
|
|
* @param string $lastName [optional] |
276
|
|
|
* @param string $firstName [optional] |
277
|
|
|
* @param string $additional [optional] |
278
|
|
|
* @param string $prefix [optional] |
279
|
|
|
* @param string $suffix [optional] |
280
|
|
|
* |
281
|
|
|
* @throws ElementAlreadyExistsException |
282
|
|
|
*/ |
283
|
|
|
private function addName( |
284
|
|
|
?string $lastName = '', |
285
|
|
|
?string $firstName = '', |
286
|
|
|
?string $additional = '', |
287
|
|
|
?string $prefix = '', |
288
|
|
|
?string $suffix = '' |
289
|
|
|
): void { |
290
|
|
|
if ($lastName !== null) { |
291
|
|
|
// define values with non-empty values |
292
|
|
|
$values = array_filter( |
293
|
|
|
[ |
294
|
|
|
$prefix, |
295
|
|
|
$firstName, |
296
|
|
|
$additional, |
297
|
|
|
$lastName, |
298
|
|
|
$suffix, |
299
|
|
|
] |
300
|
|
|
); |
301
|
|
|
|
302
|
|
|
// define filename |
303
|
|
|
$this->setFileName($values); |
304
|
|
|
|
305
|
|
|
// set property |
306
|
|
|
$property = $lastName.';'.$firstName.';'.$additional.';'.$prefix.';'.$suffix; |
307
|
|
|
$this->setProperty( |
308
|
|
|
'name', |
309
|
|
|
'N'.$this->getCharsetString(), |
310
|
|
|
$property |
311
|
|
|
); |
312
|
|
|
|
313
|
|
|
// is property FN set? |
314
|
|
|
if (!$this->hasProperty('FN'.$this->getCharsetString())) { |
315
|
|
|
// set property |
316
|
|
|
$this->setProperty( |
317
|
|
|
'fullname', |
318
|
|
|
'FN'.$this->getCharsetString(), |
319
|
|
|
trim(implode(' ', $values)) |
320
|
|
|
); |
321
|
|
|
} |
322
|
|
|
} |
323
|
|
|
} |
324
|
|
|
|
325
|
|
|
/** |
326
|
|
|
* Add categories |
327
|
|
|
* |
328
|
|
|
* @param null|array $categories |
329
|
|
|
* |
330
|
|
|
* @throws ElementAlreadyExistsException |
331
|
|
|
*/ |
332
|
|
|
private function addCategories(?array $categories): void |
333
|
|
|
{ |
334
|
|
|
if ($categories !== null) { |
335
|
|
|
$this->setProperty( |
336
|
|
|
'categories', |
337
|
|
|
'CATEGORIES'.$this->getCharsetString(), |
338
|
|
|
trim(implode(',', $categories)) |
339
|
|
|
); |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Add Array |
345
|
|
|
* |
346
|
|
|
* @param string $element |
347
|
|
|
* @param string $property |
348
|
|
|
* @param null|string[][] $values |
349
|
|
|
* |
350
|
|
|
* @throws ElementAlreadyExistsException |
351
|
|
|
*/ |
352
|
|
|
private function setArrayProperty(string $element, string $property, $values): void |
353
|
|
|
{ |
354
|
|
|
if ($values !== null) { |
355
|
|
|
foreach ($values as $type => $sub) { |
356
|
|
|
foreach ($sub as $url) { |
357
|
|
|
$this->setProperty( |
358
|
|
|
$element, |
359
|
|
|
$property.(($type !== '') ? ';'.$type : '').$this->getCharsetString(), |
|
|
|
|
360
|
|
|
$url |
361
|
|
|
); |
362
|
|
|
} |
363
|
|
|
} |
364
|
|
|
} |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Set string property |
369
|
|
|
* |
370
|
|
|
* @param string $element |
371
|
|
|
* @param string $property |
372
|
|
|
* @param null|string $value |
373
|
|
|
* |
374
|
|
|
* @throws ElementAlreadyExistsException |
375
|
|
|
*/ |
376
|
|
|
private function setStringProperty(string $element, string $property, ?string $value): void |
377
|
|
|
{ |
378
|
|
|
if ($value !== null) { |
379
|
|
|
$this->setProperty( |
380
|
|
|
$element, |
381
|
|
|
$property.$this->getCharsetString(), |
382
|
|
|
$value |
383
|
|
|
); |
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Set Media |
389
|
|
|
* |
390
|
|
|
* @param string $element |
391
|
|
|
* @param string $property |
392
|
|
|
* @param VCardMedia|null $media |
393
|
|
|
* |
394
|
|
|
* @throws ElementAlreadyExistsException |
395
|
|
|
*/ |
396
|
|
|
private function setMedia(string $element, string $property, ?VCardMedia $media): void |
397
|
|
|
{ |
398
|
|
|
if ($media !== null) { |
399
|
|
|
$result = []; |
400
|
|
|
|
401
|
|
|
if ($media->getUrl() !== null) { |
402
|
|
|
$result = $media->builderUrl($property); |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
if ($media->getRaw() !== null) { |
406
|
|
|
$result = $media->builderRaw($property); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
if ($media->getUrl() !== null || $media->getRaw() !== null) { |
410
|
|
|
$this->setProperty( |
411
|
|
|
$element, |
412
|
|
|
$result['key'], |
413
|
|
|
$result['value'] |
414
|
|
|
); |
415
|
|
|
} |
416
|
|
|
} |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Set property |
421
|
|
|
* |
422
|
|
|
* @param string $element The element name you want to set, f.e.: name, email, phoneNumber, ... |
423
|
|
|
* @param string $key |
424
|
|
|
* @param string $value |
425
|
|
|
* |
426
|
|
|
* @throws ElementAlreadyExistsException |
427
|
|
|
*/ |
428
|
|
|
private function setProperty(string $element, string $key, string $value): void |
429
|
|
|
{ |
430
|
|
|
if (isset($this->definedElements[$element]) |
431
|
|
|
&& !\in_array($element, $this::$multiplePropertiesForElementAllowed, true)) { |
432
|
|
|
throw new ElementAlreadyExistsException($element); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
// we define that we set this element |
436
|
|
|
$this->definedElements[$element] = true; |
437
|
|
|
|
438
|
|
|
// adding property |
439
|
|
|
$this->properties[] = [ |
440
|
|
|
'key' => $key, |
441
|
|
|
'value' => $value, |
442
|
|
|
]; |
443
|
|
|
} |
444
|
|
|
} |
445
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.