JsonLD::buildLocation()   B
last analyzed

Complexity

Conditions 8
Paths 9

Size

Total Lines 25
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 17
nc 9
nop 4
dl 0
loc 25
rs 8.4444
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace SKien\JsonLD;
5
6
/**
7
 * Base class of the package.
8
 * The class provides all basic functions for generating valid JsonLD objects of
9
 * different types.
10
 *
11
 * @link https://search.google.com/structured-data/testing-tool
12
 * @link https://developers.google.com/search/docs/guides/sd-policies
13
 * @link https://www.w3.org/TR/json-ld11/
14
 *
15
 * @package JsonLD
16
 * @author Stefanius <[email protected]>
17
 * @copyright MIT License - see the LICENSE file for details
18
 */
19
class JsonLD
20
{
21
    /** constants for JsonLD type */
22
    public const __TYPE = '';
23
    /** type: LocalBusiness or subtype  */
24
    public const LOCAL_BUSINESS = 0;
25
    /** type: Article, NewsArticle,   */
26
    public const ARTICLE = 1;
27
    /** type: Event   */
28
    public const EVENT = 2;
29
30
    /** constants for validation */
31
    public const __VALIDATION = '';
32
    /** validation for string   */
33
    public const STRING = 0;
34
    /** validation for date   */
35
    public const DATE = 1;
36
    /** validation for time   */
37
    public const TIME = 2;
38
    /** validation for e-mail   */
39
    public const EMAIL = 3;
40
    /** validation for url   */
41
    public const URL = 4;
42
    /** validation for longitude or latitude   */
43
    public const LONG_LAT = 5;
44
45
    /** @var int    internal object type     */
46
    protected $iType = -1;
47
    /** @var array<mixed>  the linked data as array     */
48
    protected $aJsonLD = null;
49
    /** @var bool   object is nested into another object     */
50
    protected $bIsChild = false;
51
52
    /**
53
     * Instanciation of JsonLD object.
54
     * @param int $iType        internal type
55
     * @param string $strType   type for JsonLD
56
     * @param bool $bIsChild
57
     */
58
    public function __construct(int $iType, string $strType, bool $bIsChild = false)
59
    {
60
        $this->iType = $iType;
61
        $this->bIsChild = $bIsChild;
62
        $this->aJsonLD = [
63
            "@context" => "https://schema.org",
64
            "@type" => $strType,
65
        ];
66
    }
67
68
    /**
69
     * Set description text.
70
     * Usable for all JsonLD types.
71
     * @param string $strDescription
72
     */
73
    public function setDescription(string $strDescription) : void
74
    {
75
        $this->setProperty("description", $strDescription);
76
    }
77
78
    /**
79
     * Set location for the object.
80
     * @param string $strName           Name for the location
81
     * @param string|float $latitude    latidude
82
     * @param string|float $longitude   longitude
83
     * @param string $strMap            URL to a map that shows the location
84
     */
85
    public function setLocation(string $strName, $latitude, $longitude, string $strMap = '') : void
86
    {
87
        $aLocation = $this->buildLocation($strName, $latitude, $longitude, $strMap);
88
        if ($aLocation != null) {
89
            if (isset($this->aJsonLD["location"]) && is_array($this->aJsonLD["location"])) {
90
                $this->aJsonLD["location"] = array_merge($this->aJsonLD["location"], $aLocation);
91
            } else {
92
                $this->aJsonLD["location"] = $aLocation;
93
            }
94
        }
95
    }
96
97
    /**
98
     * Add an image.
99
     * Multiple images are supported. Only existing images can be set.
100
     * Pixel size (width/height) is detected from image file.
101
     * @param string $strImageURL
102
     */
103
    public function addImage(string $strImageURL) : void
104
    {
105
        $aImg = $this->buildImageObject($strImageURL);
106
        if ($aImg != null) {
107
            if (isset($this->aJsonLD["image"])) {
108
                if (isset($this->aJsonLD["image"]["@type"])) {
109
                    // only one image set so far... change to array for multiple images
110
                    $aFirstImg = $this->aJsonLD["image"];
111
                    $this->aJsonLD["image"] = array();
112
                    $this->aJsonLD["image"][] = $aFirstImg;
113
                }
114
                $this->aJsonLD["image"][] = $aImg;
115
            } else {
116
                // first image - set property direct
117
                $this->aJsonLD["image"] = $aImg;
118
            }
119
        }
120
    }
121
122
    /**
123
     * Build ImageObject property.
124
     * Logos and images are defiend as ImageObject.
125
     * @param string $strURL    URL to a valid image (PNG, GIF, JPG)
126
     * @return array<string,int|string>    array containing the property or null if invalid URL
127
     */
128
    protected function buildImageObject(string $strURL) : ?array
129
    {
130
        $aLogo = null;
131
132
        // use curl to be independet of [allow_url_fopen] enabled on the system
133
        $curl = curl_init();
134
        curl_setopt($curl, CURLOPT_URL, $strURL);
135
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
136
137
        $image = curl_exec($curl);
138
        $iReturnCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
139
        curl_close($curl);
140
141
        if ($iReturnCode == 200 && is_string($image)) {
142
            $img = imagecreatefromstring($image);
143
            if ($img !== false) {
144
                $aLogo = array(
145
                    "@type" => "ImageObject",
146
                    "url" => $strURL,
147
                    "width" => imagesx($img),
148
                    "height" => imagesy($img),
149
                );
150
            }
151
        }
152
        return $aLogo;
153
    }
154
155
    /**
156
     * Build a postal adress object.
157
     * @param string $strStreet
158
     * @param string $strPostcode
159
     * @param string $strCity
160
     * @param string $strRegion
161
     * @param string $strCountry
162
     * @return array<string>    array containing the property
163
     */
164
    protected function buildAddress(string $strStreet, string $strPostcode, string $strCity, string $strRegion = '', string $strCountry = '') : array
165
    {
166
        $aAdress = array("@type" => "PostalAddress");
167
        if (strlen($strStreet) > 0) {
168
            $aAdress["streetAddress"] = $this->validString($strStreet);
169
        }
170
        if (strlen($strPostcode) > 0) {
171
            $aAdress["postalCode"] = $this->validString($strPostcode);
172
        }
173
        if (strlen($strCity) > 0) {
174
            $aAdress["addressLocality"] = $this->validString($strCity);
175
        }
176
        if (strlen($strCountry) > 0) {
177
            $aAdress["addressCountry"] = $this->validString($strCountry);
178
        }
179
        if (strlen($strCountry) > 0) {
180
            $aAdress["addressRegion"] = $this->validString($strRegion);
181
        }
182
        return $aAdress;
183
    }
184
185
    /**
186
     * Build a location object.
187
     * @param string $strName       Name for the location
188
     * @param string|float $latitude
189
     * @param string|float $longitude
190
     * @param string $strMap        URL to map show the location
191
     * @return array<mixed>    array containing the property or null if invalid URL
192
     */
193
    protected function buildLocation(string $strName, $latitude, $longitude, string $strMap) : ?array
194
    {
195
        $aLocation = null;
196
        $latitude = $this->validLongLat($latitude);
197
        $longitude = $this->validLongLat($longitude);
198
        $strMap = $this->validURL($strMap);
199
200
        if ((strlen($latitude) > 0 && strlen($longitude) > 0) || strlen($strMap) > 0) {
201
            $aLocation = array("@type" => "Place");
202
            $strName = $this->validString($strName);
203
            if (strlen($strName) > 0) {
204
                $aLocation["name"] = $strName;
205
            }
206
            if (strlen($latitude) > 0 && strlen($longitude) > 0) {
207
                $aLocation['geo'] = array(
208
                                "@type" => "GeoCoordinates",
209
                                "latitude" => $latitude,
210
                                "longitude" => $longitude
211
                );
212
            }
213
            if (strlen($strMap) > 0) {
214
                $aLocation["hasMap"] = $strMap;
215
            }
216
        }
217
        return $aLocation;
218
    }
219
220
    /**
221
     * Build a contact point object.
222
     * The type must not contain any predefiend value, it is to describe the contact.
223
     * (i.e. 'Information', 'Hotline', 'Customer Service', 'Administration'...)
224
     * @param string $strType
225
     * @param string $strEMail
226
     * @param string $strPhone
227
     * @return array<string>    array containing the property or null if invalid URL
228
     */
229
    protected function buildContactPoint(string $strType, string $strEMail, string $strPhone) : ?array
230
    {
231
        $aCP = null;
232
        $strType = $this->validString($strType);
233
        $strEMail = $this->validString($strEMail);
234
        $strPhone = $this->validString($strPhone);
235
        if (strlen($strType) > 0) {
236
            $aCP = array("@type" => "ContactPoint");
237
            $aCP["contactType"] = $strType;
238
            if (strlen($strEMail) > 0) {
239
                $aCP["email"] = $strEMail;
240
            }
241
            if (strlen($strPhone) > 0) {
242
                $aCP["telephone"] = $strPhone;
243
            }
244
        }
245
        return $aCP;
246
    }
247
248
    /**
249
     * Set the property to value of given type.
250
     * @param string $strName
251
     * @param mixed $value
252
     * @param int $iType
253
     */
254
    public function setProperty(string $strName, $value, int $iType = self::STRING) : void
255
    {
256
        $strValue = '';
257
        switch ($iType) {
258
            case self::DATE:
259
                $strValue = $this->validDate($value);
260
                break;
261
            case self::TIME:
262
                $strValue = $this->validTime($value);
263
                break;
264
            case self::EMAIL:
265
                $strValue = $this->validEMail($value);
266
                break;
267
            case self::URL:
268
                $strValue = $this->validURL($value);
269
                break;
270
            case self::LONG_LAT:
271
                $strValue = $this->validLongLat($value);
272
                break;
273
            case self::STRING:
274
            default:
275
                $strValue = $this->validString($value);
276
                break;
277
        }
278
        if (strlen($strValue) > 0) {
279
            $this->aJsonLD[$strName] = $strValue;
280
        }
281
    }
282
283
    /**
284
     * Get complete tag for the HTML head.
285
     * (including <script></script>)
286
     * @param bool $bPrettyPrint
287
     * @return string
288
     */
289
    public function getHTMLHeadTag(bool $bPrettyPrint = false) : string
290
    {
291
        $strTag = '';
292
        if (!$this->bIsChild) {
293
            $strTag  = '<script type="application/ld+json">' . PHP_EOL;
294
            $strTag .= json_encode($this->aJsonLD, $bPrettyPrint ? JSON_PRETTY_PRINT : 0) . PHP_EOL;
295
            $strTag .= '</script>' . PHP_EOL;
296
        }
297
        return $strTag;
298
    }
299
300
    /**
301
     * Get the resulting json object.
302
     * @param bool $bPrettyPrint
303
     * @return string
304
     */
305
    public function getJson(bool $bPrettyPrint = false) : string
306
    {
307
        $strJson = json_encode($this->aJsonLD, $bPrettyPrint ? JSON_PRETTY_PRINT : 0);
308
        if ($strJson === false) {
309
            $strJson = '';
310
        }
311
        return $strJson;
312
    }
313
314
    /**
315
     * Get the array object.
316
     * @return array<mixed>
317
     */
318
    public function getObject() : array
319
    {
320
        return $this->aJsonLD;
321
    }
322
323
    /**
324
     * Build valid string value.
325
     * @param string $str
326
     * @return string
327
     */
328
    protected function validString(string $str) : string
329
    {
330
        // replace " and all '\r' from string
331
        $str = str_replace('"', "'", $str);
332
        $str = str_replace("\r\n", "\n", $str);
333
        $str = str_replace("\r", "\n", $str);
334
        return $str;
335
    }
336
337
    /**
338
     * Build valid longitude/latidute value.
339
     * @param float|string $longlat
340
     * @return string
341
     */
342
    protected function validLongLat($longlat) : string
343
    {
344
        // TODO: long/lat validation
345
        if (is_numeric($longlat)) {
346
            $longlat = (string)$longlat;
347
        }
348
        return $longlat;
349
    }
350
351
    /**
352
     * Build valid date value.
353
     * If no time set (H,i and s == 0), only 'Y-m-d' format is used, otherwise
354
     * the full ISO8601 format is used.
355
     * @param string|int|\DateTime $date       can be string (format YYYY-MM-DD {HH:ii:ss.}), int (unixtimestamp) or DateTime - object
356
     * @return string
357
     */
358
    protected function validDate($date) : string
359
    {
360
        $strDate = '';
361
        if ($date !== null) {
362
            $uxts = 0;
363
            if (is_object($date) && get_class($date) == 'DateTime') {
364
                // DateTime -object
365
                $uxts = $date->getTimestamp();
366
            } elseif (is_numeric($date)) {
367
                // unix timestamp
368
                $uxts = intval($date);
369
            } elseif (is_string($date)) {
370
                $uxts = strtotime($date);
371
                if ($uxts === false) {
372
                    $uxts = 0;
373
                }
374
            }
375
            if ($uxts > 0) {
376
                $strTime = date('H:i:s', $uxts);
377
                if ($strTime == '00:00:00') {
378
                    $strDate = date('Y-m-d', $uxts);
379
                } else {
380
                    $strDate = date(DATE_ISO8601, $uxts);
381
                }
382
            }
383
        }
384
        return $strDate;
385
    }
386
387
    /**
388
     * Build valid time value.
389
     * @param string $strTime
390
     * @return string
391
     */
392
    protected function validTime(string $strTime) : string
393
    {
394
        $aTime = explode(':', $strTime);
395
        $strTime = '';
396
        if (count($aTime) == 2) {
397
            $iHour = intval($aTime[0]);
398
            $iMin = intval($aTime[1]);
399
            if ($iHour >= 0 && $iHour < 24 && $iMin >= 0 && $iMin < 60) {
400
                $strTime = sprintf('%02d:%02d', $iHour, $iMin);
401
            }
402
        }
403
        return $strTime;
404
    }
405
406
    /**
407
     * Check for valid URL value.
408
     * @param string $strURL
409
     * @return string
410
     */
411
    protected function validURL(string $strURL) : string
412
    {
413
        if (strlen($strURL) == 0) {
414
            return '';
415
        }
416
        $strValidURL = filter_var($strURL, FILTER_VALIDATE_URL);
417
        if ($strValidURL === false) {
418
            $strValidURL = '';
419
            trigger_error('Passed invalid URL: ' . $strURL, E_USER_WARNING);
420
        }
421
        return $strValidURL;
422
    }
423
424
    /**
425
     * Check for valid e-Mail adress.
426
     * @param string $strEMail
427
     * @return string
428
     */
429
    protected function validEMail(string $strEMail) : string
430
    {
431
        if (strlen($strEMail) == 0) {
432
            return '';
433
        }
434
        $strValidEMail = filter_var($strEMail, FILTER_VALIDATE_EMAIL);
435
        if ($strValidEMail === false) {
436
            $strValidEMail = '';
437
            trigger_error('Passed invalid e-Mail: ' . $strEMail, E_USER_WARNING);
438
        }
439
        return $strValidEMail;
440
    }
441
442
    /**
443
     * Truncate string to max length and add 'ellipsis' (...) at the end.
444
     * Truncation can be made 'hard' or 'soft' (default). Hard break menas, the text is
445
     * cut off at $iMaxLen-3 characters and '...' appended. In the case of
446
     * a soft break, the text is cut off after the last word that fits within
447
     * the maximum length and the '...' is added.
448
     * @param string    $strText
449
     * @param int       $iMaxLen
450
     * @param bool      $bHardBreak
451
     * @return string
452
     */
453
    protected function strTruncateEllipsis(string $strText, int $iMaxLen, bool $bHardBreak = false) : string
454
    {
455
        if (strlen($strText) > $iMaxLen - 3 && $iMaxLen > 4) {
456
            $strText = substr($strText, 0, $iMaxLen - 3);
457
            if (strrpos($strText, ' ') !== false && !$bHardBreak) {
458
                $strText = substr($strText, 0, strrpos($strText, ' '));
459
            }
460
            $strText .= '...';
461
        }
462
        return $strText;
463
    }
464
}
465