Completed
Push — master ( 0c2712...6cfaee )
by Andrey
01:26
created

MenuWidget::checkNewParentId()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 9
nc 3
nop 4
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
     * Reassigning child objects to their new parent after delete the main model record.
121
     * @param ActiveRecord $mainModel
122
     * @param string $primaryKeyName
123
     * @param string $parentKeyName
124
     */
125
    public static function afterDeleteMainModel(ActiveRecord $mainModel, string $primaryKeyName = 'id', string $parentKeyName = 'parentId'): void
126
    {
127
        $mainModel::updateAll([$parentKeyName => $mainModel->{$parentKeyName}], ['=', $parentKeyName, $mainModel->{$primaryKeyName}]);
128
    }
129
130
    /**
131
     * Check for configure.
132
     * @throws InvalidConfigException
133
     */
134
    private function checkConfiguration()
135
    {
136
        if (null === $this->itemTemplate){
137
            throw  new InvalidConfigException('Item template is not defined.');
138
        }
139
140
        if (is_array($this->itemTemplate) && !isset($this->itemTemplate['levels'])){
141
            throw  new InvalidConfigException('If item template is array, that has to contain levels key.');
142
        }
143
    }
144
145
    /**
146
     * Group records in to sub levels according with the relation to parent records.
147
     * @param array $models
148
     * @throws InvalidConfigException
149
     * @return array
150
     */
151
    private function groupLevels(array $models): array
152
    {
153
        if (count($models) == 0){
154
            return [];
155
        }
156
157
        $items = [];
158
159
        /** @var ActiveRecord $item */
160
        $modelsCount = count($models);
161
        for ($i=0; $i < $modelsCount; $i++) {
162
            $item = $models[$i];
163
164
            if (!($item instanceof ActiveRecord)){
165
                throw  new InvalidConfigException('Record with '.$i.' key must be an instance of ActiveRecord.');
166
            }
167
168
            $items[$item->{$this->primaryKeyName}]['data'] = $item;
169
        }
170
171
        /** @var ActiveRecord $data */
172
        foreach($items as $row) {
173
            $data = $row['data'];
174
            $parentKey = !isset($data->{$this->parentKeyName}) || empty($data->{$this->parentKeyName}) ? 0 : $data->{$this->parentKeyName};
175
            $items[$parentKey]['items'][$data->{$this->primaryKeyName}] = &$items[$data->{$this->primaryKeyName}];
176
        }
177
178
        return $items[0]['items'];
179
    }
180
181
    /**
182
     * Base render.
183
     * @param array $items
184
     * @param int $level
185
     * @return string
186
     */
187
    private function renderItems(array $items, int $level = 0): string
188
    {
189
        if (count($items) == 0){
190
            return '';
191
        }
192
193
        $outPut = '';
194
195
        /** @var array $item */
196
        foreach ($items as $item) {
197
            $contentLi = $this->render($this->levelAttributeValue('itemTemplate', $level), ArrayHelper::merge([
198
                'data' => $item['data']
199
            ], $this->levelAttributeValue('itemTemplateParams', $level)));
200
201
            if (isset($item['items'])){
202
                $contentLi .= $this->renderItems($item['items'], $level + 1);
203
            }
204
            $outPut .= Html::tag($this->itemContainerTag, $contentLi, $this->levelAttributeValue('itemContainerOptions', $level));
205
        }
206
207
        return Html::tag($this->mainContainerTag, $outPut, $this->levelAttributeValue('mainContainerOptions', $level));
208
    }
209
210
    /**
211
     * Get attribute values in current level.
212
     * @param string $attributeName
213
     * @param int $level
214
     * @throws InvalidConfigException
215
     * @return mixed
216
     */
217
    private function levelAttributeValue(string $attributeName, int $level)
218
    {
219
        $attributeValue = $this->{$attributeName};
220
221
        if (is_string($attributeValue)){
222
            return $attributeValue;
223
        }
224
225
        if (is_array($attributeValue) && !isset($attributeValue['levels'])){
226
            return $attributeValue;
227
        }
228
229
        if (is_array($attributeValue) && isset($attributeValue['levels'])){
230
231
            $countLevels = count($attributeValue['levels']);
232
233
            if ($countLevels == 0){
234
                throw new InvalidConfigException('Level values are not defined for attribute '.$attributeName.'.');
235
            }
236
237
            return isset($attributeValue['levels'][$level]) ? $attributeValue['levels'][$level] : $attributeValue['levels'][($countLevels-1)];
238
        }
239
240
        throw new InvalidConfigException('Attribute '.$attributeName.' is not defined correctly.');
241
    }
242
}
243