4141 * package generator needs and nothing more. Requests degrade to unauthenticated when no token is
4242 * configured, which keeps the generator usable on a machine without credentials.
4343 *
44- * @phpstan-type ResponseData array{status: int, body: string, links: array<string, string>}
44+ * @phpstan-type ResponseData array{status: int, body: string, links: array<string, non-empty- string>}
4545 */
4646class GitHubClient implements GitHubClientInterface
4747{
48- private const string API_ROOT = 'https://api.github.com ' ;
49- private const string API_VERSION = '2022-11-28 ' ;
48+ private const string API_ROOT = 'https://api.github.com ' ;
49+ private const string API_VERSION = '2022-11-28 ' ;
50+ private const string DEFAULT_USER_AGENT = 'dotkernel.com ' ;
51+
52+ /**
53+ * cURL rejects an empty user agent, and GitHub rejects requests without one, so an empty
54+ * configured value falls back to the default rather than failing every request.
55+ *
56+ * @var non-empty-string
57+ */
58+ private readonly string $ userAgent ;
5059
5160 public function __construct (
5261 private readonly string $ token ,
53- private readonly string $ userAgent ,
62+ string $ userAgent ,
5463 private readonly int $ timeout ,
5564 private readonly int $ connectTimeout ,
5665 ) {
66+ $ this ->userAgent = $ userAgent === '' ? self ::DEFAULT_USER_AGENT : $ userAgent ;
5767 }
5868
69+ /**
70+ * @param non-empty-string $path
71+ */
5972 public function get (string $ path , string $ accept = self ::ACCEPT_JSON ): ?string
6073 {
6174 $ response = $ this ->request ($ this ->absoluteUrl ($ path ), $ accept );
@@ -70,6 +83,7 @@ public function get(string $path, string $accept = self::ACCEPT_JSON): ?string
7083 }
7184
7285 /**
86+ * @param non-empty-string $path
7387 * @return list<array<string, mixed>>
7488 */
7589 public function getAllPages (string $ path ): array
@@ -99,6 +113,7 @@ public function getAllPages(string $path): array
99113 }
100114
101115 /**
116+ * @param non-empty-string $url
102117 * @return ResponseData
103118 */
104119 private function request (string $ url , string $ accept ): array
@@ -147,16 +162,18 @@ private function request(string $url, string $accept): array
147162 /**
148163 * Parses `<https://...>; rel="next", <https://...>; rel="last"` into a rel => url map.
149164 *
150- * @return array<string, string>
165+ * @return array<string, non-empty- string>
151166 */
152167 private function parseLinkHeader (string $ value ): array
153168 {
154169 $ links = [];
155170
156171 foreach (explode (', ' , $ value ) as $ part ) {
157- if (preg_match ('/<([^>]+)>\s*;\s*rel="([^"]+)"/ ' , trim ($ part ), $ matches ) = == 1 ) {
158- $ links [ $ matches [ 2 ]] = $ matches [ 1 ] ;
172+ if (preg_match ('/<([^>]+)>\s*;\s*rel="([^"]+)"/ ' , trim ($ part ), $ matches ) ! == 1 ) {
173+ continue ;
159174 }
175+
176+ $ links [$ matches [2 ]] = $ matches [1 ];
160177 }
161178
162179 return $ links ;
@@ -179,6 +196,10 @@ private function headers(string $accept): array
179196 return $ headers ;
180197 }
181198
199+ /**
200+ * @param non-empty-string $path
201+ * @return non-empty-string
202+ */
182203 private function absoluteUrl (string $ path ): string
183204 {
184205 if (str_starts_with ($ path , 'http:// ' ) || str_starts_with ($ path , 'https:// ' )) {
0 commit comments