GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

TabularList   B
last analyzed

Complexity

Total Complexity 49

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 96.88%

Importance

Changes 0
Metric Value
wmc 49
lcom 1
cbo 2
dl 0
loc 347
ccs 155
cts 160
cp 0.9688
rs 8.48
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A fromHtml() 0 6 1
A toHtmlTable() 0 6 1
A parseHtmlList() 0 31 4
A prepareHtmlList() 0 10 1
A extractNumCategories() 0 11 4
B parseUl() 0 53 6
B prepareTableDefinition() 0 21 6
B processList() 0 33 6
A extractNodeText() 0 21 2
A listToText() 0 19 4
A findDeep() 0 10 3
B createNewRowIfNeeded() 0 16 8
A createCell() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like TabularList 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 TabularList, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Trefoil\Helpers;
4
5
use Symfony\Component\DomCrawler\Crawler;
6
7
/**
8
 * Class TabularList
9
 *
10
 * This class manages a TabularList object, which represents an HTML list that can be alternatively shown as an HTML
11
 * table without loosing information (and vice versa).
12
 *
13
 * Its intended use is providing an adequate representation of tables in ebooks (where wide tables are not appropriate)
14
 * while maintaining the table as-is for wider formats, like PDF.
15
 *
16
 * Example source list:
17
 * <ul>
18
 *  <li><b>Category 1:</b> Value 1 for category 1
19
 *      <ul>
20
 *          <li><b>Attribute 1:</b> Value for attribute 1.1</li>
21
 *          <li><b>Attribute 2:</b> Value for attribute 1.2</li>
22
 *      </ul>
23
 *  </li>
24
 *  <li><b>Category 1:</b> Value 2 for category 1
25
 *      <ul>
26
 *          <li><b>Attribute 1:</b> Value for attribute 2.1</li>
27
 *          <li><b>Attribute 2:</b> Value for attribute 2.2</li>
28
 *      </ul>
29
 *  </li>
30
 * </ul>
31
 *
32
 * First level <li> are categories while other levels' <li>s are considered attributes. The amount of categories
33
 * is controlled by an argument at table rendering time.
34
 *
35
 * Example rendered table:
36
 * <table>
37
 *  <thead><tr><th>Category 1</th><th>Attribute 1</th><th>Attribute 2</th></tr></thead>
38
 *  <tbody>
39
 *      <tr><td>Value 1 for category 1</td><td>Value for attribute 1.1</td><td>Value for attribute 1.2</td></tr>
40
 *      <tr><td>Value 2 for category 1</td><td>Value for attribute 2.1</td><td>Value for attribute 2.2</td></tr>
41
 *  </tbody>
42
 *
43
 * @package Trefoil\Helpers
44
 */
45
class TabularList
46
{
47
    /**
48
     * @var Table
49
     */
50
    protected $table;
51
52
    /**
53
     * Number of list levels to be treated as categories.
54
     *
55
     * @var int|null
56
     */
57
    protected $numCategories = null;
58
59
    /**
60
     * Deep of the list (max number of levels).
61
     *
62
     * @var int
63
     */
64
    protected $deep = 0;
65
66 9
    public function __construct()
67
    {
68 9
        $this->table = new Table();
69 9
    }
70
71
    /**
72
     * @param string $htmlList string with HTML <ul> tag
73
     * @param int|null $numCategories number of list levels to be represented as categories (columns)
74
     *                                in the table (null=default)
75
     */
76 9
    public function fromHtml($htmlList, $numCategories = null)
77
    {
78 9
        $this->numCategories = $numCategories;
79
80 9
        $this->parseHtmlList($htmlList);
81 9
    }
82
83
    /**
84
     * @return string List rendered to HTML table
85
     */
86 9
    public function toHtmlTable()
87
    {
88 9
        $output = $this->table->toHtml();
89
90 9
        return $output;
91
    }
92
93
    /**
94
     * @param      $htmlList      string list as html (<ul>...</ul>)
95
     */
96 9
    protected function parseHtmlList($htmlList)
97
    {
98 9
        $htmlList = $this->prepareHtmlList($htmlList);
99
100 9
        $crawler = new Crawler();
101 9
        $crawler->addHtmlContent($htmlList, 'UTF-8');
102 9
        $crawler = $crawler->filter('ul');
103
104
        // not an <ul> list in the input 
105 9
        if ($crawler->count() === 0) {
106 1
            return;
107
        }
108
109 8
        $listAsArray = $this->parseUl($crawler);
110
111
        // extract the numcategories from the <ul> class, if present
112 8
        if ($this->numCategories === null) {
113 3
            $this->extractNumCategories($crawler);
114 3
        }
115
116
        // find max deep of tree and set default numcategories
117 8
        $this->findDeep($listAsArray);
118 8
        if ($this->numCategories === null) {
119 2
            $this->numCategories = $this->deep;
120 2
        }
121
122
        // ensure numcategories is never greater than deep
123 8
        $this->numCategories = min($this->numCategories, $this->deep);
124
125 8
        $this->prepareTableDefinition($listAsArray);
126 8
    }
127
128
    /**
129
     * Prepare the HTML source list, ensuring every <li>..</li> has a <p>..</p> surrounding the contents.
130
     *
131
     * @param $htmlList
132
     *
133
     * @return string
134
     */
135 9
    protected function prepareHtmlList($htmlList)
136
    {
137 9
        $htmlList = preg_replace('/<li>(?!<p>)([^(<\/li>)]*)\n/U', '<li><p>$1</p>' . "\n", $htmlList);
138 9
        $htmlList = preg_replace('/<li>(?!<p>)(.*)(?!<\/li>])\n/U', '<li><p>$1</p>' . "\n", $htmlList);
139 9
        $htmlList = preg_replace('/<li>(?!<p>)(.*)<\/li>/U', '<li><p>$1</p></li>', $htmlList);
140 9
        $htmlList = preg_replace('/<b>/U', '<strong>', $htmlList);
141 9
        $htmlList = preg_replace('/<\/b>/U', '</strong>', $htmlList);
142
143 9
        return $htmlList;
144
    }
145
146
    /**
147
     * @param Crawler $ulNode
148
     */
149 3
    protected function extractNumCategories(Crawler $ulNode)
150
    {
151 3
        $ulClasses = $ulNode->attr('class') ?: '';
152 3
        $matches = [];
153
154 3
        foreach (explode(' ', $ulClasses) as $class) {
155 3
            if (preg_match('/tabularlist-(\d*)$/', $class, $matches)) {
156 1
                $this->numCategories = (int)$matches[1];
157 1
            }
158 3
        }
159 3
    }
160
161
    /**
162
     * Parses a recursive <ul> list to an array.
163
     *
164
     * @param Crawler $ulNode
165
     *
166
     * @return array parsed <ul> list
167
     */
168 8
    protected function parseUl(Crawler $ulNode)
169
    {
170 8
        $output = [];
171
172 8
        $ulNode->children()->each(
173
            function (Crawler $liNode) use (&$output) {
174
175 8
                $cell = [];
176 8
                $cellText = '';
177
178 8
                $liNode->children()->each(
179 8
                    function (Crawler $liChildrenNode, $liChildrenIndex) use (&$cell, &$cellText) {
0 ignored issues
show
Unused Code introduced by
The parameter $liChildrenIndex is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
180
181 8
                        switch ($liChildrenNode->nodeName()) {
182 8
                            case 'p':
183
                                // append paragraphs to form the cell text
184 8
                                $cellText .= '<p>' . $liChildrenNode->html() . '</p>';
185 8
                                break;
186
187 8
                            case 'ol':
188
                                // ordered lists are treated as raw HTML, appending it to the cell text
189
                                $cellText .= '<ol>' . $liChildrenNode->html() . '</ol>';
190
                                break;
191
192 8
                            case 'ul' :
193
                                // get the collected text into the cell text
194 8
                                $cell['text'] = $cellText;
195 8
                                $cellText = '';
196
197
                                // an unordered list is a new list level
198 8
                                $cell['list'] = $this->parseUl($liChildrenNode);
199 8
                                break;
200
201
                            default:
202
                                // other tags are ignored
203
                                break;
204 8
                        }
205 8
                    }
206 8
                );
207
208
                // uncollected text
209 8
                if ($cellText) {
210 8
                    $cell['text'] = $cellText;
211 8
                }
212
213 8
                if (!empty($cell)) {
214 8
                    $output[] = $cell;
215 8
                }
216 8
            }
217 8
        );
218
219 8
        return $output;
220
    }
221
222
    /**
223
     * @param array $listAsArray the list definition as an array
224
     */
225 8
    protected function prepareTableDefinition(array $listAsArray)
226
    {
227
        // make the table body from the list
228 8
        $this->processList($listAsArray, 0);
229
230
        // ensure we have headings
231 8
        foreach ($this->table['tbody'] as $row) {
232 8
            $colIndex = 0;
233 8
            foreach ($row as $cell) {
234
235
                // detected heading for cell is in the extra
236 8
                $heading = isset($cell['extra']) ? $cell['extra'] : '';
237 8
                $existingHeading = $this->table->getHeadingCell($colIndex);
238 8
                if ($heading && !$existingHeading) {
239 8
                    $this->table->addHeadingCell($heading, $colIndex);
240 8
                }
241
242 8
                $colIndex++;
243 8
            }
244 8
        }
245 8
    }
246
247 8
    protected function processList($list, $level)
248
    {
249 8
        foreach ($list as $listNodeIndex => $listNode) {
250
251
            // if this is a 0-level node, add a new row
252 8
            $lastRow = $this->table->getBodyRowsCount() - 1;
253 8
            if ($level === 0 && $this->table->getBodyCellsCount($lastRow)) {
254 8
                $this->table->addBodyRow();
0 ignored issues
show
Unused Code introduced by
The call to the method Trefoil\Helpers\Table::addBodyRow() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
255 8
            }
256
257
            // extract heading from cell
258 8
            $node = $this->extractNodeText($listNode['text']);
259
260
            // add new row if needed to mantain the right table flow
261 8
            $this->createNewRowIfNeeded($level, $listNodeIndex);
262
263
            // start processing the node text, which each node will have
264 8
            $cellContents = $node['text'];
265
266
            // create new cell from node
267 8
            $where = $this->createCell($cellContents, $node);
268
269
            // process the node sublist, if present 
270 8
            if (isset($listNode['list'])) {
271 8
                if ($level < $this->numCategories) {
272 6
                    $this->processList($listNode['list'], $level + 1);
273 6
                } else {
274 4
                    $cellContents .= $this->listToText($listNode['list']);
275 4
                    $this->table->addBodyCell($cellContents, (int)$where['row'], (int)$where['column']);
276
                }
277 8
            }
278 8
        }
279 8
    }
280
281
    /**
282
     * Extract node text and heading from the original node text.
283
     *
284
     * @param $text
285
     *
286
     * @return array of 'text' and 'heading' components
287
     */
288 8
    protected function extractNodeText($text)
289
    {
290
        $node = [
291 8
            'text' => $text,
292
            'heading' => ''
293 8
        ];
294
295
        // extract heading
296 8
        $matches = [];
297
298 8
        if (preg_match(
299 8
            '/^(?<all><p><strong>(?<heading>.*)(?:(?::<\/strong>)|(?:<\/strong>:)))/U',
300 8
            $node['text'],
301
            $matches
302 8
        )) {
303 8
            $node['heading'] = $matches['heading'];
304 8
            $node['text'] = trim(substr($node['text'], strlen($matches['all'])));
305 8
        }
306
307 8
        return $node;
308
    }
309
310
    /**
311
     * Recursively collect all text in the list and its descendant nodes.
312
     *
313
     * @param $list
314
     *
315
     * @return string
316
     */
317 4
    protected function listToText($list)
318
    {
319 4
        $text = '';
320
321 4
        foreach ($list as $listNode) {
322
323 4
            if (isset($listNode['text'])) {
324 4
                $text .= '<li>' . $listNode['text'] . '</li>';
325 4
            }
326
327 4
            if (isset($listNode['list'])) {
328 3
                $text .= '<li style="list-style: none; display: inline">' . $this->listToText($listNode['list']) . '</li>';
329 3
            }
330 4
        }
331
332 4
        $text = '<ul>' . $text . '</ul>';
333
334 4
        return $text;
335
    }
336
337 8
    protected function findDeep(array $list, $countLevels = 1)
338
    {
339 8
        $this->deep = max($this->deep, $countLevels);
340
341 8
        foreach ($list as $listNode) {
342 8
            if (isset($listNode['list'])) {
343 8
                $this->findDeep($listNode['list'], $countLevels + 1);
344 8
            }
345 8
        }
346 8
    }
347
348
    /**
349
     * Create a new row if the table needs it (depending on level, deep).
350
     *
351
     * @param $level
352
     * @param $listNodeIndex
353
     */
354 8
    protected function createNewRowIfNeeded($level, $listNodeIndex)
355
    {
356 8
        $needsRowspan = ($level > 0 && $listNodeIndex > 0) && $this->numCategories <= $this->deep;
357 8
        $needsNewRow = $needsRowspan || $level === 0 && $listNodeIndex === 0;
358
359 8
        if ($needsNewRow) {
360 8
            $this->table->addBodyRow();
0 ignored issues
show
Unused Code introduced by
The call to the method Trefoil\Helpers\Table::addBodyRow() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
361 8
        }
362
363
        // add empty cells as rowspanned (=> with quote only)
364 8
        if ($needsRowspan) {
365 6
            for ($i = 0; $i < $level; $i++) {
366 6
                $this->table->addBodyCell("'");
367 6
            }
368 6
        }
369 8
    }
370
371
    /**
372
     * @param $cellContents
373
     * @param $node
374
     *
375
     * @return array
376
     */
377 8
    protected function createCell($cellContents, $node)
378
    {
379 8
        $where = $this->table->addBodyCell($cellContents);
380
381
        // save candidate heading in extra
382 8
        if (!$this->table->getBodyCellExtra($where['row'], $where['column'])) {
383 8
            $this->table->setBodyCellExtra($node['heading'], (int)$where['row'], (int)$where['column']);
0 ignored issues
show
Unused Code introduced by
The call to the method Trefoil\Helpers\Table::setBodyCellExtra() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
384
385 8
            return $where;
386
        }
387
388
        return $where;
389
    }
390
391
}