Issues (1)

src/MenuWidget.php (1 issue)

1
<?php
2
3
namespace Itstructure\MultiLevelMenu;
4
5
use yii\db\ActiveRecord;
6
use yii\helpers\{Html, ArrayHelper};
7
use yii\base\{Widget, InvalidConfigException};
8
9
/**
10
 * Class MenuWidget.
11
 * Multilevel menu widget.
12
 *
13
 * @property string $menuId Init level menu html tag id.
14
 * @property string $primaryKeyName Primary key name.
15
 * @property string $parentKeyName Relation key name.
16
 * @property string|array|callable $mainContainerTag Main container html tag.
17
 * @property array|callable $mainContainerOptions Main container html options.
18
 * @property string|array|callable $itemContainerTag Item container html tag.
19
 * @property array|callable $itemContainerOptions Item container html options.
20
 * @property string|array|callable $itemTemplate Item template to display widget elements.
21
 * @property array|callable $itemTemplateParams Addition item template params.
22
 * @property ActiveRecord[] $data Data records.
23
 *
24
 * @package Itstructure\MultiLevelMenu
25
 *
26
 * @author Andrey Girnik <[email protected]>
27
 */
28
class MenuWidget extends Widget
29
{
30
    /**
31
     * Init level menu html tag id.
32
     *
33
     * @var string
34
     */
35
    public $menuId;
36
37
    /**
38
     * Primary key name.
39
     *
40
     * @var string
41
     */
42
    public $primaryKeyName = 'id';
43
44
    /**
45
     * Relation key name.
46
     *
47
     * @var string
48
     */
49
    public $parentKeyName = 'parentId';
50
51
    /**
52
     * Main container html tag.
53
     *
54
     * @var string|array|callable
55
     */
56
    public $mainContainerTag = 'ul';
57
58
    /**
59
     * Main container html options.
60
     *
61
     * @var array|callable
62
     */
63
    public $mainContainerOptions = [];
64
65
    /**
66
     * Item container html tag.
67
     *
68
     * @var string|array|callable
69
     */
70
    public $itemContainerTag = 'li';
71
72
    /**
73
     * Item container html options.
74
     *
75
     * @var array|callable
76
     */
77
    public $itemContainerOptions = [];
78
79
    /**
80
     * Item template to display widget elements.
81
     *
82
     * @var string|array|callable
83
     */
84
    public $itemTemplate;
85
86
    /**
87
     * Addition item template params.
88
     *
89
     * @var array|callable
90
     */
91
    public $itemTemplateParams = [];
92
93
    /**
94
     * Data records.
95
     *
96
     * @var ActiveRecord[]
97
     */
98
    public $data;
99
100
    /**
101
     * Starts the output widget of the multi level view records according with the menu type.
102
     *
103
     * @throws InvalidConfigException
104
     */
105
    public function run()
106
    {
107
        $this->checkConfiguration();
108
109
        return $this->renderItems($this->groupLevels($this->data));
110
    }
111
112
    /**
113
     * Check whether a particular record can be used as a parent.
114
     *
115
     * @param ActiveRecord $mainModel
116
     * @param int $newParentId
117
     * @param string $primaryKeyName
118
     * @param string $parentKeyName
119
     *
120
     * @return bool
121
     */
122
    public static function checkNewParentId(
123
        ActiveRecord $mainModel,
124
        int $newParentId,
125
        string $primaryKeyName = 'id',
126
        string $parentKeyName = 'parentId'
127
    ): bool {
128
129
        $parentRecord = $mainModel::find()->select([
130
            $primaryKeyName,
131
            $parentKeyName
132
        ])->where([
133
            $primaryKeyName => $newParentId
134
        ])->one();
135
136
        if ($mainModel->{$primaryKeyName} === $parentRecord->{$primaryKeyName}) {
137
            return false;
138
        }
139
140
        if (null === $parentRecord->{$parentKeyName}) {
141
            return true;
142
        }
143
144
        return static::checkNewParentId($mainModel, $parentRecord->{$parentKeyName});
145
    }
146
147
    /**
148
     * Reassigning child objects to their new parent after delete the main model record.
149
     *
150
     * @param ActiveRecord $mainModel
151
     * @param string $primaryKeyName
152
     * @param string $parentKeyName
153
     *
154
     * @return void
155
     */
156
    public static function afterDeleteMainModel(
157
        ActiveRecord $mainModel,
158
        string $primaryKeyName = 'id',
159
        string $parentKeyName = 'parentId'
160
    ): void {
161
162
        $mainModel::updateAll([
163
            $parentKeyName => $mainModel->{$parentKeyName}
164
        ], [
165
            '=', $parentKeyName, $mainModel->{$primaryKeyName}
166
        ]);
167
    }
168
169
    /**
170
     * Check for configure.
171
     *
172
     * @throws InvalidConfigException
173
     */
174
    private function checkConfiguration()
175
    {
176
        if (null === $this->itemTemplate) {
177
            throw new InvalidConfigException('Item template is not defined.');
178
        }
179
180
        if (is_array($this->itemTemplate) && !isset($this->itemTemplate['levels'])) {
181
            throw new InvalidConfigException('If item template is array, that has to contain levels key.');
182
        }
183
    }
184
185
    /**
186
     * Group records in to sub levels according with the relation to parent records.
187
     *
188
     * @param array $models
189
     *
190
     * @throws InvalidConfigException
191
     *
192
     * @return array
193
     */
194
    private function groupLevels(array $models): array
195
    {
196
        $modelsCount = count($models);
197
198
        if ($modelsCount == 0) {
199
            return [];
200
        }
201
202
        $items = [];
203
204
        /** @var ActiveRecord $item */
205
        for ($i=0; $i < $modelsCount; $i++) {
206
            $item = $models[$i];
207
208
            if (!($item instanceof ActiveRecord)) {
209
                throw  new InvalidConfigException('Record with '.$i.' key must be an instance of ActiveRecord.');
210
            }
211
212
            $items[$item->{$this->primaryKeyName}]['data'] = $item;
213
        }
214
215
        /** @var ActiveRecord $data */
216
        foreach ($items as $row) {
217
218
            $data = $row['data'];
219
220
            $parentKey = !isset($data->{$this->parentKeyName}) || empty($data->{$this->parentKeyName}) ?
221
                0 : $data->{$this->parentKeyName};
222
223
            $items[$parentKey]['items'][$data->{$this->primaryKeyName}] = &$items[$data->{$this->primaryKeyName}];
224
        }
225
226
        return $items[0]['items'];
227
    }
228
229
    /**
230
     * Base render.
231
     *
232
     * @param array $items
233
     * @param int $level
234
     * @param array $parentItem
235
     *
236
     * @return string
237
     */
238
    private function renderItems(array $items, int $level = 0, $parentItem = []): string
239
    {
240
        if (count($items) == 0) {
241
            return '';
242
        }
243
244
        $outPut = '';
245
246
        /** @var array $item */
247
        foreach ($items as $item) {
248
249
            $contentLi = $this->render($this->levelAttributeValue($this->itemTemplate, $level, $item), ArrayHelper::merge([
250
                'data' => $item['data']
251
            ], $this->levelAttributeValue($this->itemTemplateParams, $level, $item)));
252
253
            if (isset($item['items'])) {
254
                $contentLi .= $this->renderItems($item['items'], $level + 1, $item);
255
            }
256
257
            $itemContainerTag = $this->levelAttributeValue($this->itemContainerTag, $level, $item);
258
            $outPut .= Html::tag(
259
                $itemContainerTag,
260
                $contentLi,
261
                $this->levelAttributeValue($this->itemContainerOptions, $level, $item)
0 ignored issues
show
It seems like $this->levelAttributeVal...Options, $level, $item) can also be of type string; however, parameter $options of yii\helpers\BaseHtml::tag() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

261
                /** @scrutinizer ignore-type */ $this->levelAttributeValue($this->itemContainerOptions, $level, $item)
Loading history...
262
            );
263
        }
264
265
        $mainContainerTag = $this->levelAttributeValue($this->mainContainerTag, $level, $parentItem);
266
        $mainContainerOptions = $this->levelAttributeValue($this->mainContainerOptions, $level, $parentItem);
267
268
        if ($level == 0 && null !== $this->menuId) {
269
            $mainContainerOptions = ArrayHelper::merge($mainContainerOptions, [
270
                'id' => $this->menuId
271
            ]);
272
        }
273
274
        return Html::tag($mainContainerTag, $outPut, $mainContainerOptions);
275
    }
