Completed
Push — master ( 9aa7a6...e435f6 )
by Nekrasov
01:52
created

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