Passed
Push — master ( 9fa1a7...f53c15 )
by MusikAnimal
03:46
created

AdminStats   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 296
Duplicated Lines 2.03 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 98.18%

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 2
dl 6
loc 296
ccs 54
cts 55
cp 0.9818
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B getAdminsAndGroups() 0 55 7
A numDays() 0 4 1
B prepareStats() 6 26 4
A getStats() 0 7 2
B groupAdminStatsByUsername() 0 32 4
A fillInactiveAdmins() 0 21 2
A getStart() 0 4 1
A getEnd() 0 4 1
A numAdmins() 0 4 1
A numUsers() 0 4 1
A getNumAdminsWithActions() 0 4 1
A getNumAdminsWithoutActions() 0 4 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * This file contains only the AdminStats class.
4
 */
5
6
namespace Xtools;
7
8
use Symfony\Component\DependencyInjection\Container;
9
use DateTime;
10
11
/**
12
 * AdminStats returns information about users with administrative
13
 * rights on a given wiki.
14
 */
15
class AdminStats extends Model
16
{
17
    /** @var Project Project associated with this AdminStats instance. */
18
    protected $project;
19
20
    /** @var string[] Keyed by user name, values are arrays containing actions and counts. */
21
    protected $adminStats;
22
23
    /**
24
     * Keys are user names, values are their abbreviated user groups.
25
     * If abbreviations are turned on, this will instead be a string of the abbreviated
26
     * user groups, separated by slashes.
27
     * @var string[]|string
28
     */
29
    protected $adminsAndGroups;
30
31
    /** @var int Number of admins (sysops) who haven't made any actions within the time period. */
32
    protected $adminsWithoutActions = 0;
33
34
    /** @var int Number of actual admins (sysops), not users with admin-like actions. */
35
    protected $numSysops = 0;
36
37
    /** @var int Start of time period as UTC timestamp */
38
    protected $start;
39
40
    /** @var int End of time period as UTC timestamp */
41
    protected $end;
42
43
    /**
44
     * TopEdits constructor.
45
     * @param Project $project
46
     * @param int $start as UTC timestamp.
47
     * @param int $end as UTC timestamp.
48
     */
49 3
    public function __construct(Project $project, $start = null, $end = null)
50
    {
51 3
        $this->project = $project;
52 3
        $this->start = $start;
53 3
        $this->end = $end;
54 3
    }
55
56
    /**
57
     * Get users of the project that are capable of making 'admin actions',
58
     * keyed by user name with abbreviations for the user groups as the values.
59
     * @param  string $abbreviate If set, the keys of the result with be a string containing
60
     *   abbreviated versions of their user groups, such as 'A' instead of administrator,
61
     *   'CU' instead of CheckUser, etc. If $abbreviate is false, the keys of the result
62
     *   will be an array of the full-named user groups.
63
     * @see Project::getAdmins()
64
     * @return string[]
65
     */
66 3
    public function getAdminsAndGroups($abbreviate = true)
67
    {
68 3
        if ($this->adminsAndGroups) {
69
            return $this->adminsAndGroups;
70
        }
71
72
        /**
73
         * Each user group that is considered capable of making 'admin actions'.
74
         * @var string[]
75
         */
76 3
        $adminGroups = $this->getRepository()->getAdminGroups($this->project);
77
78
        /** @var array Keys are the usernames, values are thier user groups. */
79 3
        $admins = $this->project->getUsersInGroups($adminGroups);
80
81 3
        if ($abbreviate === false) {
82 1
            return $admins;
83
        }
84
85
        /**
86
         * Keys are the database-stored names, values are the abbreviations.
87
         * FIXME: i18n this somehow.
88
         * @var string[]
89
         */
90
        $userGroupAbbrMap = [
91 3
            'sysop' => 'A',
92
            'bureaucrat' => 'B',
93
            'steward' => 'S',
94
            'checkuser' => 'CU',
95
            'oversight' => 'OS',
96
            'bot' => 'Bot',
97
        ];
98
99 3
        foreach ($admins as $admin => $groups) {
100 3
            $abbrGroups = [];
101
102
            // Keep track of actual number of sysops.
103 3
            if (in_array('sysop', $groups)) {
104 3
                $this->numSysops++;
105
            }
106
107 3
            foreach ($groups as $group) {
0 ignored issues
show
Bug introduced by
The expression $groups of type string is not traversable.
Loading history...
108 3
                if (isset($userGroupAbbrMap[$group])) {
109 3
                    $abbrGroups[] = $userGroupAbbrMap[$group];
110
                }
111
            }
112
113
            // Make 'A' (admin) come before 'CU' (CheckUser), etc.
114 3
            sort($abbrGroups);
115
116 3
            $this->adminsAndGroups[$admin] = implode('/', $abbrGroups);
117
        }
118
119 3
        return $this->adminsAndGroups;
120
    }
121
122
    /**
123
     * The number of days we're spanning between the start and end date.
124
     * @return int
125
     */
126 1
    public function numDays()
127
    {
128 1
        return ($this->end - $this->start) / 60 / 60 / 24;
129
    }
130
131
    /**
132
     * Get the array of statistics for each qualifying user. This may be called
133
     * ahead of self::getStats() so certain class-level properties will be supplied
134
     * (such as self::numUsers(), which is called in the view before iterating
135
     * over the master array of statistics).
136
     * @param boolean $abbreviateGroups If set, the 'groups' list will be
137
     *   a string with abbreivated user groups names, as opposed to an array
138
     *   of full-named user groups.
139
     * @return string[]
140
     */
141 2
    public function prepareStats($abbreviateGroups = true)
142
    {
143 2
        if (isset($this->adminStats)) {
144 1
            return $this->adminStats;
145
        }
146
147
        // UTC to YYYYMMDDHHMMSS.
148 2
        $startDb = date('Ymd000000', $this->start);
149 2
        $endDb = date('Ymd235959', $this->end);
150
151 2
        $stats = $this->getRepository()->getStats($this->project, $startDb, $endDb);
152
153
        // Group by username.
154 2
        $stats = $this->groupAdminStatsByUsername($stats, $abbreviateGroups);
155
156
        // Resort, as for some reason the SQL isn't doing this properly.
157 2 View Code Duplication
        uasort($stats, function ($a, $b) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
158 2
            if ($a['total'] === $b['total']) {
159 2
                return 0;
160
            }
161 2
            return ($a['total'] < $b['total']) ? 1 : -1;
162 2
        });
163
164 2
        $this->adminStats = $stats;
165 2
        return $this->adminStats;
166
    }
