Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
91.46% covered (success)
91.46%
75 / 82
66.67% covered (warning)
66.67%
14 / 21
62.50% covered (warning)
62.50%
10 / 16
45.45% covered (danger)
45.45%
5 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
RebrickableDataLoader
91.46% covered (success)
91.46%
75 / 82
66.67% covered (warning)
66.67%
14 / 21
62.50% covered (warning)
62.50%
10 / 16
45.45% covered (danger)
45.45%
5 / 11
32.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fetchFromExternalApi
84.62% covered (warning)
84.62%
11 / 13
33.33% covered (danger)
33.33%
1 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 fetchSetsFromExternalApi
93.75% covered (success)
93.75%
15 / 16
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 fetchPartsFromExternalApi
92.86% covered (success)
92.86%
13 / 14
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
4.12
 fetchPartElementsFromExternalApi
93.75% covered (success)
93.75%
15 / 16
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 fetchSetElementsFromExternalApi
94.12% covered (success)
94.12%
16 / 17
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 findSets
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findParts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSetParts
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPartElements
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSetElements
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace App\CollectionManagement\Infrastructure\Service;
4
5use App\CollectionManagement\Domain\Model\External\ExternalElement;
6use App\CollectionManagement\Domain\Model\External\ExternalElementCollection;
7use App\CollectionManagement\Domain\Model\External\ExternalPart;
8use App\CollectionManagement\Domain\Model\External\ExternalSet;
9use App\CollectionManagement\Domain\Model\External\ExternalSetElement;
10use App\CollectionManagement\Domain\Model\External\ExternalSetElementCollection;
11use App\CollectionManagement\Domain\Model\PartCollection;
12use App\CollectionManagement\Domain\Model\SetCollection;
13use App\CollectionManagement\Domain\Service\LegoDataLoader;
14use Override;
15use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
16use Symfony\Contracts\HttpClient\HttpClientInterface;
17
18/**
19 * @author Wilhelm Zwertvaegher
20 * Lego data loader ; this will be our only loader at the moment.
21 * This loader uses a cache to avoid external requests when possible
22 *
23 */
24
25#[Autoconfigure]
26class RebrickableDataLoader implements LegoDataLoader
27{
28    private const API_BASE_URL = 'https://rebrickable.com/api/v3/lego/';
29
30    public function __construct(
31        private readonly ExternalDataCacheManager $cacheManager,
32        private readonly HttpClientInterface      $httpClient,
33        private readonly string                   $apiKey
34    ) {
35    }
36
37    /**
38     * @param string $endpointUri
39     * @return array<mixed>|null
40     *
41     * */
42    private function fetchFromExternalApi($endpointUri): ?array
43    {
44        try {
45            $response = $this->httpClient->request(
46                'GET',
47                sprintf('%s%s', self::API_BASE_URL, $endpointUri),
48                array(
49                    'headers' => [
50                        'Authorization' => sprintf('key %s', $this->apiKey),
51                    ]
52                )
53            );
54            // return raw data fetched from external API
55            $data = $response->toArray();
56            return $data['results'] ?? [];
57        }
58        // TODO : properly handle this throwable
59        catch (\Throwable $e) {
60            return null;
61        }
62    }
63
64    /**
65     * @param string $search
66     * @return SetCollection|null
67     */
68    private function fetchSetsFromExternalApi(string $search): ?SetCollection
69    {
70        $results = $this->fetchFromExternalApi(sprintf('sets/?search=%s', $search));
71        if ($results === null) {
72            return null;
73        }
74
75        return new SetCollection(
76            array_map(
77                fn ($item) => new ExternalSet(
78                    $item['set_num'],
79                    preg_replace('/-.*$/', '', $item['set_num']),
80                    $item['name'],
81                    $item['num_parts'],
82                    $item['set_img_url'] ?? '',
83                    $item['year']
84                ),
85                $results
86            )
87        );
88    }
89
90    /**
91     * @param string $search
92     * @return PartCollection|null
93     */
94    private function fetchPartsFromExternalApi(string $search): ?PartCollection
95    {
96        $results = $this->fetchFromExternalApi(sprintf('parts/?search=%s', $search));
97        if ($results === null) {
98            return null;
99        }
100
101        return new PartCollection(
102            array_map(
103                fn ($item) => new ExternalPart(
104                    $item['part_num'],
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
106                    $item['name'],
107                    $item['part_img_url'] ?? ''
108                ),
109                $results
110            )
111        );
112    }
113
114    /**
115     * @param string $partExternalId
116     * @return ExternalElementCollection|null
117     */
118    private function fetchPartElementsFromExternalApi(string $partExternalId): ?ExternalElementCollection
119    {
120        $results = $this->fetchFromExternalApi(sprintf('parts/%s/colors/', $partExternalId));
121        if ($results === null) {
122            return null;
123        }
124
125        return new ExternalElementCollection(
126            array_map(
127                fn ($item) => new ExternalElement(
128                    $item['elements'][0],
129                    $item['elements'][0],
130                    $partExternalId,
131                    $item['part_img_url'] ?? '',
132                    $item['color_id'],
133                    $item['color_name']
134                ),
135                $results
136            )
137        );
138    }
139
140    /**
141     * @param string $setExternalId
142     * @return ExternalSetElementCollection|null
143     */
144    private function fetchSetElementsFromExternalApi(string $setExternalId): ?ExternalSetElementCollection
145    {
146        $results = $this->fetchFromExternalApi(sprintf('sets/%s/parts/?inc_part_details=1', $setExternalId));
147        if ($results === null) {
148            return null;
149        }
150
151        return new ExternalSetElementCollection(
152            array_map(
153                fn ($item) => new ExternalSetElement(
154                    $item['element_id'],
155                    $setExternalId,
156                    $item['part']['part_num'],
157                    $item['quantity']
158                ),
159                array_filter(
160                    $results,
161                    fn ($item) => $item['is_spare'] === false
162                )
163            )
164        );
165    }
166
167    #[Override]
168    public function findSets(string $search): ?SetCollection
169    {
170        return $this->cacheManager->getSets($search, fn ($s) => $this->fetchSetsFromExternalApi($s));
171    }
172
173    #[Override]
174    public function findParts(string $search): ?PartCollection
175    {
176        return $this->cacheManager->getParts($search, fn ($s) => $this->fetchPartsFromExternalApi($s));
177    }
178
179    #[Override]
180    public function getSetParts(string $setExternalId): ?ExternalSetElementCollection
181    {
182        return new ExternalSetElementCollection([]);
183    }
184
185    #[Override]
186    public function getPartElements(string $partExternalId): ?ExternalElementCollection
187    {
188        return $this->cacheManager->getPartElements($partExternalId, fn ($s) => $this->fetchPartElementsFromExternalApi($partExternalId));
189    }
190
191    #[Override]
192    public function getSetElements(string $setExternalId): ?ExternalSetElementCollection
193    {
194        return $this->cacheManager->getSetElements($setExternalId, fn ($s) => $this->fetchSetElementsFromExternalApi($setExternalId));
195    }
196}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

