Completed
Push — master ( a4a1ca...4311a9 )
by Nekrasov
01:52
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 Arrilot\BitrixModels\Relations\BaseRelation;
8
use Exception;
9
use InvalidArgumentException;
10
use LogicException;
11
12
abstract class BitrixModel extends ArrayableModel
13
{
14
    use ModelEventsTrait;
15
16
    /**
17
     * Bitrix entity object.
18
     *
19
     * @var object
20
     */
21
    public static $bxObject;
22
23
    /**
24
     * Corresponding object class name.
25
     *
26
     * @var string
27
     */
28
    protected static $objectClass = '';
29
30
    /**
31
     * Have fields been already fetched from DB?
32
     *
33
     * @var bool
34
     */
35
    protected $fieldsAreFetched = false;
36
37
    /**
38
     * Constructor.
39
     *
40
     * @param $id
41
     * @param $fields
42
     */
43
    public function __construct($id = null, $fields = null)
44
    {
45
        static::instantiateObject();
46
47
        $this->id = $id;
48
49
        $this->fill($fields);
50
    }
51
52
    /**
53
     * Get all model attributes from cache or database.
54
     *
55
     * @return array
56
     */
57
    public function get()
58
    {
59
        $this->load();
60
61
        return $this->fields;
62
    }
63
64
    /**
65
     * Load model fields from database if they are not loaded yet.
66
     *
67
     * @return $this
68
     */
69
    public function load()
70
    {
71
        if (!$this->fieldsAreFetched) {
72
            $this->refresh();
73
        }
74
        
75
        return $this;
76
    }
77
78
    /**
79
     * Get model fields from cache or database.
80
     *
81
     * @return array
82
     */
83
    public function getFields()
84
    {
85
        if ($this->fieldsAreFetched) {
86
            return $this->fields;
87
        }
88
89
        return $this->refreshFields();
90
    }
91
92
    /**
93
     * Refresh model from database and place data to $this->fields.
94
     *
95
     * @return array
96
     */
97
    public function refresh()
98
    {
99
        return $this->refreshFields();
100
    }
101
102
    /**
103
     * Refresh model fields and save them to a class field.
104
     *
105
     * @return array
106
     */
107
    public function refreshFields()
108
    {
109
        if ($this->id === null) {
110
            return $this->fields = [];
111
        }
112
        
113
        $this->fields = static::query()->getById($this->id)->fields;
114
        
115
        $this->fieldsAreFetched = true;
116
        
117
        return $this->fields;
118
    }
119
120
    /**
121
     * Fill model fields if they are already known.
122
     * Saves DB queries.
123
     *
124
     * @param array $fields
125
     *
126
     * @return void
127
     */
128
    public function fill($fields)
129
    {
130
        if (!is_array($fields)) {
131
            return;
132
        }
133
134
        if (isset($fields['ID'])) {
135
            $this->id = $fields['ID'];
136
        }
137
138
        $this->fields = $fields;
139
140
        $this->fieldsAreFetched = true;
141
142
        if (method_exists($this, 'afterFill')) {
143
            $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...
144
        }
145
    }
146
147
    /**
148
     * Activate model.
149
     *
150
     * @return bool
151
     */
152
    public function activate()
153
    {
154
        $this->fields['ACTIVE'] = 'Y';
155
156
        return $this->save(['ACTIVE']);
157
    }
158
159
    /**
160
     * Deactivate model.
161
     *
162
     * @return bool
163
     */
164
    public function deactivate()
165
    {
166
        $this->fields['ACTIVE'] = 'N';
167
168
        return $this->save(['ACTIVE']);
169
    }
170
171
    /**
172
     * Create new item in database.
173
     *
174
     * @param $fields
175
     *
176
     * @throws Exception
177
     *
178
     * @return static|bool
179
     */
180
    public static function create($fields)
181
    {
182
        $model = new static(null, $fields);
183
184
        if ($model->onBeforeSave() === false || $model->onBeforeCreate() === false) {
185
            return false;
186
        }
187
188
        $bxObject = static::instantiateObject();
189
        $id = $bxObject->add($fields);
190
        $model->setId($id);
191
192
        $result = $id ? true : false;
193
194
        $model->onAfterCreate($result);
195
        $model->onAfterSave($result);
196
197
        if (!$result) {
198
            throw new Exception($bxObject->LAST_ERROR);
199
        }
200
201
        return $model;
202
    }
203
204
    /**
205
     * Get count of items that match $filter.
206
     *
207
     * @param array $filter
208
     *
209
     * @return int
210
     */
211
    public static function count(array $filter = [])
212
    {
213
        return static::query()->filter($filter)->count();
214
    }
215
216
    /**
217
     * Get item by its id.
218
     *
219
     * @param int $id
220
     *
221
     * @return static|bool
222
     */
223
    public static function find($id)
224
    {
225
        return static::query()->getById($id);
226
    }
227
228
    /**
229
     * Delete model.
230
     *
231
     * @return bool
232
     */
233
    public function delete()
234
    {
235
        if ($this->onBeforeDelete() === false) {
236
            return false;
237
        }
238
239
        $result = static::$bxObject->delete($this->id);
240
241
        $this->onAfterDelete($result);
242
243
        return $result;
244
    }
245
246
    /**
247
     * Update model.
248
     *
249
     * @param array $fields
250
     *
251
     * @return bool
252
     */
253
    public function update(array $fields = [])
254
    {
255
        $keys = [];
256
        foreach ($fields as $key => $value) {
257
            array_set($this->fields, $key, $value);
258
            $keys[] = $key;
259
        }
260
261
        return $this->save($keys);
262
    }
263
264
    /**
265
     * Save model to database.
266
     *
267
     * @param array $selectedFields save only these fields instead of all.
268
     *
269
     * @return bool
270
     */
271
    public function save($selectedFields = [])
272
    {
273
        $selectedFields = is_array($selectedFields) ? $selectedFields : func_get_args();
274
275
        if ($this->onBeforeSave() === false || $this->onBeforeUpdate() === false) {
276
            return false;
277
        }
278
279
        $fields = $this->normalizeFieldsForSave($selectedFields);
280
        $result = !empty($fields) ? static::$bxObject->update($this->id, $fields) : false;
281
        if ($this instanceof ElementModel) {
282
            $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...
283
            $result = $result || $savePropsResult;
284
        }
285
286
        $this->onAfterUpdate($result);
287
        $this->onAfterSave($result);
288
289
        return $result;
290
    }
291
292
    /**
293
     * Scope to get only active items.
294
     *
295
     * @param BaseQuery $query
296
     *
297
     * @return BaseQuery
298
     */
299
    public function scopeActive($query)
300
    {
301
        $query->filter['ACTIVE'] = 'Y';
302
303
        return $query;
304
    }
305
306
    /**
307
     * Create an array of fields that will be saved to database.
308
     *
309
     * @param $selectedFields
310
     *
311
     * @return array
312
     */
313
    protected function normalizeFieldsForSave($selectedFields)
314
    {
315
        $fields = [];
316
        if ($this->fields === null) {
317
            return [];
318
        }
319
320
        foreach ($this->fields as $field => $value) {
321
            if (!$this->fieldShouldNotBeSaved($field, $value, $selectedFields)) {
322
                $fields[$field] = $value;
323
            }
324
        }
325
326
        return $fields;
327
    }
328
329
    /**
330
     * Determine whether the field should be stopped from passing to "update".
331
     *
332
     * @param string $field
333
     * @param mixed  $value
334
     * @param array  $selectedFields
335
     *
336
     * @return bool
337
     */
338
    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...
339
    {
340
        $blacklistedFields = [
341
            'ID',
342
            'IBLOCK_ID',
343
            'PROPERTIES',
344
            'GROUPS',
345
            'PROPERTY_VALUES',
346
        ];
347
348
        return (!empty($selectedFields) && !in_array($field, $selectedFields))
349
            || in_array($field, $blacklistedFields)
350
            || (substr($field, 0, 1) === '~')
351
            || (substr($field, 0, 9) === 'PROPERTY_');
352
    }
353
354
    /**
355
     * Instantiate bitrix entity object.
356
     *
357
     * @throws Exception
358
     *
359
     * @return object
360
     */
361
    public static function instantiateObject()
362
    {
363
        if (static::$bxObject) {
364
            return static::$bxObject;
365
        }
366
367
        if (class_exists(static::$objectClass)) {
368
            return static::$bxObject = new static::$objectClass();
369
        }
370
371
        throw new Exception('Object initialization failed');
372
    }
373
374
    /**
375
     * Destroy bitrix entity object.
376
     *
377
     * @return void
378
     */
379
    public static function destroyObject()
380
    {
381
        static::$bxObject = null;
382
    }
383
384
    /**
385
     * Instantiate a query object for the model.
386
     *
387
     * @throws Exception
388
     *
389
     * @return BaseQuery
390
     */
391
    public static function query()
392
    {
393
        throw new Exception('public static function query() is not implemented');
394
    }
395
396
    /**
397
     * Set current model id.
398
     *
399
     * @param $id
400
     */
401
    protected function setId($id)
402
    {
403
        $this->id = $id;
404
        $this->fields['ID'] = $id;
405
    }
406
}
407