Completed
Push — master ( 893d4f...379111 )
by Nekrasov
01:14
created

BitrixModel::refresh()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Arrilot\BitrixModels\Models;
4
5
use Arrilot\BitrixModels\ModelEventsTrait;
6
use Arrilot\BitrixModels\Queries\BaseQuery;
7
use Exception;
8
use Illuminate\Support\Collection;
9
10
abstract class BitrixModel extends ArrayableModel
11
{
12
    use ModelEventsTrait;
13
14
    /**
15
     * Bitrix entity object.
16
     *
17
     * @var object
18
     */
19
    public static $bxObject;
20
21
    /**
22
     * Corresponding object class name.
23
     *
24
     * @var string
25
     */
26
    protected static $objectClass = '';
27
28
    /**
29
     * Have fields been already fetched from DB?
30
     *
31
     * @var bool
32
     */
33
    protected $fieldsAreFetched = false;
34
35
    /**
36
     * Constructor.
37
     *
38
     * @param $id
39
     * @param $fields
40
     */
41
    public function __construct($id = null, $fields = null)
42
    {
43
        static::instantiateObject();
44
45
        $this->id = $id;
46
47
        $this->fill($fields);
48
    }
49
50
    /**
51
     * Get all model attributes from cache or database.
52
     *
53
     * @return array
54
     */
55
    public function get()
56
    {
57
        $this->load();
58
59
        return $this->fields;
60
    }
61
62
    /**
63
     * Load model fields from database if they are not loaded yet.
64
     *
65
     * @return $this
66
     */
67
    public function load()
68
    {
69
        if (!$this->fieldsAreFetched) {
70
            $this->refresh();
71
        }
72
        
73
        return $this;
74
    }
75
76
    /**
77
     * Get model fields from cache or database.
78
     *
79
     * @return array
80
     */
81
    public function getFields()
82
    {
83
        if ($this->fieldsAreFetched) {
84
            return $this->fields;
85
        }
86
87
        return $this->refreshFields();
88
    }
89
90
    /**
91
     * Refresh model from database and place data to $this->fields.
92
     *
93
     * @return array
94
     */
95
    public function refresh()
96
    {
97
        return $this->refreshFields();
98
    }
99
100
    /**
101
     * Refresh model fields and save them to a class field.
102
     *
103
     * @return array
104
     */
105
    public function refreshFields()
106
    {
107
        if ($this->id === null) {
108
            return $this->fields = [];
109
        }
110
        
111
        $this->fields = static::query()->getById($this->id)->fields;
112
        
113
        $this->fieldsAreFetched = true;
114
        
115
        return $this->fields;
116
    }
117
118
    /**
119
     * Fill model fields if they are already known.
120
     * Saves DB queries.
121
     *
122
     * @param array $fields
123
     *
124
     * @return void
125
     */
126
    public function fill($fields)
127
    {
128
        if (!is_array($fields)) {
129
            return;
130
        }
131
132
        if (isset($fields['ID'])) {
133
            $this->id = $fields['ID'];
134
        }
135
136
        $this->fields = $fields;
137
138
        $this->fieldsAreFetched = true;
139
140
        if (method_exists($this, 'afterFill')) {
141
            $this->afterFill();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Arrilot\BitrixModels\Models\BitrixModel as the method afterFill() does only exist in the following sub-classes of Arrilot\BitrixModels\Models\BitrixModel: Arrilot\BitrixModels\Models\ElementModel, Arrilot\BitrixModels\Models\UserModel. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
142
        }
143
    }
144
145
    /**
146
     * Activate model.
147
     *
148
     * @return bool
149
     */
150
    public function activate()
151
    {
152
        $this->fields['ACTIVE'] = 'Y';
153
154
        return $this->save(['ACTIVE']);
155
    }
156
157
    /**
158
     * Deactivate model.
159
     *
160
     * @return bool
161
     */
162
    public function deactivate()
163
    {
164
        $this->fields['ACTIVE'] = 'N';
165
166
        return $this->save(['ACTIVE']);
167
    }
168
169
    /**
170
     * Create new item in database.
171
     *
172
     * @param $fields
173
     *
174
     * @throws Exception
175
     *
176
     * @return static|bool
177
     */
178
    public static function create($fields)
179
    {
180
        return static::internalCreate($fields);
181
    }
182
183
    /**
184
     * Internal part of create to avoid problems with static and inheritance
185
     *
186
     * @param $fields
187
     *
188
     * @throws Exception
189
     *
190
     * @return static|bool
191
     */
192
    protected static function internalCreate($fields)
193
    {
194
        $model = new static(null, $fields);
195
        
196
        if ($model->onBeforeSave() === false || $model->onBeforeCreate() === false) {
197
            return false;
198
        }
199
        
200
        $bxObject = static::instantiateObject();
201
        $id = $bxObject->add($fields);
202
        $model->setId($id);
203
        
204
        $result = $id ? true : false;
205
        
206
        $model->onAfterCreate($result);
207
        $model->onAfterSave($result);
208
        
209
        if (!$result) {
210
            throw new Exception($bxObject->LAST_ERROR);
211
        }
212
        
213
        return $model;
214
    }
215
216
    /**
217
     * Get count of items that match $filter.
218
     *
219
     * @param array $filter
220
     *
221
     * @return int
222
     */
223
    public static function count(array $filter = [])
224
    {
225
        return static::query()->filter($filter)->count();
226
    }
227
228
    /**
229
     * Get item by its id.
230
     *
231
     * @param int $id
232
     *
233
     * @return static|bool
234
     */
235
    public static function find($id)
236
    {
237
        return static::query()->getById($id);
238
    }
239
240
    /**
241
     * Delete model.
242
     *
243
     * @return bool
244
     */
245
    public function delete()
246
    {
247
        if ($this->onBeforeDelete() === false) {
248
            return false;
249
        }
250
251
        $result = static::$bxObject->delete($this->id);
252
253
        $this->onAfterDelete($result);
254
255
        return $result;
256
    }
257
258
    /**
259
     * Update model.
260
     *
261
     * @param array $fields
262
     *
263
     * @return bool
264
     */
265
    public function update(array $fields = [])
266
    {
267
        $keys = [];
268
        foreach ($fields as $key => $value) {
269
            array_set($this->fields, $key, $value);
270
            $keys[] = $key;
271
        }
272
273
        return $this->save($keys);
274
    }
275
276
    /**
277
     * Save model to database.
278
     *
279
     * @param array $selectedFields save only these fields instead of all.
280
     *
281
     * @return bool
282
     */
283
    public function save($selectedFields = [])
284
    {
285
        $selectedFields = is_array($selectedFields) ? $selectedFields : func_get_args();
286
287
        if ($this->onBeforeSave() === false || $this->onBeforeUpdate() === false) {
288
            return false;
289
        }
290
291
        $fields = $this->normalizeFieldsForSave($selectedFields);
292
        $result = !empty($fields) ? static::$bxObject->update($this->id, $fields) : false;
293
        if ($this instanceof ElementModel) {
294
            $savePropsResult = $this->saveProps($selectedFields);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Arrilot\BitrixModels\Models\BitrixModel as the method saveProps() does only exist in the following sub-classes of Arrilot\BitrixModels\Models\BitrixModel: Arrilot\BitrixModels\Models\ElementModel. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
295
            $result = $result || $savePropsResult;
296
        }
297
298
        $this->onAfterUpdate($result);
299
        $this->onAfterSave($result);
300
301
        return $result;
302
    }
303
304
    /**
305
     * Scope to get only active items.
306
     *
307
     * @param BaseQuery $query
308
     *
309
     * @return BaseQuery
310
     */
311
    public function scopeActive($query)
312
    {
313
        $query->filter['ACTIVE'] = 'Y';
314
315
        return $query;
316
    }
317
318
    /**
319
     * Create an array of fields that will be saved to database.
320
     *
321
     * @param $selectedFields
322
     *
323
     * @return array
324
     */
325
    protected function normalizeFieldsForSave($selectedFields)
326
    {
327
        $fields = [];
328
        if ($this->fields === null) {
329
            return [];
330
        }
331
332
        foreach ($this->fields as $field => $value) {
333
            if (!$this->fieldShouldNotBeSaved($field, $value, $selectedFields)) {
334
                $fields[$field] = $value;
335
            }
336
        }
337
338
        return $fields;
339
    }
340
341
    /**
342
     * Determine whether the field should be stopped from passing to "update".
343
     *
344
     * @param string $field
345
     * @param mixed  $value
346
     * @param array  $selectedFields
347
     *
348
     * @return bool
349
     */
350
    protected function fieldShouldNotBeSaved($field, $value, $selectedFields)
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
351
    {
352
        $blacklistedFields = [
353
            'ID',
354
            'IBLOCK_ID',
355
            'PROPERTIES',
356
            'GROUPS',
357
            'PROPERTY_VALUES',
358
        ];
359
360
        return (!empty($selectedFields) && !in_array($field, $selectedFields))
361
            || in_array($field, $blacklistedFields)
362
            || (substr($field, 0, 1) === '~')
363
            || (substr($field, 0, 9) === 'PROPERTY_');
364
    }
365
366
    /**
367
     * Instantiate bitrix entity object.
368
     *
369
     * @throws Exception
370
     *
371
     * @return object
372
     */
373
    public static function instantiateObject()
374
    {
375
        if (static::$bxObject) {
376
            return static::$bxObject;
377
        }
378
379
        if (class_exists(static::$objectClass)) {
380
            return static::$bxObject = new static::$objectClass();
381
        }
382
383
        throw new Exception('Object initialization failed');
384
    }
385
386
    /**
387
     * Destroy bitrix entity object.
388
     *
389
     * @return void
390
     */
391
    public static function destroyObject()
392
    {
393
        static::$bxObject = null;
394
    }
395
396
    /**
397
     * Instantiate a query object for the model.
398
     *
399
     * @throws Exception
400
     *
401
     * @return BaseQuery
402
     */
403
    public static function query()
404
    {
405
        throw new Exception('public static function query() is not implemented');
406
    }
407
408
    /**
409
     * Set current model id.
410
     *
411
     * @param $id
412
     */
413
    protected function setId($id)
414
    {
415
        $this->id = $id;
416
        $this->fields['ID'] = $id;
417
    }
418
419
    /**
420
     * Handle dynamic static method calls into a new query.
421
     *
422
     * @param  string  $method
423
     * @param  array  $parameters
424
     * @return mixed
425
     */
426
    public static function __callStatic($method, $parameters)
427
    {
428
        return static::query()->$method(...$parameters);
429
    }
430
}
431