276
277
    /**
278
     * Get attribute values in current level.
279
     *
280
     * @param $attributeValue
281
     * @param int $level
282
     * @param array $item
283
     *
284
     * @return mixed
285
     *
286
     * @throws InvalidConfigException
287
     */
288
    private function levelAttributeValue($attributeValue, int $level, array $item = [])
289
    {
290
        if (is_string($attributeValue)) {
291
            return $attributeValue;
292
        }
293
294
        if (is_callable($attributeValue)) {
295
            return call_user_func($attributeValue, $level, $item);
296
        }
297
298
        if (is_array($attributeValue) && !isset($attributeValue['levels'])) {
299
            return $attributeValue;
300
        }
301
302
        if (is_array($attributeValue) && isset($attributeValue['levels'])) {
303
304
            $countLevels = count($attributeValue['levels']);
305
306
            if ($countLevels == 0) {
307
                throw new InvalidConfigException('Level values are not defined for attribute.');
308
            }
309
310
            $levelValue = isset($attributeValue['levels'][$level]) ?
311
                $attributeValue['levels'][$level] : $attributeValue['levels'][($countLevels-1)];
312
313
            if (is_callable($levelValue)) {
314
                return call_user_func($levelValue, $level, $item);
315
            }
316
317
            return $levelValue;
318
        }
319
320
        throw new InvalidConfigException('Attribute is not defined correctly.');
321
    }
322
}
323