Completed
Branch master (e8947e)
by Andreas
15:09
created

net_nehmer_blog_handler_archive   C

Complexity

Total Complexity 47

Size/Duplication

Total Lines 496
Duplicated Lines 4.23 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
dl 21
loc 496
rs 6.0316
c 0
b 0
f 0
wmc 47
lcom 1
cbo 17

12 Methods

Rating   Name   Duplication   Size   Complexity  
A _on_initialize() 0 4 1
A _prepare_request_data() 0 6 1
A _handler_welcome() 0 14 2
B _compute_welcome_first_post() 0 26 3
A _compute_welcome_posting_count() 0 11 1
B _compute_welcome_data() 0 80 6
A _get_month_names() 0 10 2
A _show_welcome() 0 15 2
C _handler_list() 0 75 10
A _set_startend_from_year() 5 18 4
C _set_startend_from_month() 5 35 8
C _show_list() 11 51 7

How to fix   Duplicated Code    Complexity   

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:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

1
<?php
2
/**
3
 * @package net.nehmer.blog
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
/**
10
 * Blog Archive pages handler
11
 *
12
 * Shows the various archive views.
13
 *
14
 * @package net.nehmer.blog
15
 */
16
class net_nehmer_blog_handler_archive extends midcom_baseclasses_components_handler
1 ignored issue
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
17
{
18
    /**
19
     * The content topic to use
20
     *
21
     * @var midcom_db_topic
22
     */
23
    private $_content_topic = null;
24
25
    /**
26
     * The articles to display
27
     *
28
     * @var array
29
     */
30
    private $_articles = null;
31
32
    /**
33
     * The datamanager for the currently displayed article.
34
     *
35
     * @var midcom_helper_datamanager2_datamanager
36
     */
37
    private $_datamanager = null;
38
39
    /**
40
     * The start date of the Archive listing.
41
     *
42
     * @var DateTime
43
     */
44
    private $_start = null;
45
46
    /**
47
     * The end date of the Archive listing.
48
     *
49
     * @var DateTime
50
     */
51
    private $_end = null;
52
53
    /**
54
     * Maps the content topic from the request data to local member variables.
55
     */
56
    public function _on_initialize()
57
    {
58
        $this->_content_topic = $this->_request_data['content_topic'];
59
    }
60
61
    /**
62
     * Simple helper which references all important members to the request data listing
63
     * for usage within the style listing.
64
     */
65
    private function _prepare_request_data()
66
    {
67
        $this->_request_data['datamanager'] = $this->_datamanager;
68
        $this->_request_data['start'] = $this->_start;
69
        $this->_request_data['end'] = $this->_end;
70
    }
71
72
    /**
73
     * Shows the archive welcome page: A listing of years/months along with total post counts
74
     * and similar stuff.
75
     *
76
     * The handler computes all necessary data and populates the request array accordingly.
77
     *
78
     * @param mixed $handler_id The ID of the handler.
79
     * @param array $args The argument list.
80
     * @param array &$data The local request data.
81
     */
82
    public function _handler_welcome ($handler_id, array $args, array &$data)
83
    {
84
        $this->_compute_welcome_data();
85
        $this->_prepare_request_data();
86
87
        if ($this->_config->get('archive_in_navigation'))
88
        {
89
            $this->set_active_leaf($this->_topic->id . '_ARCHIVE');
90
        }
91
92
        midcom::get()->head->set_pagetitle("{$this->_topic->extra}: " . $this->_l10n->get('archive'));
93
94
        midcom::get()->metadata->set_request_metadata(net_nehmer_blog_viewer::get_last_modified($this->_topic, $this->_content_topic), $this->_topic->guid);
95
    }
96
97
    /**
98
     * Loads the first posting time from the DB. This is the base for all operations on the
99
     * resultset.
100
     *
101
     * This is done under sudo if possible, to avoid problems arising if the first posting
102
     * is hidden. This keeps up performance, as an execute_unchecked() can be made in this case.
103
     * If sudo cannot be acquired, the system falls back to excute().
104
     *
105
     * @return DateTime The time of the first posting or null on failure.
106
     */
107
    private function _compute_welcome_first_post()
108
    {
109
        $qb = midcom_db_article::new_query_builder();
110
        $data =& $this->_request_data;
111
        net_nehmer_blog_viewer::article_qb_constraints($qb, $data, 'archive_welcome');
112
        $qb->add_constraint('metadata.published', '>', '1970-01-02 23:59:59');
113
114
        $qb->add_order('metadata.published');
115
        $qb->set_limit(1);
116
117
        if (midcom::get()->auth->request_sudo($this->_component))
118
        {
119
            $result = $qb->execute_unchecked();
120
            midcom::get()->auth->drop_sudo();
121
        }
122
        else
123
        {
124
            $result = $qb->execute();
125
        }
126
127
        if (!empty($result))
128
        {
129
            return new DateTime(strftime('%Y-%m-%d %H:%M:%S', $result[0]->metadata->published));
130
        }
131
        return null;
132
    }
133
134
    /**
135
     * Computes the number of postings in a given timeframe.
136
     *
137
     * @param DateTime $start Start of the timeframe (inclusive)
138
     * @param DateTime $end End of the timeframe (exclusive)
139
     * @return int Posting count
140
     */
141
    private function _compute_welcome_posting_count($start, $end)
142
    {
143
        $data =& $this->_request_data;
144
        $qb = midcom_db_article::new_query_builder();
145
146
        $qb->add_constraint('metadata.published', '>=', $start->format('Y-m-d H:i:s'));
147
        $qb->add_constraint('metadata.published', '<', $end->format('Y-m-d H:i:s'));
148
        net_nehmer_blog_viewer::article_qb_constraints($qb, $data, 'archive_welcome');
149
150
        return $qb->count();
151
    }
152
153
    /**
154
     * Computes the data nececssary for the welcome screen. Automatically put into the request
155
     * data array.
156
     */
157
    private function _compute_welcome_data()
158
    {
159
        // Helpers
160
        $prefix = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_ANCHORPREFIX) . 'archive/';
161
162
        // First step of request data: Overall info
163
        $total_count = 0;
164
        $year_data = Array();
165
        $first_post = $this->_compute_welcome_first_post();
166
        $this->_request_data['first_post'] = $first_post;
167
        $this->_request_data['total_count'] =& $total_count;
168
        $this->_request_data['year_data'] =& $year_data;
169
        if (! $first_post)
170
        {
171
            return;
172
        }
173
174
        // Second step of request data: Years and months.
175
        $now = new DateTime();
176
        $first_year = $first_post->format('Y');
177
        $last_year = $now->format('Y');
178
179
        $month_names = $this->_get_month_names();
180
181
        for ($year = $last_year; $year >= $first_year; $year--)
182
        {
183
            $year_url = "{$prefix}year/{$year}/";
184
            $year_count = 0;
185
            $month_data = Array();
186
187
            // Loop over the months, start month is either first posting month
188
            // or January in all other cases. End months are treated similarly,
189
            // being december by default unless for the current year.
190
            if ($year == $first_year)
191
            {
192
                $first_month = $first_post->format('n');
193
            }
194
            else
195
            {
196
                $first_month = 1;
197
            }
198
199
            if ($year == $last_year)
200
            {
201
                $last_month = $now->format('n');
202
            }
203
            else
204
            {
205
                $last_month = 12;
206
            }
207
208
            for ($month = $first_month; $month <= $last_month; $month++)
209
            {
210
                $start_time = new DateTime();
211
                $start_time->setDate($year, $month, 1);
212
                $end_time = clone $start_time;
213
                $end_time->modify('+1 month');
214
215
                $month_url = "{$prefix}month/{$year}/{$month}/";
216
                $month_count = $this->_compute_welcome_posting_count($start_time, $end_time);
217
                $year_count += $month_count;
218
                $total_count += $month_count;
219
                $month_data[$month] = Array
220
                (
221
                    'month' => $month,
222
                    'name' => $month_names[$month],
223
                    'url' => $month_url,
224
                    'count' => $month_count,
225
                );
226
            }
227
228
            $year_data[$year] = Array
229
            (
230
                'year' => $year,
231
                'url' => $year_url,
232
                'count' => $year_count,
233
                'month_data' => $month_data,
234
            );
235
        }
236
    }
