GContacts   F
last analyzed

Complexity

Total Complexity 66

Size/Duplication

Total Lines 511
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 231
c 0
b 0
f 0
dl 0
loc 511
rs 3.12
wmc 66

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A addPersonFields() 0 11 5
B loadImageFromURL() 0 35 7
A setContactPhoto() 0 20 4
A getUpdatePersonFields() 0 15 3
A deleteContact() 0 7 1
A createContact() 0 25 4
B search() 0 34 8
A loadImageFromFile() 0 19 6
A updateContact() 0 23 4
A deleteContactPhoto() 0 8 1
A setContactStarred() 0 9 2
A getContact() 0 20 3
B list() 0 39 11
A setPageSize() 0 3 1
A setContactPhotoFile() 0 24 5

How to fix   Complexity   

Complex Class

Complex classes like GContacts often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GContacts, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace SKien\Google;
5
6
/**
7
 * Class to manage the contacts of a google account.
8
 *
9
 * This class encapsulates the following Google People API resources:
10
 *  - people
11
 *  - people.connections
12
 *
13
 * If one of the methods that calls the google API fails (if it returns `false`),
14
 * the last responsecode and furter informations can be retrieved through
15
 * following methods of the `GClient` instance this object was created with:
16
 *  - `$oClient->getLastResponseCode()`
17
 *  - `$oClient->getLastError()`
18
 *  - `$oClient->getLastStatus()`
19
 *
20
 * @see \SKien\Google\GClient::getLastResponseCode()
21
 * @see \SKien\Google\GClient::getLastError()
22
 * @see \SKien\Google\GClient::getLastStatus()
23
 *
24
 * @link https://developers.google.com/people/api/rest/v1/people
25
 * @link https://developers.google.com/people/api/rest/v1/people.connections
26
 *
27
 * @author Stefanius <[email protected]>
28
 * @copyright MIT License - see the LICENSE file for details
29
 */
