Completed
Push — master ( 2d6522...97a78a )
by Nekrasov
01:49
created

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