• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eliashaeussler / typo3-badges / 23913678716

02 Apr 2026 05:38PM UTC coverage: 95.333% (-0.8%) from 96.154%
23913678716

push

github

web-flow
Merge pull request #1225 from eliashaeussler/feature/health-check

99 of 108 new or added lines in 5 files covered. (91.67%)

572 of 600 relevant lines covered (95.33%)

6.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.38
/src/Service/ApiService.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Symfony project "eliashaeussler/typo3-badges".
7
 *
8
 * Copyright (C) 2021-2026 Elias Häußler <elias@haeussler.dev>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace App\Service;
25

26
use App\Entity\Dto\ExtensionMetadata;
27
use DateTime;
28
use Symfony\Component\Cache\Adapter\NullAdapter;
29
use Symfony\Contracts\Cache\CacheInterface;
30
use Symfony\Contracts\Cache\ItemInterface;
31
use Symfony\Contracts\HttpClient\HttpClientInterface;
32
use Symfony\Contracts\HttpClient\ResponseInterface;
33

34
/**
35
 * ApiService.
36
 *
37
 * @author Elias Häußler <elias@haeussler.dev>
38
 * @license GPL-3.0-or-later
39
 */
40
final readonly class ApiService
41
{
42
    private const string FALLBACK_EXTENSION_KEY = 'handlebars';
43

44
    public function __construct(
35✔
45
        private HttpClientInterface $client,
46
        private CacheInterface $cache,
47
        private int $cacheExpirationPeriod = 3600,
48
    ) {}
35✔
49

50
    public function getExtensionMetadata(string $extensionKey, bool $disableCaching = false): ExtensionMetadata
24✔
51
    {
52
        $apiPath = $this->buildApiPath('/extension/{extension}', ['extension' => $extensionKey]);
24✔
53

54
        if ($disableCaching) {
24✔
55
            $cache = new NullAdapter();
1✔
56
        } else {
57
            $cache = $this->cache;
23✔
58
        }
59

60
        // Fetch extension metadata from cache or external API
61
        $extensionMetadata = $cache->get(
24✔
62
            $this->calculateCacheIdentifier('typo3_api.extension_metadata', ['apiPath' => $apiPath]),
24✔
63
            fn (ItemInterface $item) => $this->sendRequestAndCacheResponse($apiPath, $item),
24✔
64
            null,
24✔
65
            $cacheMetadata,
24✔
66
        );
24✔
67

68
        return new ExtensionMetadata(
24✔
69
            $extensionMetadata,
24✔
70
            $this->determineCacheExpiryDateFromCacheMetadata($cacheMetadata),
24✔
71
        );
24✔
72
    }
73

74
    public function getRandomExtensionMetadata(): ExtensionMetadata
4✔
75
    {
76
        $apiPath = $this->buildApiPath('/extension');
4✔
77

78
        // Fetch current extensions from cache or external API
79
        $result = $this->cache->get(
4✔
80
            $this->calculateCacheIdentifier('typo3_api.random_extensions', ['apiPath' => $apiPath]),
4✔
81
            function (ItemInterface $item) use ($apiPath): array {
4✔
82
                // Build random filter options
83
                $filterOptions = [
3✔
84
                    'page' => random_int(1, 10),
3✔
85
                    'per_page' => 20,
3✔
86
                    'filter' => [
3✔
87
                        'typo3_version' => random_int(9, 13),
3✔
88
                    ],
3✔
89
                ];
3✔
90
                $apiUrl = $apiPath.'?'.http_build_query($filterOptions);
3✔
91

92
                return $this->sendRequestAndCacheResponse($apiUrl, $item, 60 * 60 * 24);
3✔
93
            },
4✔
94
            null,
4✔
95
            $cacheMetadata,
4✔
96
        );
4✔
97

98
        $extensions = $result['extensions'] ?? [];
4✔
99

100
        if ([] === $extensions) {
4✔
101
            return new ExtensionMetadata(['key' => self::FALLBACK_EXTENSION_KEY]);
1✔
102
        }
103

104
        return new ExtensionMetadata(
3✔
105
            ['key' => $extensions[array_rand($extensions)]['key']],
3✔
106
            $this->determineCacheExpiryDateFromCacheMetadata($cacheMetadata),
3✔
107
        );
3✔
108
    }
109

NEW
110
    public function sendPing(): ResponseInterface
×
111
    {
NEW
112
        return $this->client->request('GET', $this->buildApiPath('/ping'));
×
113
    }
114

115
    /**
116
     * @return array<int|string, mixed>
117
     */
118
    private function sendRequestAndCacheResponse(string $path, ItemInterface $item, ?int $expiresAfter = null): array
26✔
119
    {
120
        $response = $this->client->request('GET', $path);
26✔
121
        $responseArray = $response->toArray();
26✔
122

123
        $item->expiresAfter($expiresAfter ?? $this->cacheExpirationPeriod);
26✔
124
        $item->set($responseArray);
26✔
125

126
        return $responseArray;
26✔
127
    }
128

129
    /**
130
     * @param array<string, mixed> $parameters
131
     */
132
    private function buildApiPath(string $endpoint, array $parameters = []): string
28✔
133
    {
134
        $replacePairs = array_combine(
28✔
135
            array_map(fn (string $parameter): string => '{'.trim($parameter, '{}').'}', array_keys($parameters)),
28✔
136
            array_values($parameters),
28✔
137
        );
28✔
138

139
        return '/api/v1/'.ltrim(strtr($endpoint, $replacePairs), '/');
28✔
140
    }
141

142
    /**
143
     * @param array<string, string> $options
144
     */
145
    private function calculateCacheIdentifier(string $key, array $options = []): string
28✔
146
    {
147
        return hash('sha512', $key.'_'.json_encode($options, JSON_THROW_ON_ERROR));
28✔
148
    }
149

150
    /**
151
     * @param array{expiry?: int|numeric-string}|null $cacheMetadata
152
     */
153
    private function determineCacheExpiryDateFromCacheMetadata(?array $cacheMetadata): ?DateTime
27✔
154
    {
155
        if (!isset($cacheMetadata[ItemInterface::METADATA_EXPIRY])) {
27✔
156
            return null;
3✔
157
        }
158

159
        $expiryDate = DateTime::createFromFormat('U', (string) (int) $cacheMetadata[ItemInterface::METADATA_EXPIRY]);
24✔
160

161
        if (false === $expiryDate) {
24✔
162
            return null;
×
163
        }
164

165
        return $expiryDate;
24✔
166
    }
167
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc