ColumnModel   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 341
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 98.29%

Importance

Changes 0
Metric Value
wmc 48
lcom 1
cbo 8
dl 0
loc 341
ccs 115
cts 117
cp 0.9829
rs 8.5599
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getFormatters() 0 11 3
A exclude() 0 9 1
A getColumns() 0 11 4
A setFormatter() 0 6 1
A __construct() 0 5 1
A addRowRenderer() 0 15 3
A getRowRenderers() 0 4 1
A getUniqueFormatters() 0 22 5
B add() 0 38 10
A exists() 0 8 2
A getExcluded() 0 11 3
A get() 0 8 2
A sort() 0 21 4
A includeOnly() 0 24 4
A search() 0 8 2
A setMetatadata() 0 6 1
A getMetadata() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like ColumnModel often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ColumnModel, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * soluble-flexstore library
7
 *
8
 * @author    Vanvelthem Sébastien
9
 * @link      https://github.com/belgattitude/soluble-flexstore
10
 * @copyright Copyright (c) 2016-2017 Vanvelthem Sébastien
11
 * @license   MIT License https://github.com/belgattitude/soluble-flexstore/blob/master/LICENSE.md
12
 *
13
 */
14
15
namespace Soluble\FlexStore\Column;
16
17
use Soluble\FlexStore\Renderer\RowRendererInterface;
18
use Soluble\FlexStore\Column\ColumnModel\Search;
19
use Soluble\FlexStore\Formatter\FormatterInterface;
20
use Soluble\Metadata\ColumnsMetadata;
21
use ArrayObject;
22
23
class ColumnModel
24
{
25
    const ADD_COLUMN_AFTER = 'after';
26
    const ADD_COLUMN_BEFORE = 'before';
27
28
    /**
29
     * @var ArrayObject
30
     */
31
    protected $columns;
32
33
    /**
34
     * @var Search
35
     */
36
    protected $search;
37
38
    /**
39
     * @var ArrayObject
40
     */
41
    protected $row_renderers;
42
43
    /**
44
     * @var ColumnsMetadata|null
45
     */
46
    protected $metadata;
47
48 30
    public function __construct()
49
    {
50 30
        $this->columns = new ArrayObject();
51 30
        $this->row_renderers = new ArrayObject();
52 30
    }
53
54
    /**
55
     * Add a row renderer.
56
     *
57
     * @throws Exception\InvalidArgumentException
58
     *
59
     * @param RowRendererInterface $renderer
60
     */
61 6
    public function addRowRenderer(RowRendererInterface $renderer): void
62
    {
63
        // Test if all required columns are present in column model
64 6
        $required_columns = $renderer->getRequiredColumns();
65
66 6
        foreach ($required_columns as $column) {
67 2
            if (!$this->exists($column)) {
68 1
                $cls = get_class($renderer);
69 1
                $msg = "Renderer '$cls' requires column '$column' to be present in column model.";
70 2
                throw new Exception\MissingColumnException(__METHOD__ . ': ' . $msg);
71
            }
72
        }
73
74 5
        $this->row_renderers->append($renderer);
75 5
    }
76
77
    /**
78
     * @return ArrayObject
79
     */
80 9
    public function getRowRenderers()
81
    {
82 9
        return $this->row_renderers;
83
    }
84
85
    /**
86
     * Return an array object containing all
87
     * columns that have a formatter (FormatterInterface).
88
     * [column_name] => [FormatterInterface].
89
     *
90
     * @see self::getUniqueFormatters()
91
     */
92 9
    public function getFormatters(): ArrayObject
93
    {
94 9
        $arr = new ArrayObject();
95 9
        foreach ($this->columns as $key => $column) {
96 9
            if (($formatter = $column->getFormatter()) !== null) {
97 9
                $arr->offsetSet($key, $formatter);
98
            }
99
        }
100
101 9
        return $arr;
102
    }
103
104
    /**
105
     * This method returns unique formatters set in the column model
106
     * in an ArrayObject.
107
     *
108
     *
109
     * @param bool $include_excluded_columns
110
     *
111
     * @see self::getFormatters()
112
     */
113 9
    public function getUniqueFormatters(bool $include_excluded_columns = false): ArrayObject
114
    {
115 9
        $unique = new ArrayObject();
116
117 9
        $formatters = $this->getFormatters();
118 9
        foreach ($formatters as $column => $formatter) {
119 2
            if ($include_excluded_columns || !$this->get($column)->isExcluded()) {
120 2
                $hash = spl_object_hash($formatter);
121 2
                if (!$unique->offsetExists($hash)) {
122 2
                    $tmp = new ArrayObject([
123 2
                                                'formatter' => $formatter,
124 2
                                                'columns' => new ArrayObject([$column])
125
                    ]);
126 2
                    $unique->offsetSet($hash, $tmp);
127
                } else {
128 2
                    $unique->offsetGet($hash)->offsetGet('columns')->append($column);
129
                }
130
            }
131
        }
132
133 9
        return $unique;
134
    }
135
136
    /**
137
     * Add a new column to the column model.
138
     *
139
     * @throws Exception\InvalidArgumentException when mode is not supported
140
     * @throws Exception\DuplicateColumnException when column name already exists
141
     * @throws Exception\ColumnNotFoundException  when after_column does not exists
142
     *
143
     * @param string $after_column add the new column after this existing one
144
     * @param string $mode         change after to before (see self::ADD_COLUMN_AFTER, self::ADD_COLUMN_BEFORE)
145
     */
146 30
    public function add(Column $column, string $after_column = null, string $mode = self::ADD_COLUMN_AFTER): self
147
    {
148 30
        $name = $column->getName();
149 30
        if ($this->exists($name)) {
150 1
            $msg = "Cannot add column '$name', it's already present in column model";
151 1
            throw new Exception\DuplicateColumnException(__METHOD__ . ': ' . $msg);
152
        }
153
154 30
        if ($after_column !== null) {
155
            // Test existence of column
156 2
            if (!$this->exists($after_column)) {
157 1
                $msg = "Cannot add column '$name' after '$after_column', column does not exists.";
158 1
                throw new Exception\ColumnNotFoundException(__METHOD__ . ': ' . $msg);
159
            }
160
161 2
            if (!in_array($mode, [self::ADD_COLUMN_BEFORE, self::ADD_COLUMN_AFTER], true)) {
162 1
                $msg = "Cannot add column '$name', invalid mode specified '$mode'";
163 1
                throw new Exception\InvalidArgumentException(__METHOD__ . ': ' . $msg);
164
            }
165
166 2
            $new_columns = new ArrayObject();
167 2
            foreach ($this->columns as $key => $col) {
168 2
                if ($mode === self::ADD_COLUMN_BEFORE && $key === $after_column) {
169 1
                    $new_columns->offsetSet($name, $column);
170
                }
171 2
                $new_columns->offsetSet($key, $col);
172 2
                if ($mode === self::ADD_COLUMN_AFTER && $key === $after_column) {
173 2
                    $new_columns->offsetSet($name, $column);
174
                }
175
            }
176 2
            $this->columns->exchangeArray($new_columns);
177
        } else {
178
            // Simply append
179 30
            $this->columns->offsetSet($name, $column);
180
        }
181
182 30
        return $this;
183
    }
184
185
    /**
186
     * Tells whether a column exists.
187
     *
188
     * @throws Exception\InvalidArgumentException
189
     */
190 30
    public function exists(string $column): bool
191
    {
192 30
        if (trim($column) === '') {
193 1
            throw new Exception\InvalidArgumentException(__METHOD__ . ' Column name cannot be empty');
194
        }
195
196 30
        return $this->columns->offsetExists($column);
197
    }
198
199
    /**
200
     * Return column that have been excluded in getData() and getColumns().
201
     *
202
     * @return array
203
     */
204 4
    public function getExcluded(): array
205
    {
206 4
        $arr = [];
207 4
        foreach ($this->columns as $name => $column) {
208 4
            if ($column->isExcluded()) {
209 4
                $arr[] = $name;
210
            }
211
        }
212
213 4
        return $arr;
214
    }
215
216
    /**
217
     * Return column from identifier name.
218
     *
219
     * @param string $column column name
220
     *
221
     * @throws Exception\InvalidArgumentException
222
     * @throws Exception\ColumnNotFoundException  when column does not exists in model
223
     */
224 13
    public function get($column): Column
225
    {
226 13
        if (!$this->exists($column)) {
227 1
            throw new Exception\ColumnNotFoundException(__METHOD__ . " Column '$column' not present in column model.");
228
        }
229
230 12
        return $this->columns->offsetGet($column);
231
    }
232
233
    /**
234
     * Sort columns in the order specified, columns that exists
235
     * in the dataset but not in the sorted_columns will be
236
     * appended to the end.
237
     *
238
     * @param string[] $sorted_columns
239
     */
240 7
    public function sort(array $sorted_columns): self
241
    {
242 7
        $diff = array_diff_assoc($sorted_columns, array_unique($sorted_columns));
243 7
        if (count($diff) > 0) {
244 1
            $cols = implode(',', $diff);
245 1
            throw new Exception\DuplicateColumnException(__METHOD__ . " Duplicate column found in paramter sorted_columns : '$cols'");
246
        }
247 6
        $columns = [];
248
249 6
        foreach ($sorted_columns as $idx => $column) {
250 6
            if (!$this->exists($column)) {
251 1
                throw new Exception\InvalidArgumentException(__METHOD__ . " Column '$column' does not exists.");
252
            }
253 6
            $columns[$column] = $this->get($column);
254
        }
255
        // Appending eventual non sorted columns at the end
256 5
        $columns = array_merge($columns, (array) $this->columns);
257 5
        $this->columns->exchangeArray($columns);
258
259 5
        return $this;
260
    }
261
262
    /**
263
     * Set column that must be excluded in getData() and getColumns().
264
     *
265
     * @param string[] $excluded_columns column names to exclude
266
     * @param bool     $excluded         whether to set exclude to true (default) or false (opposite: include)
267
     */
268 6
    public function exclude(array $excluded_columns, bool $excluded = true): self
269
    {
270
        // trim column names automatically
271 6
        $excluded_columns = array_map('trim', $excluded_columns);
272
273 6
        $this->search()->in($excluded_columns)->setExcluded($excluded);
274
275 6
        return $this;
276
    }
277
278
    /**
279
     * Exclude all other columns that the one specified
280
     * Column sort is preserved in getData().
281
     *
282
     * @throws Exception\InvalidArgumentException
283
     *
284
     * @param string[] $include_only_columns
285
     * @param bool     $sort                 automatically apply sortColumns
286
     * @param bool     $preserve_excluded    preserve excluded columns
287
     */
288 2
    public function includeOnly(array $include_only_columns, bool $sort = true, bool $preserve_excluded = true): self
289
    {
290
        // trim column
291 2
        $include_only_columns = array_map('trim', $include_only_columns);
292
293 2
        if ($preserve_excluded) {
294 2
            $previous_excluded_cols = $this->getExcluded();
295
        } else {
296 1
            $previous_excluded_cols = [];
297
        }
298
299 2
        $this->search()->all()->setExcluded(true);
300 2
        $this->search()->in($include_only_columns)->setExcluded(false);
301
302 2
        if ($sort) {
303 2
            $this->sort($include_only_columns);
304
        }
305
306 2
        if (count($previous_excluded_cols) > 0) {
307 1
            $this->exclude($previous_excluded_cols);
308
        }
309
310 2
        return $this;
311
    }
312
313
    /**
314
     * Return columns.
315
     */
316 19
    public function getColumns($include_excluded_columns = false): ArrayObject
317
    {
318 19
        $arr = new ArrayObject();
319 19
        foreach ($this->columns as $key => $column) {
320 19
            if ($include_excluded_columns || !$column->isExcluded()) {
321 19
                $arr->offsetSet($key, $column);
322
            }
323
        }
324
325 19
        return $arr;
326
    }
327
328
    /**
329
     * Set formatter to specific columns.
330
     *
331
     * @throws Exception\InvalidArgumentException
332
     *
333
     * @param FormatterInterface $formatter
334
     * @param string[]           $columns
335
     */
336 1
    public function setFormatter(FormatterInterface $formatter, array $columns): self
337
    {
338 1
        $this->search()->in($columns)->setFormatter($formatter);
339
340 1
        return $this;
341
    }
342
343 11
    public function search(): Search
344
    {
345 11
        if ($this->search === null) {
346 11
            $this->search = new Search($this->columns);
347
        }
348
349 11
        return $this->search;
350
    }
351
352 30
    public function setMetatadata(ColumnsMetadata $metadata): self
353
    {
354 30
        $this->metadata = $metadata;
355
356 30
        return $this;
357
    }
358
359
    public function getMetadata(): ?ArrayObject
360
    {
361
        return $this->metadata;
362
    }
363
}
364