AbstractCrudObject   C
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 65
lcom 1
cbo 4
dl 0
loc 454
rs 5.7894
c 0
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 29 6
A setId() 0 4 1
A setParentId() 0 3 1
A setApi() 0 4 1
A getEndpoint() 0 3 1
A assureApi() 0 9 3
A getParentId() 0 3 1
A assureParentId() 0 6 2
A assureId() 0 6 2
A getApi() 0 3 1
A getChangedValues() 0 3 1
A getChangedFields() 0 3 1
A exportData() 0 7 2
A clearHistory() 0 3 1
A __set() 0 7 3
A setDefaultReadFields() 0 3 1
A getDefaultReadFields() 0 3 1
A getNodePath() 0 3 1
D create() 0 25 9
A read() 0 13 3
A update() 0 8 1
A deleteSelf() 0 6 1
A save() 0 7 2
A assureEndpoint() 0 11 3
A fetchConnection() 0 15 3
A getOneByConnection() 0 16 2
A getManyByConnection() 0 11 1
A createAsyncJob() 0 13 2
A deleteIds() 0 21 4
B readIds() 0 23 4

How to fix   Complexity   

Complex Class

Complex classes like AbstractCrudObject 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 AbstractCrudObject, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
4
 *
5
 * You are hereby granted a non-exclusive, worldwide, royalty-free license to
6
 * use, copy, modify, and distribute this software in source code or binary
7
 * form for use in connection with the web services and APIs provided by
8
 * Facebook.
9
 *
10
 * As with any software that integrates with the Facebook platform, your use
11
 * of this software is subject to the Facebook Developer Principles and