167
168
    /**
169
     * Get the master array of statistics for each qualifying user.
170
     * @param boolean $abbreviateGroups If set, the 'groups' list will be
171
     *   a string with abbreivated user groups names, as opposed to an array
172
     *   of full-named user groups.
173
     * @return string[]
174
     */
175 1
    public function getStats($abbreviateGroups = true)
176
    {
177 1
        if (isset($this->adminStats)) {
178 1
            $this->adminStats = $this->prepareStats($abbreviateGroups);
179
        }
180 1
        return $this->adminStats;
181
    }
182
183
    /**
184
     * Given the data returned by AdminStatsRepository::getStats,
185
     * return the stats keyed by user name, adding in a key/value for user groups.
186
     * @param  string[] $data As retrieved by AdminStatsRepository::getStats
187
     * @param boolean $abbreviateGroups If set, the 'groups' list will be
188
     *   a string with abbreivated user groups names, as opposed to an array
189
     *   of full-named user groups.
190
     * @return string[] Stats keyed by user name.
191
     * Functionality covered in test for self::getStats().
192
     * @codeCoverageIgnore
193
     */
194
    private function groupAdminStatsByUsername($data, $abbreviateGroups = true)
195
    {
196
        $adminsAndGroups = $this->getAdminsAndGroups($abbreviateGroups);
197
        $users = [];
198
199
        foreach ($data as $datum) {
200
            $username = $datum['user_name'];
201
202
            // Push to array containing all users with admin actions.
203
            // We also want numerical values to be integers.
204
            $users[$username] = array_map('intval', $datum);
205
206
            // Push back username which was casted to an integer.
207
            $users[$username]['user_name'] = $username;
208
209
            // Set the 'groups' property with the user groups they belong to (if any),
210
            // going off of self::getAdminsAndGroups().
211
            if (isset($adminsAndGroups[$username])) {
212
                $users[$username]['groups'] = $adminsAndGroups[$username];
213
214
                // Remove from actual admin list so later we can re-populate with zeros.
215
                unset($adminsAndGroups[$username]);
216
            } else {
217
                $users[$username]['groups'] = $abbreviateGroups ? '' : [];
218
            }
219
        }
220
221
        // Push any inactive admins back to $users with zero values.
222
        $users = $this->fillInactiveAdmins($users, $adminsAndGroups);
0 ignored issues
show
Bug introduced by
It seems like $adminsAndGroups defined by $this->getAdminsAndGroups($abbreviateGroups) on line 196 can also be of type string; however, Xtools\AdminStats::fillInactiveAdmins() does only seem to accept array<integer,string>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
223
224
        return $users;
225
    }
