Completed
Push — master ( 355253...ad79f2 )
by Nekrasov
01:49
created

BitrixModel::load()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
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
        $model = new static(null, $fields);
181
182
        if ($model->onBeforeSave() === false || $model->onBeforeCreate() === false) {
183
            return false;
184
        }
185
186
        $bxObject = static::instantiateObject();
187
        $id = $bxObject->add($fields);
188
        $model->setId($id);
189
190
        $result = $id ? true : false;
191
192
        $model->onAfterCreate($result);
193
        $model->onAfterSave($result);
194
195
        if (!$result) {
196
            throw new Exception($bxObject->LAST_ERROR);
197
        }
198
199
        return $model;
200
    }
201
202
    /**
203
     * Get count of items that match $filter.
204
     *
205
     * @param array $filter
206
     *
207
     * @return int
208
     */
209
    public static function count(array $filter = [])
210
    {
211
        return static::query()->filter($filter)->count();
212
    }
213
214
    /**
215
     * Get item by its id.
216
     *
217
     * @param int $id
218
     *
219
     * @return static|bool
220
     */
221
    public static function find($id)
222
    {
223
        return static::query()->getById($id);
224
    }
225
226
    /**
227
     * Delete model.
228
     *
229
     * @return bool
230
     */
231
    public function delete()
232
    {
233
        if ($this->onBeforeDelete() === false) {
234
            return false;
235
        }
236
237
        $result = static::$bxObject->delete($this->id);
238
239
        $this->onAfterDelete($result);
240
241
        return $result;
242
    }
243
244
    /**
245
     * Update model.
246
     *
247
     * @param array $fields
248
     *
249
     * @return bool
250
     */
251
    public function update(array $fields = [])
252
    {
253
        $keys = [];
254
        foreach ($fields as $key => $value) {
255
            array_set($this->fields, $key, $value);
256
            $keys[] = $key;
257
        }
258
259
        return $this->save($keys);
260
    }
261
262
    /**
263
     * Save model to database.
264
     *
265
     * @param array $selectedFields save only these fields instead of all.
266
     *
267
     * @return bool
268
     */
269
    public function save($selectedFields = [])
270
    {
271
        $selectedFields = is_array($selectedFields) ? $selectedFields : func_get_args();
272
273
        if ($this->onBeforeSave() === false || $this->onBeforeUpdate() === false) {
274
            return false;
275
        }
276
277
        $fields = $this->normalizeFieldsForSave($selectedFields);
278
        $result = !empty($fields) ? static::$bxObject->update($this->id, $fields) : false;
279
        if ($this instanceof ElementModel) {
280
            $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...
281
            $result = $result || $savePropsResult;
282
        }
283
284
        $this->onAfterUpdate($result);
285
        $this->onAfterSave($result);
286
287
        return $result;
288
    }
289
290
    /**
291
     * Scope to get only active items.
292
     *
293
     * @param BaseQuery $query
294
     *
295
     * @return BaseQuery
296
     */
297
    public function scopeActive($query)
298
    {
299
        $query->filter['ACTIVE'] = 'Y';
300
301
        return $query;
302
    }
303
304
    /**
305
     * Create an array of fields that will be saved to database.
306
     *
307
     * @param $selectedFields
308
     *
309
     * @return array
310
     */
311
    protected function normalizeFieldsForSave($selectedFields)
312
    {
313
        $fields = [];
314
        if ($this->fields === null) {
315
            return [];
316
        }
317
318
        foreach ($this->fields as $field => $value) {
319
            if (!$this->fieldShouldNotBeSaved($field, $value, $selectedFields)) {
320
                $fields[$field] = $value;
321
            }
322
        }
323
324
        return $fields;
325
    }
326
327
    /**
328
     * Determine whether the field should be stopped from passing to "update".
329
     *
330
     * @param string $field
331
     * @param mixed  $value
332
     * @param array  $selectedFields
333
     *
334
     * @return bool
335
     */
336
    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...
337
    {
338
        $blacklistedFields = [
339
            'ID',
340
            'IBLOCK_ID',
341
            'PROPERTIES',
342
            'GROUPS',
343
            'PROPERTY_VALUES',
344
        ];
345
346
        return (!empty($selectedFields) && !in_array($field, $selectedFields))
347
            || in_array($field, $blacklistedFields)
348
            || (substr($field, 0, 1) === '~')
349
            || (substr($field, 0, 9) === 'PROPERTY_');
350
    }
351
352
    /**
353
     * Instantiate bitrix entity object.
354
     *
355
     * @throws Exception
356
     *
357
     * @return object
358
     */
359
    public static function instantiateObject()
360
    {
361
        if (static::$bxObject) {
362
            return static::$bxObject;
363
        }
364
365
        if (class_exists(static::$objectClass)) {
366
            return static::$bxObject = new static::$objectClass();
367
        }
368
369
        throw new Exception('Object initialization failed');
370
    }
371
372
    /**
373
     * Destroy bitrix entity object.
374
     *
375
     * @return void
376
     */
377
    public static function destroyObject()
378
    {
379
        static::$bxObject = null;
380
    }
381
382
    /**
383
     * Instantiate a query object for the model.
384
     *
385
     * @throws Exception
386
     *
387
     * @return BaseQuery
388
     */
389
    public static function query()
390
    {
391
        throw new Exception('public static function query() is not implemented');
392
    }
393
394
    /**
395
     * Set current model id.
396
     *
397
     * @param $id
398
     */
399
    protected function setId($id)
400
    {
401
        $this->id = $id;
402
        $this->fields['ID'] = $id;
403
    }
404
405
    /**
406
     * Handle dynamic static method calls into a new query.
407
     *
408
     * @param  string  $method
409
     * @param  array  $parameters
410
     * @return mixed
411
     */
412
    public static function __callStatic($method, $parameters)
413
    {
414
        return static::query()->$method(...$parameters);
415
    }
416
}
417