237
238
    private function _get_month_names()
239
    {
240
        $names = array();
241
        for ($i = 1; $i < 13; $i++)
242
        {
243
            $timestamp = mktime(0, 0, 0, $i, 1, 2011);
244
            $names[$i] = strftime('%B', $timestamp);
245
        }
246
        return $names;
247
    }
248
249
    /**
250
     * Displays the welcome page.
251
     *
252
     * Element sequence:
253
     *
254
     * - archive-welcome-start (Start of the archive welcome page)
255
     * - archive-welcome-year (Display of a single year, may not be called when there are no postings)
256
     * - archive-welcome-end (End of the archive welcome page)
257
     *
258
     * Context data for all elements:
259
     *
260
     * - int total_count (total number of postings w/o ACL restrictions)
261
     * - DateTime first_post (the first posting date, may be null)
262
     * - Array year_data (the year data, contains the year context info as outlined below)
263
     *
264
     * Context data for year elements:
265
     *
266
     * - int year (the year displayed)
267
     * - string url (url to display the complete year)
268
     * - int count (Number of postings in that year)
269
     * - array month_data (the monthly data)
270
     *
271
     * month_data will contain an associative array containing the following array of data
272
     * indexed by month number (1-12):
273
     *
274
     * - string 'url' => The URL to the month.
275
     * - string 'name' => The localized name of the month.
276
     * - int 'count' => The number of postings in that month.
277
     *
278
     * @param mixed $handler_id The ID of the handler.
279
     * @param array &$data The local request data.
280
     */