30
class GContacts
31
{
32
    /** full access to the users contacts */
33
    public const CONTACTS = "https://www.googleapis.com/auth/contacts";
34
    /** readonly access to the users contacts */
35
    public const CONTACTS_READONLY = "https://www.googleapis.com/auth/contacts.readonly";
36
    /** readonly access to the users other contacts */
37
    public const CONTACTS_OTHER_READONLY = "https://www.googleapis.com/auth/contacts.other.readonly";
38
39
    /**	Sort people by when they were changed; older entries first. */
40
    public const SO_LAST_MODIFIED_ASCENDING = 'LAST_MODIFIED_ASCENDING';
41
    /**	Sort people by when they were changed; newer entries first. */
42
    public const SO_LAST_MODIFIED_DESCENDING = 'LAST_MODIFIED_DESCENDING';
43
    /**	Sort people by first name. */
44
    public const SO_FIRST_NAME_ASCENDING = 'FIRST_NAME_ASCENDING';
45
    /**	Sort people by last name. */
46
    public const SO_LAST_NAME_ASCENDING = 'LAST_NAME_ASCENDING';
47
48
    /** the default personFields for detail view    */
49
    public const DEF_DETAIL_PERSON_FIELDS = [
50
        GContact::PF_NAMES,
51
        GContact::PF_ORGANIZATIONS,
52
        GContact::PF_NICKNAMES,
53
        GContact::PF_BIRTHDAYS,
54
        GContact::PF_PHOTOS,
55
        GContact::PF_ADDRESSES,
56
        GContact::PF_EMAIL_ADDRESSES,
57
        GContact::PF_PHONE_NUMBERS,
58
        GContact::PF_GENDERS,
59
        GContact::PF_MEMBERSHIPS,
60
        GContact::PF_METADATA,
61
        GContact::PF_BIOGRAPHIES,
62
        GContact::PF_URLS,
63
    ];
64
65
    /** the default personFields for detail view    */
66
    public const DEF_LIST_PERSON_FIELDS = [
67
        GContact::PF_NAMES,
68
        GContact::PF_ORGANIZATIONS,
69
        GContact::PF_NICKNAMES,
70
        GContact::PF_BIRTHDAYS,
71
        GContact::PF_ADDRESSES,
72
        GContact::PF_EMAIL_ADDRESSES,
73
        GContact::PF_PHONE_NUMBERS,
74
        GContact::PF_MEMBERSHIPS,
75
        GContact::PF_METADATA,
76
    ];
77
78
    /** max. pagesize for the list request */
79
    protected const CONTACTS_MAX_PAGESIZE = 1000;
80
    /** max. pagesize for a search */
81
    protected const SEARCH_MAX_PAGESIZE = 30;
82
83
    /** @var array<string>  personFields/readMask to be returned by the request */
84
    protected array $aPersonFields = [];
85
    /** pagesize for a request */
86
    protected int $iPageSize = 200;
87
88
    /** @var GClient    clients to perform the requests to the api     */
89
    protected GClient $oClient;
90
91
    /**
92
     * Create instance an pass the clinet for the requests.
93
     * @param GClient $oClient
94
     */
95
    public function __construct(GClient $oClient)
96
    {
97
        $this->oClient = $oClient;
98
    }
99
100
    /**
101
     * Add personFields/readMask for next request.
102
     * Can be called multiple and/or by passing an array of personFields. All
103
     * const `GContact::PF_xxxx` can be specified.
104
     * If no personFields specified before an API call, as default setting
105
     * - `self::DEF_DETAIL_PERSON_FIELDS` for the getContact() and
106
     * - `self::DEF_LIST_PERSON_FIELDS` for the list() and search() call is used
107
     * @param string|array<string>|null $fields; if set to null, the internal array is cleared
108
     * @param bool $bReset  rest current set personFields
109
     */
110
    public function addPersonFields($fields, bool $bReset = false) : void
111
    {
112
        if ($bReset) {
113
            $this->aPersonFields = [];
114
        }
115
        if ($fields === null) {
116
            $this->aPersonFields = [];
117
        } else if (is_array($fields)) {
118
            $this->aPersonFields = array_merge($this->aPersonFields, $fields);
119
        } else if (!in_array($fields, $this->aPersonFields)) {
120
            $this->aPersonFields[] = $fields;
121
        }
122
    }
123
124
    /**
125
     * Set the pagesize for reading lists.
126
     * May be limited to the max. pgaesize for the request.
127
     * - contact list: max. pagesize is 1000
128
     * - search: max. pagesize is 30
129
     * @param int $iPageSize
130
     */
131
    public function setPageSize(int $iPageSize) : void
132
    {
133
        $this->iPageSize = $iPageSize;
134
    }
135
136
    /**
137
     * Get the whole contact list.
138
     * @link https://developers.google.com/people/api/rest/v1/people.connections/list
139
     * @param string $strSortOrder  one of the self::SO_xxx constants
140
     * @param string $strGroupResourceName
141
     * @return array<mixed>|false
142
     */
143
    public function list(string $strSortOrder = self::SO_LAST_NAME_ASCENDING, string $strGroupResourceName = '')
144
    {
145
        $aHeader = [$this->oClient->getAuthHeader()];
146
147
        if (count($this->aPersonFields) == 0) {
148
            $this->aPersonFields = self::DEF_LIST_PERSON_FIELDS;
149
        }
150
151
        $aParams = [
152
            'personFields' => implode(',', $this->aPersonFields),
153
            'sortOrder' => $strSortOrder,
154
            'pageSize' => ($this->iPageSize > self::CONTACTS_MAX_PAGESIZE ? self::CONTACTS_MAX_PAGESIZE : $this->iPageSize),
155
        ];
156
157
        $bEndOfList = false;
158
        $aContactList = [];
159
        while ($bEndOfList === false) {
160
            $strURI = 'https://people.googleapis.com/v1/people/me/connections?' . http_build_query($aParams);
161
            $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader);
162
            if ($strResponse === false) {
163
                return false;
164
            }
165
            $oResponse = json_decode($strResponse, true);
166
            if (is_array($oResponse)) {
167
                if (isset($oResponse['nextPageToken']) && !empty($oResponse['nextPageToken'])) {
168
                    $aParams['pageToken'] = $oResponse['nextPageToken'];
169
                } else {
170
                    $bEndOfList = true;
171
                }
172
                foreach ($oResponse['connections'] as $aContact) {
173
                    if (empty($strGroupResourceName) || GContact::fromArray($aContact)->belongsToGroup($strGroupResourceName)) {
174
                        $aContactList[] = $aContact;
175
                    }
176
                }
177
            } else {
178
                break;
179
            }
180
        }
181
        return $aContactList;
182
    }
183
184
    /**
185
     * Search within the contacts.
186
     * The query matches on a contact's `names`, `nickNames`, `emailAddresses`,
187
     * `phoneNumbers`, and `organizations` fields. The search for phone numbers only
188
     * works, if leading '+' and contained '(', ')' or spaces are omitted in the query!
189
     * The query is used to match <b>prefix</B> phrases of the fields on a person.
190
     * For example, a person with name "foo name" matches queries such as "f", "fo",
191
     * "foo", "foo n", "nam", etc., but not "oo n".
192
     * > <b>Note:</b>
193
     * > The count of contacts, the search request returns is limitetd to the
194
     * > pageSize (which is limited itself to max. 30 at all). If there are
195
     * > more contacts in the list that matches the query, unfortunately NO
196
     * > further information about that additional contacts - and how many - are
197
     * > available!
198
     * @link https://developers.google.com/people/api/rest/v1/people/searchContacts
199
     * @param string $strQuery  the query to search for
200
     * @return array<mixed>|false
201
     */
