Completed
Pull Request — master (#17)
by
unknown
01:35
created

BaseBitrixModel::internalCreate()   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 Illuminate\Support\Collection;
8
use LogicException;
9
10
abstract class BaseBitrixModel extends ArrayableModel
11
{
12
    use ModelEventsTrait;
13
14
    /**
15
     * Array of model fields keys that needs to be saved with next save().
16
     *
17
     * @var array
18
     */
19
    protected $fieldsSelectedForSave = [];
20
21
    /**
22
     * Array of errors that are passed to model events.
23
     *
24
     * @var array
25
     */
26
    protected $eventErrors = [];
27
28
    /**
29
     * Have fields been already fetched from DB?
30
     *
31
     * @var bool
32
     */
33
    protected $fieldsAreFetched = false;
34
35
    /**
36
     * @var array - Array related models indexed by the relation names
37
     */
38
    public $related = [];
39
40
    /**
41
     * Internal part of create to avoid problems with static and inheritance
42
     *
43
     * @param $fields
44
     *
45
     * @throws LogicException
46
     *
47
     * @return static|bool
48
     */
49
    protected static function internalCreate($fields)
50
    {
51
        throw new LogicException('internalCreate is not implemented');
52
    }
53
54
    /**
55
     * Save model to database.
56
     *
57
     * @param array $selectedFields save only these fields instead of all.
58
     *
59
     * @return bool
60
     */
61
    abstract public function save($selectedFields = []);
62
63
    /**
64
     * Determine whether the field should be stopped from passing to "update".
65
     *
66
     * @param string $field
67
     * @param mixed  $value
68
     * @param array  $selectedFields
69
     *
70
     * @return bool
71
     */
72
    abstract protected function fieldShouldNotBeSaved($field, $value, $selectedFields);
73
    
74
    /**
75
     * Get all model attributes from cache or database.
76
     *
77
     * @return array
78
     */
79
    public function get()
80
    {
81
        $this->load();
82
        
83
        return $this->fields;
84
    }
85
86
    /**
87
     * Load model fields from database if they are not loaded yet.
88
     *
89
     * @return $this
90
     */
91
    public function load()
92
    {
93
        if (!$this->fieldsAreFetched) {
94
            $this->refresh();
95
        }
96
        
97
        return $this;
98
    }
99
100
    /**
101
     * Get model fields from cache or database.
102
     *
103
     * @return array
104
     */
105
    public function getFields()
106
    {
107
        if ($this->fieldsAreFetched) {
108
            return $this->fields;
109
        }
110
        
111
        return $this->refreshFields();
112
    }
113
114
    /**
115
     * Refresh model from database and place data to $this->fields.
116
     *
117
     * @return array
118
     */
119
    public function refresh()
120
    {
121
        return $this->refreshFields();
122
    }
123
124
    /**
125
     * Refresh model fields and save them to a class field.
126
     *
127
     * @return array
128
     */
129
    public function refreshFields()
130
    {
131
        if ($this->id === null) {
132
            return $this->fields = [];
133
        }
134
        
135
        $this->fields = static::query()->getById($this->id)->fields;
136
        
137
        $this->fieldsAreFetched = true;
138
        
139
        return $this->fields;
140
    }
141
142
    /**
143
     * Fill model fields if they are already known.
144
     * Saves DB queries.
145
     *
146
     * @param array $fields
147
     *
148
     * @return void
149
     */
150
    public function fill($fields)
151
    {
152
        if (!is_array($fields)) {
153
            return;
154
        }
155
        
156
        if (isset($fields['ID'])) {
157
            $this->id = $fields['ID'];
158
        }
159
        
160
        $this->fields = $fields;
161
        
162
        $this->fieldsAreFetched = true;
163
        
164
        if (method_exists($this, 'afterFill')) {
165
            $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\BaseBitrixModel as the method afterFill() does only exist in the following sub-classes of Arrilot\BitrixModels\Models\BaseBitrixModel: 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...
166
        }
167
    }
168
169
    /**
170
     * Set current model id.
171
     *
172
     * @param $id
173
     */
174
    protected function setId($id)
175
    {
176
        $this->id = $id;
177
        $this->fields['ID'] = $id;
178
    }
179
180
    /**
181
     * Create new item in database.
182
     *
183
     * @param $fields
184
     *
185
     * @throws LogicException
186
     *
187
     * @return static|bool
188
     */
189
    public static function create($fields)
190
    {
191
        return static::internalCreate($fields);
192
    }
193
194
    /**
195
     * Get count of items that match $filter.
196
     *
197
     * @param array $filter
198
     *
199
     * @return int
200
     */
201
    public static function count(array $filter = [])
202
    {
203
        return static::query()->filter($filter)->count();
204
    }
205
206
    /**
207
     * Get item by its id.
208
     *
209
     * @param int $id
210
     *
211
     * @return static|bool
212
     */
213
    public static function find($id)
214
    {
215
        return static::query()->getById($id);
216
    }
217
218
    /**
219
     * Update model.
220
     *
221
     * @param array $fields
222
     *
223
     * @return bool
224
     */
225
    public function update(array $fields = [])
226
    {
227
        $keys = [];
228
        foreach ($fields as $key => $value) {
229
            array_set($this->fields, $key, $value);
230
            $keys[] = $key;
231
        }
232
233
        return $this->save($keys);
234
    }
235
236
    /**
237
     * Create an array of fields that will be saved to database.
238
     *
239
     * @param $selectedFields
240
     *
241
     * @return array
242
     */
243
    protected function normalizeFieldsForSave($selectedFields)
244
    {
245
        $fields = [];
246
        if ($this->fields === null) {
247
            return [];
248
        }
249
250
        foreach ($this->fields as $field => $value) {
251
            if (!$this->fieldShouldNotBeSaved($field, $value, $selectedFields)) {
252
                $fields[$field] = $value;
253
            }
254
        }
255
256
        return $fields;
257
    }
258
259
    /**
260
     * Instantiate a query object for the model.
261
     *
262
     * @throws LogicException
263
     *
264
     * @return BaseQuery
265
     */
266
    public static function query()
267
    {
268
        throw new LogicException('public static function query() is not implemented');
269
    }
270
271
    /**
272
     * Handle dynamic static method calls into a new query.
273
     *
274
     * @param  string  $method
275
     * @param  array  $parameters
276
     * @return mixed
277
     */
278
    public static function __callStatic($method, $parameters)
279
    {
280
        return static::query()->$method(...$parameters);
281
    }
282
283
    /**
284
     * Returns the value of a model property.
285
     *
286
     * This method will check in the following order and act accordingly:
287
     *
288
     *  - a property defined by a getter: return the getter result
289
     *
290
     * Do not call this method directly as it is a PHP magic method that
291
     * will be implicitly called when executing `$value = $component->property;`.
292
     * @param string $name the property name
293
     * @return mixed the property value
294
     * @throws \Exception if the property is not defined
295
     * @see __set()
296
     */
297
    public function __get($name)
298
    {
299
        // Если уже сохранен такой релейшн, то возьмем его
300
        if (isset($this->related[$name]) || array_key_exists($name, $this->related)) {
301
            return $this->related[$name];
302
        }
303
304
        // Если нет сохраненных данных, ищем подходящий геттер
305
        $getter = $name;
306
        if (method_exists($this, $getter)) {
307
            // read property, e.g. getName()
308
            $value = $this->$getter();
309
310
            // Если геттер вернул запрос, значит $name - релейшен. Нужно выполнить запрос и сохранить во внутренний массив
311
            if ($value instanceof BaseQuery) {
312
                $this->related[$name] = $value->findFor();
313
                return $this->related[$name];
314
            }
315
        }
316
317
        throw new \Exception('Getting unknown property: ' . get_class($this) . '::' . $name);
318
    }
319
320
    /**
321
     * Получить запрос для релейшена по имени
322
     * @param string $name - название релейшена, например `orders` для релейшена, определенного через метод getOrders()
323
     * @param bool $throwException - кидать ли исключение в случае ошибки
324
     * @return BaseQuery - запрос для подгрузки релейшена
325
     * @throws \InvalidArgumentException
326
     */
327
    public function getRelation($name, $throwException = true)
328
    {
329
        $getter = $name;
330
        try {
331
            $relation = $this->$getter();
332
        } catch (\BadMethodCallException $e) {
333
            if ($throwException) {
334
                throw new \InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
335
            }
336
337
            return null;
338
        }
339
340
        if (!$relation instanceof BaseQuery) {
341
            if ($throwException) {
342
                throw new \InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".');
343
            }
344
345
            return null;
346
        }
347
348
        return $relation;
349
    }
350
351
    /**
352
     * Reset event errors back to default.
353
     */
354
    protected function resetEventErrors()
355
    {
356
        $this->eventErrors = [];
357
    }
358
359
    /**
360
     * Declares a `has-one` relation.
361
     * The declaration is returned in terms of a relational [[BaseQuery]] instance
362
     * through which the related record can be queried and retrieved back.
363
     *
364
     * A `has-one` relation means that there is at most one related record matching
365
     * the criteria set by this relation, e.g., a customer has one country.
366
     *
367
     * For example, to declare the `country` relation for `Customer` class, we can write
368
     * the following code in the `Customer` class:
369
     *
370
     * ```php
371
     * public function country()
372
     * {
373
     *     return $this->hasOne(Country::className(), 'ID', 'PROPERTY_COUNTRY');
374
     * }
375
     * ```
376
     *
377
     * Note that in the above, the 'ID' key in the `$link` parameter refers to an attribute name
378
     * in the related class `Country`, while the 'PROPERTY_COUNTRY' value refers to an attribute name
379
     * in the current BaseBitrixModel class.
380
     *
381
     * Call methods declared in [[BaseQuery]] to further customize the relation.
382
     *
383
     * @param string $class the class name of the related record
384
     * @param string $foreignKey
385
     * @param string $localKey
386
     * @return BaseQuery the relational query object.
387
     */
388
    public function hasOne($class, $foreignKey, $localKey = 'ID')
389
    {
390
        return $this->createRelationQuery($class, $foreignKey, $localKey, false);
391
    }
392
393
    /**
394
     * Declares a `has-many` relation.
395
     * The declaration is returned in terms of a relational [[BaseQuery]] instance
396
     * through which the related record can be queried and retrieved back.
397
     *
398
     * A `has-many` relation means that there are multiple related records matching
399
     * the criteria set by this relation, e.g., a customer has many orders.
400
     *
401
     * For example, to declare the `orders` relation for `Customer` class, we can write
402
     * the following code in the `Customer` class:
403
     *
404
     * ```php
405
     * public function orders()
406
     * {
407
     *     return $this->hasMany(Order::className(), 'PROPERTY_COUNTRY_VALUE', 'ID');
408
     * }
409
     * ```
410
     *
411
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to
412
     * an attribute name in the related class `Order`, while the 'id' value refers to
413
     * an attribute name in the current BaseBitrixModel class.
414
     *
415
     * Call methods declared in [[BaseQuery]] to further customize the relation.
416
     *
417
     * @param string $class the class name of the related record
418
     * @param string $foreignKey
419
     * @param string $localKey
420
     * @return BaseQuery the relational query object.
421
     */
422
    public function hasMany($class, $foreignKey, $localKey = 'ID')
423
    {
424
        return $this->createRelationQuery($class, $foreignKey, $localKey, true);
425
    }
426
427
    /**
428
     * Creates a query instance for `has-one` or `has-many` relation.
429
     * @param string $class the class name of the related record.
430
     * @param string $foreignKey
431
     * @param string $localKey
432
     * @param bool $multiple whether this query represents a relation to more than one record.
433
     * @return BaseQuery the relational query object.
434
     * @see hasOne()
435
     * @see hasMany()
436
     */
437
    protected function createRelationQuery($class, $foreignKey, $localKey, $multiple)
438
    {
439
        /* @var $class BaseBitrixModel */
440
        /* @var $query BaseQuery */
441
        $query = $class::query();
442
        $query->foreignKey = $localKey;
443
        $query->localKey = $foreignKey;
444
        $query->primaryModel = $this;
445
        $query->multiple = $multiple;
446
        return $query;
447
    }
448
449
    /**
450
     * Записать модели как связанные
451
     * @param string $name - название релейшена
452
     * @param Collection|BaseBitrixModel $records - связанные модели
453
     * @see getRelation()
454
     */
455
    public function populateRelation($name, $records)
456
    {
457
        $this->related[$name] = $records;
458
    }
459
}
460