12
 * Policies [http://developers.facebook.com/policy/]. This copyright notice
13
 * shall be included in all copies or substantial portions of the software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
 * DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
namespace FacebookAds\Object;
25
use FacebookAds\Api;
26
use FacebookAds\Cursor;
27
use FacebookAds\Http\RequestInterface;
28
use FacebookAds\Http\ResponseInterface;
29
class AbstractCrudObject extends AbstractObject {
30
  /**
31
   * @var string
32
   */
33
  const FIELD_ID = 'id';
34
  /**
35
   * @var string[] set of fields to read by default
36
   */
37
  protected static $defaultReadFields = array();
38
  /**
39
   * @var array set of fields that have been mutated
40
   */
41
  protected $changedFields = array();
42
  /**
43
   * @var Api instance of the Api used by this object
44
   */
45
  protected $api;
46
  /**
47
   * @var string ID of the adaccount this object belongs to
48
   */
49
  protected $parentId;
50
51
  /**
52
   * @deprecated deprecate constructor with null and parent_id
53
   * @param string $id Optional (do not set for new objects)
54
   * @param string $parent_id Optional, needed for creating new objects.
55
   * @param Api $api The Api instance this object should use to make calls
56
   */
57
  public function __construct($id = null, $parent_id = null, Api $api = null) {
58
    parent::__construct();
59
60
    // check that $id is an integer or a string integer
61
    $int_id = $id;
62
    if (strpos($id, 'act_') === 0) {
63
      $int_id = substr($id, 4);
64
    }
65
    if (!is_null($int_id) && !ctype_digit((string) $int_id)) {
66
      $extra_message = '';
67
      if (is_numeric($int_id)) {
68
        $extra_message = ' Please use an integer string'
69
        .' to prevent integer overflow.';
70
      }
71
      throw new \InvalidArgumentException(
72
        'Object ID must be an integer or integer string but was passed "'
73
        .(string)$id.'" ('.gettype($id).').'.(string)$extra_message);
74
    }
75
    $this->data[static::FIELD_ID] = $id;
76
77
    if (!is_null($parent_id)) {
78
      $warning_message = "\$parent_id as a parameter of constructor is being " .
79
        "deprecated, please try not to use this in new code.\n";
80
      error_log($warning_message);
81
    }
82
    $this->parentId = $parent_id;
83
84
    $this->api = static::assureApi($api);
85
  }
86
87
  /**
88
   * @param string $id
89
   */
90
  public function setId($id) {
91
    $this->data[static::FIELD_ID] = $id;
92
    return $this;
93
  }
94
  /**
95
   * @param string $parent_id
96
   */
97
  public function setParentId($parent_id) {
98
    $this->parentId = $parent_id;
99
  }
100
  /**
101
   * @param Api $api The Api instance this object should use to make calls
102
   */
103
  public function setApi(Api $api) {
104
    $this->api = static::assureApi($api);
105
    return $this;
106
  }
107
  /**
108
   * @deprecated getEndpoint function is deprecated
109
   * @return string
110
   */
111
  protected function getEndpoint() {
112
    return null;
113
  }
114
  /**
115
   * @param Api|null $instance
116
   * @return Api
117
   * @throws \InvalidArgumentException
118
   */
119
  protected static function assureApi(Api $instance = null) {
120
    $instance = $instance ?: Api::instance();
121
    if (!$instance) {
122
      throw new \InvalidArgumentException(
123
        'An Api instance must be provided as argument or '.
124
        'set as instance in the \FacebookAds\Api');
125
    }
126
    return $instance;
127
  }
128
  /**
129
   * @return string|null
130
   */
131
  public function getParentId() {
132
    return $this->parentId;
133
  }
134
  /**
135
   * @return string
136
   * @throws \Exception
137
   */
138
  protected function assureParentId() {
139
    if (!$this->parentId) {
140
      throw new \Exception("A parent ID is required.");
141
    }
142
    return $this->parentId;
143
  }
144
  /**
145
   * @return string
146
   * @throws \Exception
147
   */
148
  protected function assureId() {
149
    if (!$this->data[static::FIELD_ID]) {
150
      throw new \Exception("field '".static::FIELD_ID."' is required.");
151
    }
152
    return (string) $this->data[static::FIELD_ID];
153
  }
154
  /**
155
   * @return Api
156
   */
157
  public function getApi() {
158
    return $this->api;
159
  }
160
  /**
161
   * Get the values which have changed
162
   *
163
   * @return array Key value pairs of changed variables
164
   */
165
  public function getChangedValues() {
166
    return $this->changedFields;
167
  }
168
  /**
169
   * Get the name of the fields that have changed
170
   *
171
   * @return array Array of changed field names
172
   */
173
  public function getChangedFields() {
174
    return array_keys($this->changedFields);
175
  }
176
  /**
177
   * Get the values which have changed, converting them to scalars
178
   */
179
  public function exportData() {
180
    $data = array();
181
    foreach ($this->changedFields as $key => $val) {
182
      $data[$key] = parent::exportValue($val);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (exportValue() instead of exportData()). Are you sure this is correct? If so, you might want to change this to $this->exportValue().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
183
    }
184
    return $data;
185
  }
186
  /**
187
   * @return void
188
   */
189
  protected function clearHistory() {
190
    $this->changedFields = array();
191
  }
192
  /**
193
   * @param string $name
194
   * @param mixed $value
195
   */
196
  public function __set($name, $value) {
197
    if (!array_key_exists($name, $this->data)
198
      || $this->data[$name] !== $value) {
199
      $this->changedFields[$name] = $value;
200
    }
201
    parent::__set($name, $value);
202
  }
203
  /**
204
   * @param string[] $fields
205
   */
206
  public static function setDefaultReadFields(array $fields = array()) {
207
    static::$defaultReadFields = $fields;
208
  }
209
  /**
210
   * @return string[]
211
   */
212
  public static function getDefaultReadFields() {
213
    return static::$defaultReadFields;
214
  }
215
  /**
216
   * @return string
217
   */
218
  protected function getNodePath() {
219
    return '/'.$this->assureId();
220
  }
221
  /**
222
   * Create function for the object.
223
   *
224
   * @param array $params Additional parameters to include in the request
225
   * @return $this
226
   * @throws \Exception
227
   */
228
  public function create(array $params = array()) {
229
    if ($this->data[static::FIELD_ID]) {
230
      throw new \Exception("Object has already an ID");
231
    }
232
    $response = $this->getApi()->call(
233
      '/'.$this->assureParentId().'/'.$this->getEndpoint(),
0 ignored issues
show
Deprecated Code introduced by
The method FacebookAds\Object\Abstr...udObject::getEndpoint() has been deprecated with message: getEndpoint function is deprecated

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
234
      RequestInterface::METHOD_POST,
235
      array_merge($this->exportData(), $params));
236
    $this->clearHistory();
237
    $data = $response->getContent();
238
    if (!isset($params['execution_options'])){
239
      $id = is_string($data) ? $data : $data[static::FIELD_ID];
240
    /** @var AbstractCrudObject $this */
241
      if ($this instanceof CanRedownloadInterface
242
        && isset($params[CanRedownloadInterface::PARAM_REDOWNLOAD])
243
        && $params[CanRedownloadInterface::PARAM_REDOWNLOAD] === true
244
        && isset($data['data'][$id])
245
        && is_array($data['data'][$id])
246
      ) {
247
        $this->setDataWithoutValidation($data['data'][$id]);
248
      }
249
      $this->data[static::FIELD_ID] = (string) $id;
250
    }
251
    return $this;
252
  }
253
  /**
254
   * Read object data from the graph
255
   *
256
   * @param string[] $fields Fields to request
257
   * @param array $params Additional request parameters
258
   * @return $this
259
   */
260
  public function read(array $fields = array(), array $params = array()) {
261
    $fields = implode(',', $fields ?: static::getDefaultReadFields());
262
    if ($fields) {
263
      $params['fields'] = $fields;
264
    }
265
    $response = $this->getApi()->call(
266
      $this->getNodePath(),
267
      RequestInterface::METHOD_GET,
268
      $params);
269
    $this->setDataWithoutValidation($response->getContent());
0 ignored issues
show
Bug introduced by
It seems like $response->getContent() targeting FacebookAds\Http\ResponseInterface::getContent() can also be of type null; however, FacebookAds\Object\Abstr...DataWithoutValidation() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
270
    $this->clearHistory();
271
    return $this;
272
  }
273
  /**
274
   * Update the object. Function parameters are similar with the create function
275
   *
276
   * @param array $params Update parameters in assoc
277
   * @return $this
278
   */
279
  public function update(array $params = array()) {
280
    $this->getApi()->call(
281
      $this->getNodePath(),
282
      RequestInterface::METHOD_POST,
283
      array_merge($this->exportData(), $params));
284
    $this->clearHistory();
285
    return $this;
286
  }
287
  /**
288
   * Delete this object from the graph
289
   *
290
   * @param array $params
291
   * @return void
292
   */
293
  public function deleteSelf(array $params = array()) {
294
    $this->getApi()->call(
295
      $this->getNodePath(),
296
      RequestInterface::METHOD_DELETE,
297
      $params);
298
  }
299
  /**
300
   * Perform object upsert
301
   *
302
   * Helper function which determines whether an object should be created or
303
   * updated
304
   *
305
   * @param array $params
306
   * @return $this
307
   */
308
  public function save(array $params = array()) {
309
    if ($this->data[static::FIELD_ID]) {
310
      return $this->update($params);
311
    } else {
312
      return $this->create($params);
313
    }
314
  }
315
  /**
316
   * @param string $prototype_class
317
   * @param string $endpoint
318
   * @return string
319
   * @throws \InvalidArgumentException
320
   */
321
  protected function assureEndpoint($prototype_class, $endpoint) {
322
    if (!$endpoint) {
323
      $prototype = new $prototype_class(null, null, $this->getApi());
324
      if (!$prototype instanceof AbstractCrudObject) {
325
        throw new \InvalidArgumentException('Either prototype must be instance
326
          of AbstractCrudObject or $endpoint must be given');
327
      }
328
      $endpoint = $prototype->getEndpoint();
0 ignored issues
show
Deprecated Code introduced by
The method FacebookAds\Object\Abstr...udObject::getEndpoint() has been deprecated with message: getEndpoint function is deprecated

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
329
    }
330
    return $endpoint;
331
  }
332
  /**
333
   * @param array $fields
334
   * @param array $params
335
   * @param string $prototype_class
336
   * @param string|null $endpoint
337
   * @return ResponseInterface
338
   */
339
  protected function fetchConnection(
340
    array $fields = array(),
341
    array $params = array(),
342
    $prototype_class,
343
    $endpoint = null) {
344
    $fields = implode(',', $fields ?: static::getDefaultReadFields());
345
    if ($fields) {
346
      $params['fields'] = $fields;
347
    }
348
    $endpoint = $this->assureEndpoint($prototype_class, $endpoint);
349
    return $this->getApi()->call(
350
      '/'.$this->assureId().'/'.$endpoint,
351
      RequestInterface::METHOD_GET,
352
      $params);
353
  }
354
  /**
355
   * Read a single connection object
356
   *
357
   * @param string $prototype_class
358
   * @param array $fields Fields to request
359
   * @param array $params Additional filters for the reading
360
   * @param string|null $endpoint
361
   * @return AbstractObject
362
   */
363
  protected function getOneByConnection(
364
    $prototype_class,
365
    array $fields = array(),
366
    array $params = array(),
367
    $endpoint = null) {
368
    $response = $this->fetchConnection(
369
      $fields, $params, $prototype_class, $endpoint);
370
    if (!$response->getContent()) {
371
      return null;
372
    }
373
    $object = new $prototype_class(
374
      null, $this->{static::FIELD_ID}, $this->getApi());
375
    /** @var AbstractCrudObject $object */
376
    $object->setDataWithoutValidation($response->getContent());
377
    return $object;
378
  }
379
  /**
380
   * Read objects from a connection
381
   *
382
   * @param string $prototype_class
383
   * @param array $fields Fields to request
384
   * @param array $params Additional filters for the reading
385
   * @param string|null $endpoint
386
   * @return Cursor
387
   */
388
  protected function getManyByConnection(
389
    $prototype_class,
390
    array $fields = array(),
391
    array $params = array(),
392
    $endpoint = null) {
393
    $response = $this->fetchConnection(
394
      $fields, $params, $prototype_class, $endpoint);
395
    return new Cursor(
396
      $response,
397
      new $prototype_class(null, $this->{static::FIELD_ID}, $this->getApi()));
398
  }
399
  /**
400
   * @param string $job_class
401
   * @param array $fields
402
   * @param array $params
403
   * @return AbstractAsyncJobObject
404
   * @throws \InvalidArgumentException
405
   */
406
  protected function createAsyncJob(
407
    $job_class,
408
    array $fields = array(),
409
    array $params = array()) {
410
    $object = new $job_class(null, $this->assureId(), $this->getApi());
411
    if (!$object instanceof AbstractAsyncJobObject) {
0 ignored issues
show
Bug introduced by
The class FacebookAds\Object\AbstractAsyncJobObject does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
412
      throw new \InvalidArgumentException(
413
        "Class {$job_class} is not of type "
414
        .AbstractAsyncJobObject::className());
415
    }
416
    $params['fields'] = $fields;
417
    return $object->create($params);
418
  }
419
  /**
420
   * Delete objects.
421
   *
422
   * Used batch API calls to delete multiple objects at once
423
   *
424
   * @param string[] $ids Array or single Object ID to delete
425
   * @param Api $api Api Object to use
426
   * @return bool Returns true on success
427
   */
428
  public static function deleteIds(array $ids, Api $api = null) {
429
    $batch = array();
430
    foreach ($ids as $id) {
431
      $request = array(
432
        'relative_url' => '/'.$id,
433
        'method' => RequestInterface::METHOD_DELETE,
434
      );
435
      $batch[] = $request;
436
    }
437
    $api = static::assureApi($api);
438
    $response = $api->call(
439
      '/',
440
      RequestInterface::METHOD_POST,
441
      array('batch' => json_encode($batch)));
442
    foreach ($response->getContent() as $result) {
0 ignored issues
show
Bug introduced by
The expression $response->getContent() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
443
      if (200 != $result['code']) {
444
        return false;
445
      }
446
    }
447
    return true;
448
  }
449
  /**
450
   * Read function for the object. Convert fields and filters into the query
451
   * part of uri and return objects.
452
   *
453
   * @param mixed $ids Array or single object IDs
454
   * @param array $fields Array of field names to read
455
   * @param array $params Additional filters for the reading, in assoc
456
   * @param Api $api Api Object to use
457
   * @return Cursor
458
   */
459
  public static function readIds(
460
    array $ids,
461
    array $fields = array(),
462
    array $params = array(),
463
    Api $api = null) {
464
    if (empty($fields)) {
465
      $fields = static::getDefaultReadFields();
466
    }
467
    if (!empty($fields)) {
468
      $params['fields'] = implode(',', $fields);
469
    }
470
    $params['ids'] = implode(',', $ids);
471
    $api = static::assureApi($api);
472
    $response = $api->call('/', RequestInterface::METHOD_GET, $params);
473
    $result = array();
474
    foreach ($response->getContent() as $data) {
0 ignored issues
show
Bug introduced by
The expression $response->getContent() of type array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
475
      /** @var AbstractObject $object */
476
      $object = new static(null, null, $api);
477
      $object->setDataWithoutValidation((array) $data);
478
      $result[] = $object;
479
    }
480
    return $result;
481
  }
482
}
483