202
    public function search(string $strQuery)
203
    {
204
        $aHeader = [$this->oClient->getAuthHeader()];
205
206
        if (count($this->aPersonFields) == 0) {
207
            $this->aPersonFields = self::DEF_LIST_PERSON_FIELDS;
208
        }
209
210
        $aParams = [
211
            'query' => '',
212
            'readMask' => implode(',', $this->aPersonFields),
213
            'pageSize' => ($this->iPageSize > self::SEARCH_MAX_PAGESIZE ? self::SEARCH_MAX_PAGESIZE : $this->iPageSize),
214
        ];
215
        $strURI = 'https://people.googleapis.com/v1/people:searchContacts?' . http_build_query($aParams);
216
217
        // 'warmup' request
218
        // Note from google documentation:
219
        // Before searching, clients should send a warmup request with an empty query to update the cache.
220
        // https://developers.google.com/people/v1/contacts#search_the_users_contacts
221
        $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader);
222
        $aParams['query'] = $strQuery;
223
        $strURI = 'https://people.googleapis.com/v1/people:searchContacts?' . http_build_query($aParams);
224
225
        $aContactList = false;
226
        if (($strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader)) !== false) {
227
            $aContactList = [];
228
            $oResponse = json_decode($strResponse, true);
229
            if (is_array($oResponse) && isset($oResponse['results']) && count($oResponse['results']) > 0) {
230
                foreach ($oResponse['results'] as $aContact) {
231
                    $aContactList[] = $aContact['person'];
232
                }
233
            }
234
        }
235
        return $aContactList;
236
    }
237
238
    /**
239
     * Get the contact specified by its resourceName.
240
     * Note that only the person fields specified with `addPersonFields()` are included
241
     * in the result.
242
     * @link https://developers.google.com/people/api/rest/v1/people/get
243
     * @see GContacts::addPersonFields()
244
     * @param string $strResourceName
245
     * @return GContact|false
246
     */
247
    public function getContact(string $strResourceName)
248
    {
249
        $aHeader = [$this->oClient->getAuthHeader()];
250
251
        if (count($this->aPersonFields) == 0) {
252
            $this->aPersonFields = self::DEF_DETAIL_PERSON_FIELDS;
253
        }
254
255
        $aParams = [
256
            'personFields' => implode(',', $this->aPersonFields),
257
        ];
258
259
        $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . '?' . http_build_query($aParams);
260
        $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::GET, $aHeader);
261
262
        $result = false;
263
        if ($strResponse !== false) {
264
            $result = GContact::fromJSON($strResponse, $this->aPersonFields);
265
        }
266
        return $result;
267
    }
268
269
    /**
270
     * Creates a new contact.
271
     * @link https://developers.google.com/people/api/rest/v1/people/createContact
272
     * @param GContact $oContact
273
     * @return GContact|false
274
     */
275
    public function createContact(GContact $oContact)
276
    {
277
        $aHeader = [
278
            $this->oClient->getAuthHeader(),
279
            'Content-Type: application/json',
280
        ];
281
282
        if (count($this->aPersonFields) == 0) {
283
            $this->aPersonFields = self::DEF_DETAIL_PERSON_FIELDS;
284
        }
285
        $aParams = ['personFields' => implode(',', $this->getUpdatePersonFields())];
286
287
        $result = false;
288
        $data = json_encode($oContact->getArrayCopy());
289
        if ($data !== false) {
290
            $strURI = 'https://people.googleapis.com/v1/people:createContact/?' . http_build_query($aParams);
291
            $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::POST, $aHeader, $data);
292
293
            if ($strResponse !== false) {
294
                $result = GContact::fromJSON($strResponse, $this->aPersonFields);
295
            }
296
        } else {
297
            $this->oClient->setError(json_last_error(), json_last_error_msg(), 'ERROR_JSON_ENCODE');
298
        }
299
        return $result;
300
    }
301
302
    /**
303
     * Updates an existing contact specified by resourceName.
304
     * To prevent the user from data loss, this request fails with an 400 response
305
     * code, if the contact has changed on the server, since it was loaded.
306
     * Reload the data from the server and make the changes again!
307
     * @link https://developers.google.com/people/api/rest/v1/people/updateContact
308
     * @param string $strResourceName
309
     * @param GContact $oContact
310
     * @return GContact|false
311
     */