226
227
    /**
228
     * Fill in inactive admins (no actions within time period), given the list of
229
     * remaining $adminsAndGroups from processing within self::groupAdminStatsByUsername().
230
     * @param  string[] $users As computed by self::groupAdminStatsByUsername().
231
     * @param  string[] $adminsAndGroups Remaining from self::getAdminsAndGroups().
232
     * @return string[]
233
     * @codeCoverageIgnore
234
     */
235
    private function fillInactiveAdmins($users, $adminsAndGroups)
236
    {
237
        foreach ($adminsAndGroups as $username => $groups) {
238
            $users[$username] = [
239
                'user_name' => $username,
240
                'delete' => 0,
241
                'restore' => 0,
242
                'block' => 0,
243
                'unblock' => 0,
244
                'protect' => 0,
245
                'unprotect' => 0,
246
                'rights' => 0,
247
                'import' => 0,
248
                'total' => 0,
249
                'groups' => $groups,
250
            ];
251
            $this->adminsWithoutActions++;
252
        }
253
254
        return $users;
255
    }
256
257
    /**
258
     * Get the formatted start date.
259
     * @return string
260
     */
261 1
    public function getStart()
262
    {
263 1
        return date('Y-m-d', $this->start);
264
    }
265
266
    /**
267
     * Get the formatted end date.
268
     * @return string
269
     */
270 1
    public function getEnd()
271
    {
272 1
        return date('Y-m-d', $this->end);
273
    }
274
275
    /**
276
     * Get the total number of admins (users currently with qualifying permissions).
277
     * @return int
278
     */
279 1
    public function numAdmins()
280
    {
281 1
        return $this->numSysops;
282
    }
283
284
    /**
285
     * Get the total number of users we're reporting as having made admin actions.
286
     * @return int
287
     */
288 1
    public function numUsers()
289
    {
290 1
        return count($this->adminStats);
291
    }
292
293
    /**
294
     * Number of admins who did make actions within the time period.
295
     * @return int
296
     */
297 1
    public function getNumAdminsWithActions()
298
    {
299 1
        return $this->numAdmins() - $this->adminsWithoutActions;
300
    }
301
302
    /**
303
     * Number of admins who did not make any actions within the time period.
304
     * @return int
305
     */
306 1
    public function getNumAdminsWithoutActions()
307
    {
308 1
        return $this->adminsWithoutActions;
309
    }
310
}
311