Passed
Push — master ( 5ce1b0...1f33cf )
by Stefan
07:05
created

JsonLD::validDate()   B

Complexity

Conditions 8
Paths 13

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 18
nc 13
nop 1
dl 0
loc 27
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
43
    /** @var int    internal object type     */
44
    protected $iType = -1;
45
    /** @var array<mixed>  the linked data as array     */
46
    protected $aJsonLD = null;
47
    /** @var bool   object is nested into another object     */
48
    protected $bIsChild = false;
49
50
    /**
51
     * Instanciation of JsonLD object.
52
     * @param int $iType        internal type
53
     * @param string $strType   type for JsonLD
54
     * @param bool $bIsChild
55
     */
56
    public function __construct(int $iType, string $strType, bool $bIsChild = false)
57
    {
58
        $this->iType = $iType;
59
        $this->bIsChild = $bIsChild;
60
        $this->aJsonLD = [
61
            "@context" => "https://schema.org",
62
            "@type" => $strType,
63
        ];
64
    }
65
66
    /**
67
     * Set description text.
68
     * Usable for all JsonLD types.
69
     * @param string $strDescription
70
     */
71
    public function setDescription(string $strDescription) : void
72
    {
73
        $this->setProperty("description", $strDescription);
74
    }
75
76
    /**
77
     * Set location for the object.
78
     * @param string $strName           Name for the location
79
     * @param string|float $latitude    latidude
80
     * @param string|float $longitude   longitude
81
     * @param string $strMap            URL to a map that shows the location
82
     */
83
    public function setLocation(string $strName, $latitude, $longitude, string $strMap = '') : void
84
    {
85
        $aLocation = $this->buildLocation($strName, $latitude, $longitude, $strMap);
86
        if ($aLocation != null) {
87
            if (isset($this->aJsonLD["location"]) && is_array($this->aJsonLD["location"])) {
88
                $this->aJsonLD["location"] = array_merge($this->aJsonLD["location"], $aLocation);
89
            } else {
90
                $this->aJsonLD["location"] = $aLocation;
91
            }
92
        }
93
    }
94
95
    /**
96
     * Add an image.
97
     * Multiple images are supported. Only existing images can be set.
98
     * Pixel size (width/height) is detected from image file.
99
     * @param string $strImageURL
100
     */
101
    public function addImage(string $strImageURL) : void
102
    {
103
        $aImg = $this->buildImageObject($strImageURL);
104
        if ($aImg != null) {
105
            if (isset($this->aJsonLD["image"])) {
106
                if (isset($this->aJsonLD["image"]["@type"])) {
107
                    // only one image set so far... change to array for multiple images
108
                    $aFirstImg = $this->aJsonLD["image"];
109
                    $this->aJsonLD["image"] = array();
110
                    $this->aJsonLD["image"][] = $aFirstImg;
111
                }
112
                $this->aJsonLD["image"][] = $aImg;
113
            } else {
114
                // first image - set property direct
115
                $this->aJsonLD["image"] = $aImg;
116
            }
117
        }
118
    }
119
120
    /**
121
     * Build ImageObject property.
122
     * Logos and images are defiend as ImageObject.
123
     * @param string $strURL    URL to a valid image (PNG, GIF, JPG)
124
     * @return array<string>    array containing the property or null if invalid URL
125
     */
126
    protected function buildImageObject(string $strURL) : ?array
127
    {
128
        $aLogo = null;
129
        if (file_exists($strURL)) {
130
            $aSize = getimagesize($strURL);
131
            if ($aSize) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aSize of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
132
                $aLogo = array(
133
                                "@type" => "ImageObject",
134
                                "url" =>  $strURL,
135
                                "width" => $aSize[0],
136
                                "height" => $aSize[1]
137
                );
138
            }
139
        }
140
        return $aLogo;
141
    }
142
143
    /**
144
     * Build a postal adress object.
145
     * @param string $strStreet
146
     * @param string $strPostcode
147
     * @param string $strCity
148
     * @param string $strRegion
149
     * @param string $strCountry
150
     * @return array<string>    array containing the property
151
     */
152
    protected function buildAdress(string $strStreet, string $strPostcode, string $strCity, string $strRegion = '', string $strCountry = '') : array
153
    {
154
        $aAdress = array("@type" => "PostalAddress");
155
        if (strlen($strStreet) > 0) {
156
            $aAdress["streetAddress"] = $this->validString($strStreet);
157
        }
158
        if (strlen($strPostcode) > 0) {
159
            $aAdress["postalCode"] = $this->validString($strPostcode);
160
        }
161
        if (strlen($strCity) > 0) {
162
            $aAdress["addressLocality"] = $this->validString($strCity);
163
        }
164
        if (strlen($strCountry) > 0) {
165
            $aAdress["addressCountry"] = $this->validString($strCountry);
166
        }
167
        if (strlen($strCountry) > 0) {
168
            $aAdress["addressRegion"] = $this->validString($strRegion);
169
        }
170
        return $aAdress;
171
    }
