Completed
Push — dev ( 6cfaee...600c1b )
by Andrey
01:41
created

MenuWidget::levelAttributeValue()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 12
nc 6
nop 2
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 $mainContainerTag Main container html tag.
17
 * @property array $mainContainerOptions Main container html options.
18
 * @property string $itemContainerTag Item container html tag.
19
 * @property array $itemContainerOptions Item container html options.
20
 * @property string|array $itemTemplate Item template to display widget elements.
21
 * @property array $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
     * @var string
33
     */
34
    public $menuId;
35
36
    /**
37
     * Primary key name.
38
     * @var string
39
     */
40
    public $primaryKeyName = 'id';
41
42
    /**
43
     * Relation key name.
44
     * @var string
45
     */
46
    public $parentKeyName = 'parentId';
47
48
    /**
49
     * Main container html tag.
50
     * @var string
51
     */
52
    public $mainContainerTag = 'ul';
53
54
    /**
55
     * Main container html options.
56
     * @var array
57
     */
58
    public $mainContainerOptions = [];
59
60
    /**
61
     * Item container html tag.
62
     * @var string
63
     */
64
    public $itemContainerTag = 'li';
65
66
    /**
67
     * Item container html options.
68
     * @var array
69
     */
70
    public $itemContainerOptions = [];
71
72
    /**
73
     * Item template to display widget elements.
74
     * @var string|array
75
     */
76
    public $itemTemplate;
77
78
    /**
79
     * Addition item template params.
80
     * @var array
81
     */
82
    public $itemTemplateParams = [];
83
84
    /**
85
     * Data records.
86
     * @var ActiveRecord[]
87
     */
88
    public $data;
89
90
    /**
91
     * Starts the output widget of the multi level view records according with the menu type.
92
     * @throws InvalidConfigException
93
     */
94
    public function run()
95
    {
96
        $this->checkConfiguration();
97
98
        return $this->renderItems($this->groupLevels($this->data));
99
    }
100
101
    /**
102
     * Check whether a particular record can be used as a parent.
103
     * @param ActiveRecord $mainModel
104
     * @param int $newParentId
105
     * @param string $primaryKeyName
106
     * @param string $parentKeyName
107
     * @return bool
108
     */
109
    public static function checkNewParentId(ActiveRecord $mainModel, int $newParentId, string $primaryKeyName = 'id', string $parentKeyName = 'parentId'): bool
110
    {
111
        $parentRecord = $mainModel::find()->select([$primaryKeyName, $parentKeyName])->where([
112
            $primaryKeyName => $newParentId
113
        ])->one();
114
115
        if ($mainModel->{$primaryKeyName} === $parentRecord->{$primaryKeyName}){
116
            return false;
117
        }
118
119
        if (null === $parentRecord->{$parentKeyName}){
120
            return true;
121
        }
122
123
        return static::checkNewParentId($mainModel, $parentRecord->{$parentKeyName});
124
    }
125
126
    /**
127
     * Reassigning child objects to their new parent after delete the main model record.
128
     * @param ActiveRecord $mainModel
129
     * @param string $primaryKeyName
130
     * @param string $parentKeyName
131
     */
132
    public static function afterDeleteMainModel(ActiveRecord $mainModel, string $primaryKeyName = 'id', string $parentKeyName = 'parentId'): void
133
    {
134
        $mainModel::updateAll([$parentKeyName => $mainModel->{$parentKeyName}], ['=', $parentKeyName, $mainModel->{$primaryKeyName}]);
135
    }
136
137
    /**
138
     * Check for configure.
139
     * @throws InvalidConfigException
140
     */
141
    private function checkConfiguration()
142
    {
143
        if (null === $this->itemTemplate){
144
            throw  new InvalidConfigException('Item template is not defined.');
145
        }
146
147
        if (is_array($this->itemTemplate) && !isset($this->itemTemplate['levels'])){
148
            throw  new InvalidConfigException('If item template is array, that has to contain levels key.');
149
        }
150
    }