312
    public function updateContact(string $strResourceName, GContact $oContact)
313
    {
314
        $aHeader = [
315
            $this->oClient->getAuthHeader(),
316
            'Content-Type: application/json',
317
        ];
318
319
        if (count($this->aPersonFields) == 0) {
320
            $this->aPersonFields = self::DEF_DETAIL_PERSON_FIELDS;
321
        }
322
        $aParams = ['updatePersonFields' => implode(',', $this->getUpdatePersonFields())];
323
324
        $result = false;
325
        $data = json_encode($oContact);
326
        if ($data !== false) {
327
            $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':updateContact/?' . http_build_query($aParams);
328
            $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::PATCH, $aHeader, $data);
329
330
            if ($strResponse !== false) {
331
                $result = GContact::fromJSON($strResponse, $this->aPersonFields);
332
            }
333
        }
334
        return $result;
335
    }
336
337
    /**
338
     * Delete the requested contact.
339
     * @link https://developers.google.com/people/api/rest/v1/people/deleteContact
340
     * @param string $strResourceName
341
     * @return bool
342
     */
343
    public function deleteContact(string $strResourceName) : bool
344
    {
345
        $aHeader = [$this->oClient->getAuthHeader()];
346
347
        $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':deleteContact';
348
        $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::DELETE, $aHeader);
349
        return ($strResponse !== false);
350
    }
351
352
    /**
353
     * Set or unset the 'starred' mark for the specified contact.
354
     * The 'starred' mark just means, the contact belongs to the system group `contactGroups/starred`.
355
     * @see GContactGroups::addContactsToGroup()
356
     * @see GContactGroups::removeContactsFromGroup()
357
     * @param string $strResourceName
358
     * @param bool $bSetStarred
359
     * @return bool
360
     */
361
    public function setContactStarred(string $strResourceName, bool $bSetStarred = true) : bool
362
    {
363
        $oContactGroups = new GContactGroups($this->oClient);
364
        if ($bSetStarred) {
365
            $result = $oContactGroups->addContactsToGroup(GContactGroups::GRP_STARRED, [$strResourceName]);
366
        } else {
367
            $result = $oContactGroups->removeContactsFromGroup(GContactGroups::GRP_STARRED, [$strResourceName]);
368
        }
369
        return $result !== false;
370
    }
371
372
373
    /**
374
     * Set contact photo from image file.
375
     * Supported types are JPG, PNG, GIF and BMP.
376
     * @link https://developers.google.com/people/api/rest/v1/people/updateContactPhoto
377
     * @param string $strResourceName
378
     * @param string $strFilename
379
     * @return bool
380
     */
381
    public function setContactPhotoFile(string $strResourceName, string $strFilename) : bool
382
    {
383
        $blobPhoto = '';
384
        if (filter_var($strFilename, FILTER_VALIDATE_URL)) {
385
            $blobPhoto = $this->loadImageFromURL($strFilename);
386
        } elseif (file_exists($strFilename)) {
387
            $blobPhoto = $this->loadImageFromFile($strFilename);
388
        } else {
389
            $this->oClient->setError(0, 'File not found: ' . $strFilename, 'INVALID_ARGUMENT');
390
        }
391
        $result = false;
392
        if (!empty($blobPhoto)) {
393
            $aHeader = [
394
                $this->oClient->getAuthHeader(),
395
                'Content-Type: application/json',
396
            ];
397
            $data = json_encode(['photoBytes' => $blobPhoto]);
398
            if ($data !== false) {
399
                $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':updateContactPhoto';
400
                $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::PATCH, $aHeader, $data);
401
                $result = ($strResponse !== false);
402
            }
403
        }
404
        return $result;
405
    }
406
407
    /**
408
     * Set contact photo from base64 encoded image data.
409
     * @link https://developers.google.com/people/api/rest/v1/people/updateContactPhoto
410
     * @param string $strResourceName
411
     * @param string $blobPhoto  base64 encoded image
412
     * @return bool
413
     */
414
    public function setContactPhoto(string $strResourceName, string $blobPhoto) : bool
