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}