172
173
    /**
174
     * Build a location object.
175
     * @param string $strName       Name for the location
176
     * @param string|float $latitude
177
     * @param string|float $longitude
178
     * @param string $strMap        URL to map show the location
179
     * @return array<mixed>    array containing the property or null if invalid URL
180
     */
181
    protected function buildLocation(string $strName, $latitude, $longitude, string $strMap) : ?array
182
    {
183
        $aLocation = null;
184
        $latitude = $this->validLongLat($latitude);
185
        $longitude = $this->validLongLat($longitude);
186
        $strMap = $this->validURL($strMap);
187
188
        if ((strlen($latitude) > 0 && strlen($longitude) > 0) || strlen($strMap) > 0) {
189
            $aLocation = array("@type" => "Place");
190
            $strName = $this->validString($strName);
191
            if (strlen($strName) > 0) {
192
                $aLocation["name"] = $strName;
193
            }
194
            if (strlen($latitude) > 0 && strlen($longitude) > 0) {
195
                $aLocation['geo'] = array(
196
                                "@type" => "GeoCoordinates",
197
                                "latitude" => $latitude,
198
                                "longitude" => $longitude
199
                );
200
            }
201
            if (strlen($strMap) > 0) {
202
                $aLocation["hasMap"] = $strMap;
203
            }
204
        }
205
        return $aLocation;
206
    }
207
208
    /**
209
     * Build a contact point object.
210
     * The type must not contain any predefiend value, it is to describe the contact.
211
     * (i.e. 'Information', 'Hotline', 'Customer Service', 'Administration'...)
212
     * @param string $strType
213
     * @param string $strEMail
214
     * @param string $strPhone
215
     * @return array<string>    array containing the property or null if invalid URL
216
     */
217
    protected function buildContactPoint(string $strType, string $strEMail, string $strPhone) : ?array
218
    {
219
        $aCP = null;
220
        $strType = $this->validString($strType);
221
        $strEMail = $this->validString($strEMail);
222
        $strPhone = $this->validString($strPhone);
223
        if (strlen($strType) > 0) {
224
            $aCP = array("@type" => "ContactPoint");
225
            $aCP["contactType"] = $strType;
226
            if (strlen($strEMail) > 0 ) {
227
                $aCP["email"] = $strEMail;
228
            }
229
            if (strlen($strPhone) > 0 ) {
230
                $aCP["telephone"] = $strPhone;
231
            }
232
        }
233
        return $aCP;
234
    }
235
236
    /**
237
     * Set the property to value of given type.
238
     * @param string $strName
239
     * @param string $strValue
240
     * @param int $iType
241
     */
242
    public function setProperty(string $strName, string $strValue, int $iType=self::STRING) : void
243
    {
244
        switch ($iType) {
245
            case self::DATE:
246
                $strValue = $this->validDate($strValue);
247
                break;
248
            case self::TIME:
249
                $strValue = $this->validTime($strValue);
250
                break;
251
            case self::EMAIL:
252
                $strValue = $this->validEMail($strValue);
253
                break;
254
            case self::URL:
255
                $strValue = $this->validURL($strValue);
256
                break;
257
            case self::STRING:
258
            default:
259
                $strValue = $this->validString($strValue);
260
                break;
261
        }
262
        if (strlen($strValue) > 0) {
263
            $this->aJsonLD[$strName] = $strValue;
264
        }
265
    }
266
267
    /**
268
     * Get complete tag for the HTML head.
269
     * (including <script></script>)
270
     * @param bool $bPrettyPrint
271
     * @return string
272
     */
273
    public function getHTMLHeadTag(bool $bPrettyPrint = false) : string
274
    {
275
        $strTag = '';
276
        if (!$this->bIsChild) {
277
            $strTag  = '<script type="application/ld+json">' . PHP_EOL;
278
            $strTag .= json_encode($this->aJsonLD, $bPrettyPrint ? JSON_PRETTY_PRINT : 0) . PHP_EOL;
279
            $strTag .= '</script>' . PHP_EOL;
280
        }
281
        return $strTag;
282
    }
283
284
    /**
285
     * Get the resulting json object.
286
     * @param bool $bPrettyPrint
287
     * @return string
288
     */
289
    public function getJson(bool $bPrettyPrint = false) : string
290
    {
291
        $strJson = json_encode($this->aJsonLD, $bPrettyPrint ? JSON_PRETTY_PRINT : 0);
292
        if ($strJson === false) {
293
            $strJson = '';
294
        }
295
        return $strJson;
296
    }
297
298
    /**
299
     * Get the array object.
300
     * @return array<mixed>
301
     */
302
    public function getObject() : array