281
    public function _show_welcome($handler_id, array &$data)
282
    {
283
        midcom_show_style('archive-welcome-start');
284
285
        foreach ($data['year_data'] as $year => $year_data)
286
        {
287
            $data['year'] = $year;
288
            $data['url'] = $year_data['url'];
289
            $data['count'] = $year_data['count'];
290
            $data['month_data'] = $year_data['month_data'];
291
            midcom_show_style('archive-welcome-year');
292
        }
293
294
        midcom_show_style('archive-welcome-end');
295
    }
296
297
    /**
298
     * Shows the archive. Depending on the selected handler various constraints are added to
299
     * the QB. See the add_*_constraint methods for details.
300
     *
301
     * @param mixed $handler_id The ID of the handler.
302
     * @param array $args The argument list.
303
     * @param array &$data The local request data.
304
     */
305
    public function _handler_list ($handler_id, array $args, array &$data)
306
    {
307
        // Get Articles, distinguish by handler.
308
        $qb = midcom_db_article::new_query_builder();
309
        net_nehmer_blog_viewer::article_qb_constraints($qb, $data, $handler_id);
310
311
        // Use helper functions to determine start/end
312
        switch ($handler_id)
313
        {
314
            case 'archive-year-category':
315
                $data['category'] = trim(strip_tags($args[1]));
316
                if (   isset($data['schemadb']['default']->fields['categories'])
317
                    && array_key_exists('allow_multiple', $data['schemadb']['default']->fields['categories']['type_config'])
318
                    && !$data['schemadb']['default']->fields['categories']['type_config']['allow_multiple'])
319
                {
320
                    $qb->add_constraint('extra1', '=', (string) $data['category']);
321
                }
322
                else
323
                {
324
                    $qb->add_constraint('extra1', 'LIKE', "%|{$this->_request_data['category']}|%");
325
                }
326
                //Fall-through
327
328
            case 'archive-year':
329
                if (!$this->_config->get('archive_years_enable'))
330
                {
331
                    throw new midcom_error_notfound('Year archive not allowed');
332
                }
333
334
                $this->_set_startend_from_year($args[0]);
335
                break;
336
337
            case 'archive-month':
338
                $this->_set_startend_from_month($args[0], $args[1]);
339
                break;
340
341
            default:
342
                throw new midcom_error("The request handler {$handler_id} is not supported.");
343
        }
344
345
        $qb->add_constraint('metadata.published', '>=', $this->_start->format('Y-m-d H:i:s'));
346
        $qb->add_constraint('metadata.published', '<', $this->_end->format('Y-m-d H:i:s'));
347
        $qb->add_order('metadata.published', $this->_config->get('archive_item_order'));
348
        $this->_articles = $qb->execute();
0 ignored issues
show
Documentation Bug introduced by
It seems like $qb->execute() can also be of type false. However, the property $_articles is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
349
350
        $this->_datamanager = new midcom_helper_datamanager2_datamanager($this->_request_data['schemadb']);
351
352
        // Move end date one day backwards for display purposes.
353
        $now = new DateTime();
354
        if ($now < $this->_end)
355
        {
356
            $this->_end = $now;
357
        }
358
        else
359
        {
360
            $this->_end->modify('-1 day');
361
        }
362
363
        $timeframe = $this->_l10n->get_formatter()->timeframe($this->_start, $this->_end, 'date');
364
        $this->add_breadcrumb("archive/year/{$args[0]}/", $timeframe);
365
366
        $this->_prepare_request_data();
367
368
        if ($this->_config->get('archive_in_navigation'))
369
        {
370
            $this->set_active_leaf($this->_topic->id . '_ARCHIVE');
371
        }
372
        else
373
        {
374
            $this->set_active_leaf($this->_topic->id . '_ARCHIVE_' . $args[0]);
375
        }
376
377
        midcom::get()->metadata->set_request_metadata(net_nehmer_blog_viewer::get_last_modified($this->_topic, $this->_content_topic), $this->_topic->guid);
378
        midcom::get()->head->set_pagetitle("{$this->_topic->extra}: {$timeframe}");
379
    }
