Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Index
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
4 / 4
16
100.00% covered (success)
100.00%
1 / 1
 index
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 cards
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
5
 recent
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
6
 typeLabels
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Controller\Panel;
6
7use Celema\Core\Factory\Factory;
8use Celema\Core\Response;
9use Celema\Quma\Database;
10use Cosray\Bootstrap;
11use Cosray\Context;
12use Cosray\Contract\DashboardCard;
13use Cosray\Node\Types;
14use Cosray\Panel\Dashboard;
15use Cosray\Panel\NodeUrls;
16use Cosray\Title\Resolver as TitleResolver;
17use DateTimeImmutable;
18use IntlDateFormatter;
19use NumberFormatter;
20
21final class Index extends Panel
22{
23    private const int RECENT_LIMIT = 6;
24
25    protected const string AREA = 'dashboard';
26
27    /** @return array<string, mixed> */
28    public function index(
29        Context $context,
30        Database $db,
31        Dashboard $dashboard,
32        Factory $factory,
33        Types $types,
34    ): array|Response {
35        if (!$this->config->panel->dashboard) {
36            return Response::create($factory)->redirect($this->homeUrl(), 303);
37        }
38
39        return $this->context([
40            'cards' => $this->cards($context, $dashboard),
41            'recent' => $this->recent($context, $db, $types),
42        ]);
43    }
44
45    /** @return list<array{label: string, value: string, note: ?string, url: ?string}> */
46    private function cards(Context $context, Dashboard $dashboard): array
47    {
48        $cards = [];
49        $formatter = new NumberFormatter($this->localeId(), NumberFormatter::DECIMAL);
50        $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 0);
51        $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
52
53        foreach ($dashboard->cards() as $definition) {
54            $provider = is_string($definition) ? $context->create($definition) : $definition;
55            assert($provider instanceof DashboardCard, 'The dashboard registry must contain card providers');
56            $card = $provider->card();
57
58            if ($card === null) {
59                continue;
60            }
61
62            $value = is_int($card->value) ? (string) $formatter->format($card->value) : $card->value;
63            $cards[] = [
64                'label' => $card->label,
65                'value' => $value,
66                'note' => $card->note,
67                'url' => $card->url,
68            ];
69        }
70
71        return $cards;
72    }
73
74    /**
75     * @return list<array{
76     *     uid: string,
77     *     url: string,
78     *     title: string,
79     *     type: string,
80     *     changed: string,
81     *     datetime: string,
82     *     published: bool,
83     *     status: string,
84     * }>
85     */
86    private function recent(Context $context, Database $db, Types $types): array
87    {
88        $labels = $this->typeLabels($types);
89        $links = new NodeUrls($this->panelPath(), 'dashboard');
90        $titles = new TitleResolver($types);
91        $date = new IntlDateFormatter(
92            $this->localeId(),
93            IntlDateFormatter::MEDIUM,
94            IntlDateFormatter::NONE,
95            $this->config->app->timezone->getName(),
96        );
97        $recent = [];
98
99        foreach ($db->dashboard->recent(['limit' => self::RECENT_LIMIT])->all() as $row) {
100            $uid = (string) ($row['uid'] ?? '');
101            $map = $row['title'] ?? [];
102
103            if (is_string($map)) {
104                $map = json_decode($map, true);
105            }
106
107            $title = is_array($map) ? $titles->stored($map, $context->locale()) : null;
108            $changed = new DateTimeImmutable((string) ($row['changed'] ?? 'now'));
109            $formatted = $date->format($changed->getTimestamp());
110            $published = (bool) ($row['published'] ?? false);
111            $type = (string) ($row['type'] ?? '');
112            $recent[] = [
113                'uid' => $uid,
114                'url' => $links->edit($uid),
115                'title' => $title ?? $uid,
116                'type' => $labels[$type] ?? $type,
117                'changed' => $formatted === false ? '' : $formatted,
118                'datetime' => $changed->format(DATE_ATOM),
119                'published' => $published,
120                'hasDraft' => (bool) ($row['has_draft'] ?? false),
121                'status' => $published ? __('status:published') : __('status:unpublished'),
122            ];
123        }
124
125        return $recent;
126    }
127
128    /** @return array<string, string> */
129    private function typeLabels(Types $types): array
130    {
131        $registered = $this->container->tag(Bootstrap::NODE_TAG);
132        $labels = [];
133
134        foreach ($registered->entries() as $handle) {
135            $class = $registered->entry($handle)->definition();
136            assert(is_string($class) && class_exists($class), 'Registered node types must be existing classes');
137            $labels[$handle] = __((string) $types->get($class, 'label', $handle));
138        }
139
140        return $labels;
141    }
142}