151
152
    /**
153
     * Group records in to sub levels according with the relation to parent records.
154
     * @param array $models
155
     * @throws InvalidConfigException
156
     * @return array
157
     */
158
    private function groupLevels(array $models): array
159
    {
160
        if (count($models) == 0){
161
            return [];
162
        }
163
164
        $items = [];
165
166
        /** @var ActiveRecord $item */
167
        $modelsCount = count($models);
168
        for ($i=0; $i < $modelsCount; $i++) {
169
            $item = $models[$i];
170
171
            if (!($item instanceof ActiveRecord)){
172
                throw  new InvalidConfigException('Record with '.$i.' key must be an instance of ActiveRecord.');
173
            }
174
175
            $items[$item->{$this->primaryKeyName}]['data'] = $item;
176
        }
177
178
        /** @var ActiveRecord $data */
179
        foreach($items as $row) {
180
            $data = $row['data'];
181
            $parentKey = !isset($data->{$this->parentKeyName}) || empty($data->{$this->parentKeyName}) ? 0 : $data->{$this->parentKeyName};
182
            $items[$parentKey]['items'][$data->{$this->primaryKeyName}] = &$items[$data->{$this->primaryKeyName}];
183
        }
184
185
        return $items[0]['items'];
186
    }
187
188
    /**
189
     * Base render.
190
     * @param array $items
191
     * @param int $level
192
     * @return string
193
     */
194
    private function renderItems(array $items, int $level = 0): string
195
    {
196
        if (count($items) == 0){
197
            return '';
198
        }
199
200
        $outPut = '';
201
202
        /** @var array $item */
203
        foreach ($items as $item) {
204
            $contentLi = $this->render($this->levelAttributeValue('itemTemplate', $level), ArrayHelper::merge([
205
                'data' => $item['data']
206
            ], $this->levelAttributeValue('itemTemplateParams', $level)));
207
208
            if (isset($item['items'])){
209
                $contentLi .= $this->renderItems($item['items'], $level + 1);
210
            }
211
            $outPut .= Html::tag($this->itemContainerTag, $contentLi, $this->levelAttributeValue('itemContainerOptions', $level));
212
        }
213
214
        if ($level == 0 && null !== $this->menuId){
215
            $mainContainerOptions = $level==0 ? ArrayHelper::merge($this->levelAttributeValue('mainContainerOptions', $level), [
216
                'id' => $this->menuId
217
            ]) : $this->levelAttributeValue('mainContainerOptions', $level);
218
        } else {
219
            $mainContainerOptions = $this->levelAttributeValue('mainContainerOptions', $level);
220
        }
221
222
        return Html::tag($this->mainContainerTag, $outPut, $mainContainerOptions);
223
    }
224
225
    /**
226
     * Get attribute values in current level.
227
     * @param string $attributeName
228
     * @param int $level
229
     * @throws InvalidConfigException
230
     * @return mixed
231
     */
232
    private function levelAttributeValue(string $attributeName, int $level)
233
    {
234
        $attributeValue = $this->{$attributeName};
235
236
        if (is_string($attributeValue)){
237
            return $attributeValue;
238
        }
239
240
        if (is_array($attributeValue) && !isset($attributeValue['levels'])){
241
            return $attributeValue;
242
        }
243
244
        if (is_array($attributeValue) && isset($attributeValue['levels'])){
245
246
            $countLevels = count($attributeValue['levels']);
247
248
            if ($countLevels == 0){
249
                throw new InvalidConfigException('Level values are not defined for attribute '.$attributeName.'.');
250
            }
251
252
            return isset($attributeValue['levels'][$level]) ? $attributeValue['levels'][$level] : $attributeValue['levels'][($countLevels-1)];
253
        }
254
255
        throw new InvalidConfigException('Attribute '.$attributeName.' is not defined correctly.');
256
    }
257
}
258