RebrickableDataLoader->__construct
30    public function __construct(
31        private readonly ExternalDataCacheManager $cacheManager,
32        private readonly HttpClientInterface      $httpClient,
33        private readonly string                   $apiKey
34    ) {
35    }
RebrickableDataLoader->fetchFromExternalApi
42    private function fetchFromExternalApi($endpointUri): ?array
43    {
44        try {
45            $response = $this->httpClient->request(
46                'GET',
47                sprintf('%s%s', self::API_BASE_URL, $endpointUri),
48                array(
49                    'headers' => [
50                        'Authorization' => sprintf('key %s', $this->apiKey),
51                    ]
52                )
53            );
54            // return raw data fetched from external API
55            $data = $response->toArray();
56            return $data['results'] ?? [];
59        catch (\Throwable $e) {
 
60            return null;
61        }
62    }
RebrickableDataLoader->fetchPartElementsFromExternalApi
118    private function fetchPartElementsFromExternalApi(string $partExternalId): ?ExternalElementCollection
119    {
120        $results = $this->fetchFromExternalApi(sprintf('parts/%s/colors/', $partExternalId));
121        if ($results === null) {
 
122            return null;
118    private function fetchPartElementsFromExternalApi(string $partExternalId): ?ExternalElementCollection
119    {
120        $results = $this->fetchFromExternalApi(sprintf('parts/%s/colors/', $partExternalId));
121        if ($results === null) {
 
125        return new ExternalElementCollection(
126            array_map(
127                fn ($item) => new ExternalElement(
128                    $item['elements'][0],
129                    $item['elements'][0],
130                    $partExternalId,
131                    $item['part_img_url'] ?? '',
132                    $item['color_id'],
133                    $item['color_name']
134                ),
135                $results
136            )
137        );
138    }
RebrickableDataLoader->fetchPartsFromExternalApi
94    private function fetchPartsFromExternalApi(string $search): ?PartCollection
95    {
96        $results = $this->fetchFromExternalApi(sprintf('parts/?search=%s', $search));
97        if ($results === null) {
 
98            return null;
94    private function fetchPartsFromExternalApi(string $search): ?PartCollection
95    {
96        $results = $this->fetchFromExternalApi(sprintf('parts/?search=%s', $search));
97        if ($results === null) {
 
101        return new PartCollection(
102            array_map(
103                fn ($item) => new ExternalPart(
104                    $item['part_num'],
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
106                    $item['name'],
107                    $item['part_img_url'] ?? ''
108                ),
109                $results
110            )
111        );
112    }
RebrickableDataLoader->fetchSetElementsFromExternalApi
144    private function fetchSetElementsFromExternalApi(string $setExternalId): ?ExternalSetElementCollection
145    {
146        $results = $this->fetchFromExternalApi(sprintf('sets/%s/parts/?inc_part_details=1', $setExternalId));
147        if ($results === null) {
 
148            return null;
144    private function fetchSetElementsFromExternalApi(string $setExternalId): ?ExternalSetElementCollection
145    {
146        $results = $this->fetchFromExternalApi(sprintf('sets/%s/parts/?inc_part_details=1', $setExternalId));
147        if ($results === null) {
 
151        return new ExternalSetElementCollection(
152            array_map(
153                fn ($item) => new ExternalSetElement(
154                    $item['element_id'],
155                    $setExternalId,
156                    $item['part']['part_num'],
157                    $item['quantity']
158                ),
159                array_filter(
160                    $results,
161                    fn ($item) => $item['is_spare'] === false
162                )
163            )
164        );
165    }
RebrickableDataLoader->fetchSetsFromExternalApi
68    private function fetchSetsFromExternalApi(string $search): ?SetCollection
69    {
70        $results = $this->fetchFromExternalApi(sprintf('sets/?search=%s', $search));
71        if ($results === null) {
 
72            return null;
68    private function fetchSetsFromExternalApi(string $search): ?SetCollection
69    {
70        $results = $this->fetchFromExternalApi(sprintf('sets/?search=%s', $search));
71        if ($results === null) {
 
75        return new SetCollection(
76            array_map(
77                fn ($item) => new ExternalSet(
78                    $item['set_num'],
79                    preg_replace('/-.*$/', '', $item['set_num']),
80                    $item['name'],
81                    $item['num_parts'],
82                    $item['set_img_url'] ?? '',
83                    $item['year']
84                ),
85                $results
86            )
87        );
88    }
RebrickableDataLoader->findParts
174    public function findParts(string $search): ?PartCollection
175    {
176        return $this->cacheManager->getParts($search, fn ($s) => $this->fetchPartsFromExternalApi($s));
177    }
RebrickableDataLoader->findSets
168    public function findSets(string $search): ?SetCollection
169    {
170        return $this->cacheManager->getSets($search, fn ($s) => $this->fetchSetsFromExternalApi($s));
171    }
RebrickableDataLoader->getPartElements
186    public function getPartElements(string $partExternalId): ?ExternalElementCollection
187    {
188        return $this->cacheManager->getPartElements($partExternalId, fn ($s) => $this->fetchPartElementsFromExternalApi($partExternalId));
189    }
RebrickableDataLoader->getSetElements
192    public function getSetElements(string $setExternalId): ?ExternalSetElementCollection
193    {
194        return $this->cacheManager->getSetElements($setExternalId, fn ($s) => $this->fetchSetElementsFromExternalApi($setExternalId));
195    }
RebrickableDataLoader->getSetParts
180    public function getSetParts(string $setExternalId): ?ExternalSetElementCollection
181    {
182        return new ExternalSetElementCollection([]);
183    }
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:103-108}
103                fn ($item) => new ExternalPart(
104                    $item['part_num'],
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
 
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
 
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
106                    $item['name'],
107                    $item['part_img_url'] ?? ''
108                ),
103                fn ($item) => new ExternalPart(
104                    $item['part_num'],
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
 
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
 
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
106                    $item['name'],
107                    $item['part_img_url'] ?? ''
108                ),
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:127-134}
127                fn ($item) => new ExternalElement(
128                    $item['elements'][0],
129                    $item['elements'][0],
130                    $partExternalId,
131                    $item['part_img_url'] ?? '',
132                    $item['color_id'],
133                    $item['color_name']
134                ),
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:153-158}
153                fn ($item) => new ExternalSetElement(
154                    $item['element_id'],
155                    $setExternalId,
156                    $item['part']['part_num'],
157                    $item['quantity']
158                ),
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:161-162}
161                    fn ($item) => $item['is_spare'] === false
162                )
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:170-170}
170        return $this->cacheManager->getSets($search, fn ($s) => $this->fetchSetsFromExternalApi($s));
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:176-176}
176        return $this->cacheManager->getParts($search, fn ($s) => $this->fetchPartsFromExternalApi($s));
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:188-188}
188        return $this->cacheManager->getPartElements($partExternalId, fn ($s) => $this->fetchPartElementsFromExternalApi($partExternalId));
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:194-194}
194        return $this->cacheManager->getSetElements($setExternalId, fn ($s) => $this->fetchSetElementsFromExternalApi($setExternalId));
{closure:/home/runner/work/my-lego-collection/my-lego-collection/back/src/CollectionManagement/Infrastructure/Service/RebrickableDataLoader.php:77-84}
77                fn ($item) => new ExternalSet(
78                    $item['set_num'],
79                    preg_replace('/-.*$/', '', $item['set_num']),
 
79                    preg_replace('/-.*$/', '', $item['set_num']),
 
79                    preg_replace('/-.*$/', '', $item['set_num']),
80                    $item['name'],
81                    $item['num_parts'],
82                    $item['set_img_url'] ?? '',
83                    $item['year']
84                ),
77                fn ($item) => new ExternalSet(
78                    $item['set_num'],
79                    preg_replace('/-.*$/', '', $item['set_num']),
 
79                    preg_replace('/-.*$/', '', $item['set_num']),
 
79                    preg_replace('/-.*$/', '', $item['set_num']),
80                    $item['name'],
81                    $item['num_parts'],
82                    $item['set_img_url'] ?? '',
83                    $item['year']
84                ),
{main}
3namespace App\CollectionManagement\Infrastructure\Service;
4
5use App\CollectionManagement\Domain\Model\External\ExternalElement;
6use App\CollectionManagement\Domain\Model\External\ExternalElementCollection;
7use App\CollectionManagement\Domain\Model\External\ExternalPart;
8use App\CollectionManagement\Domain\Model\External\ExternalSet;
9use App\CollectionManagement\Domain\Model\External\ExternalSetElement;
10use App\CollectionManagement\Domain\Model\External\ExternalSetElementCollection;
11use App\CollectionManagement\Domain\Model\PartCollection;
12use App\CollectionManagement\Domain\Model\SetCollection;
13use App\CollectionManagement\Domain\Service\LegoDataLoader;
14use Override;
15use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
16use Symfony\Contracts\HttpClient\HttpClientInterface;
17
18/**
19 * @author Wilhelm Zwertvaegher
20 * Lego data loader ; this will be our only loader at the moment.
21 * This loader uses a cache to avoid external requests when possible
22 *
23 */
24
25#[Autoconfigure]
26class RebrickableDataLoader implements LegoDataLoader
27{
28    private const API_BASE_URL = 'https://rebrickable.com/api/v3/lego/';
29
30    public function __construct(
31        private readonly ExternalDataCacheManager $cacheManager,
32        private readonly HttpClientInterface      $httpClient,
33        private readonly string                   $apiKey
34    ) {
35    }
36
37    /**
38     * @param string $endpointUri
39     * @return array<mixed>|null
40     *
41     * */
42    private function fetchFromExternalApi($endpointUri): ?array
43    {
44        try {
45            $response = $this->httpClient->request(
46                'GET',
47                sprintf('%s%s', self::API_BASE_URL, $endpointUri),
48                array(
49                    'headers' => [
50                        'Authorization' => sprintf('key %s', $this->apiKey),
51                    ]
52                )
53            );
54            // return raw data fetched from external API
55            $data = $response->toArray();
56            return $data['results'] ?? [];
57        }
58        // TODO : properly handle this throwable
59        catch (\Throwable $e) {
60            return null;
61        }
62    }
63
64    /**
65     * @param string $search
66     * @return SetCollection|null
67     */
68    private function fetchSetsFromExternalApi(string $search): ?SetCollection
69    {
70        $results = $this->fetchFromExternalApi(sprintf('sets/?search=%s', $search));
71        if ($results === null) {
72            return null;
73        }
74
75        return new SetCollection(
76            array_map(
77                fn ($item) => new ExternalSet(
78                    $item['set_num'],
79                    preg_replace('/-.*$/', '', $item['set_num']),
80                    $item['name'],
81                    $item['num_parts'],
82                    $item['set_img_url'] ?? '',
83                    $item['year']
84                ),
85                $results
86            )
87        );
88    }
89
90    /**
91     * @param string $search
92     * @return PartCollection|null
93     */
94    private function fetchPartsFromExternalApi(string $search): ?PartCollection
95    {
96        $results = $this->fetchFromExternalApi(sprintf('parts/?search=%s', $search));
97        if ($results === null) {
98            return null;
99        }
100
101        return new PartCollection(
102            array_map(
103                fn ($item) => new ExternalPart(
104                    $item['part_num'],
105                    isset($item['external_ids']['LEGO']) ? $item['external_ids']['LEGO'][0] : '',
106                    $item['name'],
107                    $item['part_img_url'] ?? ''
108                ),
109                $results
110            )
111        );
112    }
113
114    /**
115     * @param string $partExternalId
116     * @return ExternalElementCollection|null
117     */
118    private function fetchPartElementsFromExternalApi(string $partExternalId): ?ExternalElementCollection
119    {
120        $results = $this->fetchFromExternalApi(sprintf('parts/%s/colors/', $partExternalId));
121        if ($results === null) {
122            return null;
123        }
124
125        return new ExternalElementCollection(
126            array_map(
127                fn ($item) => new ExternalElement(
128                    $item['elements'][0],
129                    $item['elements'][0],
130                    $partExternalId,
131                    $item['part_img_url'] ?? '',
132                    $item['color_id'],
133                    $item['color_name']
134                ),
135                $results
136            )
137        );
138    }
139
140    /**
141     * @param string $setExternalId
142     * @return ExternalSetElementCollection|null
143     */
144    private function fetchSetElementsFromExternalApi(string $setExternalId): ?ExternalSetElementCollection
145    {
146        $results = $this->fetchFromExternalApi(sprintf('sets/%s/parts/?inc_part_details=1', $setExternalId));
147        if ($results === null) {
148            return null;
149        }
150
151        return new ExternalSetElementCollection(
152            array_map(
153                fn ($item) => new ExternalSetElement(
154                    $item['element_id'],
155                    $setExternalId,
156                    $item['part']['part_num'],
157                    $item['quantity']
158                ),
159                array_filter(
160                    $results,
161                    fn ($item) => $item['is_spare'] === false
162                )
163            )
164        );
165    }
166
167    #[Override]
168    public function findSets(string $search): ?SetCollection
169    {
170        return $this->cacheManager->getSets($search, fn ($s) => $this->fetchSetsFromExternalApi($s));
171    }
172
173    #[Override]
174    public function findParts(string $search): ?PartCollection
175    {
176        return $this->cacheManager->getParts($search, fn ($s) => $this->fetchPartsFromExternalApi($s));
177    }
178
179    #[Override]
180    public function getSetParts(string $setExternalId): ?ExternalSetElementCollection
181    {
182        return new ExternalSetElementCollection([]);
183    }
184
185    #[Override]
186    public function getPartElements(string $partExternalId): ?ExternalElementCollection
187    {
188        return $this->cacheManager->getPartElements($partExternalId, fn ($s) => $this->fetchPartElementsFromExternalApi($partExternalId));
189    }
190
191    #[Override]
192    public function getSetElements(string $setExternalId): ?ExternalSetElementCollection
193    {
194        return $this->cacheManager->getSetElements($setExternalId, fn ($s) => $this->fetchSetElementsFromExternalApi($setExternalId));
195    }