303
    {
304
        return $this->aJsonLD;
305
    }
306
307
    /**
308
     * Build valid string value.
309
     * @param string $str
310
     * @return string
311
     */
312
    protected function validString(string $str) : string
313
    {
314
        // replace " and all '\r' from string
315
        $str = str_replace('"', "'", $str);
316
        $str = str_replace("\r\n", "\n", $str);
317
        $str = str_replace("\r", "\n", $str);
318
        return $str;
319
    }
320
321
    /**
322
     * Build valid longitude/latidute value.
323
     * @param float|string $longlat
324
     * @return string
325
     */
326
    protected function validLongLat($longlat) : string
327
    {
328
        // TODO: long/lat validation
329
        if (is_numeric($longlat)) {
330
            $longlat = (string)$longlat;
331
        }
332
        return $longlat;
333
    }
334
335
    /**
336
     * Build valid date value.
337
     * If no time set (H,i and s == 0), only 'Y-m-d' format is used, otherwise
338
     * the full ISO8601 format is used.
339
     * @param string|int|\DateTime $date       can be string (format YYYY-MM-DD {HH:ii:ss.}), int (unixtimestamp) or DateTime - object
340
     * @return string
341
     */
342
    protected function validDate($date) : string
343
    {
344
        $strDate = '';
345
        if ($date != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $date of type DateTime|integer|string against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
346
            $uxts = 0;
347
            if (is_object($date) && get_class($date) == 'DateTime') {
348
                // DateTime -object
349
                $uxts = $date->getTimestamp();
350
            } else if (is_numeric($date)) {
351
                // unix timestamp
352
                $uxts = intval($date);
353
            } else {
354
                $uxts = strtotime($date);
0 ignored issues
show
Bug introduced by
It seems like $date can also be of type DateTime; however, parameter $datetime of strtotime() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

354
                $uxts = strtotime(/** @scrutinizer ignore-type */ $date);
Loading history...
355
                if ($uxts === false) {
356
                    $uxts = 0;
357
                }
358
            }
359
            if ($uxts > 0) {
360
                $strTime = date('H:i:s', $uxts);
361
                if ($strTime == '00:00:00') {
362
                    $strDate = date('Y-m-d', $uxts);
363
                } else {
364
                    $strDate = date(DATE_ISO8601, $uxts);
365
                }
366
            }
367
        }
368
        return $strDate;
369
    }
370
371
    /**
372
     * Build valid time value.
373
     * @param string $strTime
374
     * @return string
375
     */
376
    protected function validTime(string $strTime) : string
377
    {
378
        $aTime = explode(':', $strTime);
379
        $strTime = '';
380
        if (count($aTime) == 2) {
381
            $iHour = intval($aTime[0]);
382
            $iMin = intval($aTime[1]);
383
            if ($iHour >= 0 && $iHour < 24 && $iMin >= 0 && $iMin <60) {
384
                $strTime = sprintf('%02d:%02d', $iHour, $iMin);
385
            }
386
        }
387
        return $strTime;
388
    }
389
390
    /**
391
     * Check for valid URL value.
392
     * @param string $strURL
393
     * @return string
394
     */
395
    protected function validURL(string $strURL) : string
396
    {
397
        if (!($strURL = filter_var($strURL, FILTER_VALIDATE_URL))) {
398
            $strURL = '';
399
        }
400
        return $strURL;
401
    }
402
403
    /**
404
     * Check for valid e-Mail adress.
405
     * @param string $strEMail
406
     * @return string
407
     */
408
    protected function validEMail(string $strEMail) : string
409
    {
410
        if (!($strEMail = filter_var($strEMail, FILTER_VALIDATE_EMAIL))) {
411
            $strEMail = '';
412
        }
413
        return $strEMail;
414
    }
415
416
    /**
417
     * Truncate string to max length and add 'ellipsis' (...) at the end.
418
     * Truncation can be made 'hard' or 'soft' (default). Hard break menas, the text is
419
     * cut off at $iMaxLen-3 characters and '...' appended. In the case of
420
     * a soft break, the text is cut off after the last word that fits within
421
     * the maximum length and the '...' is added.
422
     * @param string    $strText
423
     * @param int       $iMaxLen
424
     * @param bool      $bHardBreak
425
     * @return string
426
     */
427
    protected function strTruncateEllipsis(string $strText, int $iMaxLen, bool $bHardBreak = false) : string
428
    {
429
        if (strlen($strText) > $iMaxLen - 3 && $iMaxLen > 4) {
430
            $strText = substr($strText, 0, $iMaxLen - 3);
431
            if (strrpos($strText, ' ') !== false && !$bHardBreak) {
432
                $strText = substr($strText, 0, strrpos($strText, ' '));
433
            }
434
            $strText .= '...';
435
        }
436
        return $strText;
437
    }
438
}
439