415
    {
416
        $result = false;
417
        // check for valid base64 encoded imagedata
418
        $img = base64_decode($blobPhoto);
419
        if ($img !== false && imagecreatefromstring(base64_decode($blobPhoto)) !== false) {
420
            $aHeader = [
421
                $this->oClient->getAuthHeader(),
422
                'Content-Type: application/json',
423
            ];
424
            $data = json_encode(['photoBytes' => $blobPhoto]);
425
            if ($data !== false) {
426
                $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':updateContactPhoto';
427
                $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::PATCH, $aHeader, $data);
428
                $result = ($strResponse !== false);
429
            }
430
        } else {
431
            $this->oClient->setError(0, 'Invalid base64 encoded image data', 'INVALID_ARGUMENT');
432
        }
433
        return $result;
434
    }
435
436
    /**
437
     * Delete contact photo for given contact.
438
     * @link https://developers.google.com/people/api/rest/v1/people/deleteContactPhoto
439
     * @param string $strResourceName
440
     * @return bool
441
     */
442
    public function deleteContactPhoto(string $strResourceName) : bool
443
    {
444
        $aHeader = [$this->oClient->getAuthHeader()];
445
446
        $strURI = 'https://people.googleapis.com/v1/' . $strResourceName . ':deleteContactPhoto';
447
        $strResponse = $this->oClient->fetchJsonResponse($strURI, GClient::DELETE, $aHeader);
448
449
        return ($strResponse !== false);
450
    }
451
452
    /**
453
     * Get updateable personFields.
454
     * @return array<string>
455
     */
456
    private function getUpdatePersonFields() : array
457
    {
458
        $aReadonlyPersonFields = [
459
            GContact::PF_PHOTOS,
460
            GContact::PF_COVER_PHOTOS,
461
            GContact::PF_AGE_RANGES,
462
            GContact::PF_METADATA,
463
        ];
464
        $aUpdatePersonFields = $this->aPersonFields;
465
        foreach ($aReadonlyPersonFields as $strReadonly) {
466
            if (($key = array_search($strReadonly, $aUpdatePersonFields)) !== false) {
467
                unset($aUpdatePersonFields[$key]);
468
            }
469
        }
470
        return $aUpdatePersonFields;
471
    }
472
473
    /**
474
     * Load an image from an URL.
475
     * The method uses curl to be independet of [allow_url_fopen] enabled on the system.
476
     * @param string $strURL
477
     * @return string   base64 encoded imagedata
478
     */
479
    private function loadImageFromURL(string $strURL) : string
480
    {
481
        $blobPhoto = '';
482
483
        $curl = curl_init();
484
        curl_setopt($curl, CURLOPT_URL, $strURL);
485
        curl_setopt($curl, CURLOPT_HEADER, true);
486
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
487
488
        $strResponse = curl_exec($curl);
489
490
        $iResponseCode = intval(curl_getinfo($curl, CURLINFO_RESPONSE_CODE));
491
        $iHeaderSize = intval(curl_getinfo($curl, CURLINFO_HEADER_SIZE));
492
493
        curl_close($curl);
494
495
        if ($iResponseCode == 200 && is_string($strResponse)) {
496
            $aHeader = $this->oClient->parseHttpHeader(substr($strResponse, 0, $iHeaderSize));
497
            $strContentType = $aHeader['content-type'] ?? '';
498
            switch ($strContentType) {
499
                case 'image/jpeg':
500
                case 'image/png':
501
                case 'image/gif':
502
                case 'image/bmp':
503
                    $img = substr($strResponse, $iHeaderSize);
504
                    $blobPhoto = base64_encode($img);
505
                    break;
506
                default:
507
                    $this->oClient->setError(0, 'Unsupported file type: ' . $strContentType, 'INVALID_ARGUMENT');
508
                    break;
509
            }
510
        } else {
511
            $this->oClient->setError(0, 'File not found: ' . $strURL, 'INVALID_ARGUMENT');
512
        }
513
        return $blobPhoto;
514
    }
515
516
    /**
517
     * Load an image from an file.
518
     * In most cases an uploaded imagefile.
519
     * @param string $strFilename
520
     * @return string   base64 encoded imagedata
521
     */
522
    private function loadImageFromFile(string $strFilename) : string
523
    {
524
        $blobPhoto = '';
525
        $iImageType = exif_imagetype($strFilename);
526
        switch ($iImageType) {
527
            case IMAGETYPE_JPEG:
528
            case IMAGETYPE_PNG:
529
            case IMAGETYPE_GIF:
530
            case IMAGETYPE_BMP:
531
                $img = file_get_contents($strFilename);
532
                if ($img !== false) {
533
                    $blobPhoto = base64_encode($img);
534
                }
535
                break;
536
            default:
537
                $this->oClient->setError(0, 'Unsupported image type: ' . image_type_to_mime_type($iImageType), 'INVALID_ARGUMENT');
538
                break;
539
        }
540
        return $blobPhoto;
541
    }
542
}
543