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