380
381
    /**
382
     * Computes the start/end dates to only query a given year. It will do validation
383
     * before processing, throwing 404 in case of incorrectly formatted dates.
384
     *
385
     * This is used by the archive-year handler, which expects the year to be in $args[0].
386
     *
387
     * @param int $year The year to query.
388
     */
389
    private function _set_startend_from_year($year)
390
    {
391 View Code Duplication
        if (   ! is_numeric($year)
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...
392
            || strlen($year) != 4)
393
        {
394
            throw new midcom_error_notfound("The year '{$year}' is not a valid year identifier.");
395
        }
396
397
        $now = new DateTime();
398
        if ($year > (int) $now->format('Y'))
399
        {
400
            throw new midcom_error_notfound("The year '{$year}' is in the future, no archive available.");
401
        }
402
403
        $endyear = $year + 1;
404
        $this->_start = new DateTime("{$year}-01-01 00:00:00");
405
        $this->_end = new DateTime("{$endyear}-01-01 00:00:00");
406
    }
407
408
    /**
409
     * Computes the start/end dates to only query a given month. It will do validation
410
     * before processing, throwing 404 in case of incorrectly formatted dates.
411
     *
412
     * This is used by the archive-month handler, which expects the year to be in $args[0]
413
     * and the month to be in $args[1].
414
     *
415
     * @param int $year The year to query.
416
     * @param int $month The month to query.
417
     */
418
    private function _set_startend_from_month($year, $month)
419
    {
420 View Code Duplication
        if (   ! is_numeric($year)
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...
421
            || strlen($year) != 4)
422
        {
423
            throw new midcom_error_notfound("The year '{$year}' is not a valid year identifier.");
424
        }
425
426
        if (   ! is_numeric($month)
427
            || $month < 1
428
            || $month > 12)
429
        {
430
            throw new midcom_error_notfound("The year {$month} is not a valid year identifier.");
431
        }
432
433
        $now = new DateTime();
434
        $this->_start = new DateTime("{$year}-" . sprintf('%02d', $month) .  "-01 00:00:00");
435
        if ($this->_start > $now)
436
        {
437
            throw new midcom_error_notfound("The month '{$year}-" . sprintf('%02d', $month) .  "' is in the future, no archive available.");
438
        }
439
440
        if ($month == 12)
441
        {
442
            $endyear = $year + 1;
443
            $endmonth = 1;
444
        }
445
        else
446
        {
447
            $endyear = $year;
448
            $endmonth = $month + 1;
449
        }
450
451
        $this->_end = new DateTime("{$endyear}-" . sprintf('%02d', $endmonth) .  "-01 00:00:00");
452
    }
453
454
    /**
455
     * Displays the archive.
456
     *
457
     * @param mixed $handler_id The ID of the handler.
458
     * @param array &$data The local request data.
459
     */
460
    public function _show_list($handler_id, array &$data)
461
    {
462
        // FIXME: For some reason the config topic is lost between _handle and _show phases
463
        $this->_config->store_from_object($this->_topic, $this->_component);
464
465
        midcom_show_style('archive-list-start');
466
        if ($this->_articles)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_articles of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
467
        {
468
            $data['index_fulltext'] = $this->_config->get('index_fulltext');
469
            $data['comments_enable'] = (boolean) $this->_config->get('comments_enable');
470
471
            $total_count = count($this->_articles);
472
            $prefix = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_ANCHORPREFIX);
473
474
            foreach ($this->_articles as $article_counter => $article)
475
            {
476 View Code Duplication
                if (! $this->_datamanager->autoset_storage($article))
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...
477
                {
478
                    debug_add("The datamanager for article {$article->id} could not be initialized, skipping it.");
479
                    debug_print_r('Object was:', $article);
480
                    continue;
481
                }
482
483
                $data['article'] = $article;
484
                $data['article_counter'] = $article_counter;
485
                $data['article_count'] = $total_count;
486
                $data['view_url'] = $prefix . $this->_master->get_url($article);
487
                $data['local_view_url'] = $data['view_url'];
488
                if (   $this->_config->get('link_to_external_url')
489
                    && !empty($article->url))
490
                {
491
                    $data['view_url'] = $article->url;
492
                }
493
494
                $data['linked'] = ($article->topic !== $this->_content_topic->id);
495 View Code Duplication
                if ($data['linked'])
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...
496
                {
497
                    $nap = new midcom_helper_nav();
498
                    $data['node'] = $nap->get_node($article->topic);
499
                }
500
501
                midcom_show_style('archive-list-item');
502
            }
503
        }
504
        else
505
        {
506
            midcom_show_style('archive-list-empty');
507
        }
508
509
        midcom_show_style('archive-list-end');
510
    }
511
}
512