Completed
Push — dev ( 0c2712...fb2e66 )
by Andrey
01:42
created

MenuWidget::currentItemContainerTag()   A

Complexity

Conditions 2
Paths 2

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