Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
32 / 33
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Reader
96.97% covered (success)
96.97%
32 / 33
87.50% covered (warning)
87.50%
7 / 8
14
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
1
 readData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLicense
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getPackageName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAuthors
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getVendor
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getYear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extractLicense
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of fast-forward/dev-tools.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/dev-tools
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\DevTools\License;
20
21use Safe\Exceptions\JsonException;
22use SplFileObject;
23
24use function Safe\json_decode;
25
26/**
27 * Reads composer.json and exposes metadata for license generation.
28 *
29 * This class parses a composer.json file via SplFileObject and provides
30 * methods to extract license information, package name, authors, vendor,
31 * and the current year for copyright notices.
32 */
33  class Reader implements ReaderInterface
34{
35    private array $data;
36
37    /**
38     * Creates a new Reader instance.
39     *
40     * @param SplFileObject $source The source file to read from, typically composer.json
41     *
42     * @throws JsonException if the JSON content is invalid
43     */
44    public function __construct(SplFileObject $source)
45    {
46        $this->data = $this->readData($source);
47    }
48
49    /**
50     * Reads and parses the JSON content from the source file.
51     *
52     * @param SplFileObject $source The source file to read from
53     *
54     * @return array The parsed JSON data as an associative array
55     *
56     * @throws JsonException if the JSON is invalid
57     */
58    private function readData(SplFileObject $source): array
59    {
60        $content = $source->fread($source->getSize());
61
62        return json_decode($content, true);
63    }
64
65    /**
66     * Retrieves the license identifier from composer.json.
67     *
68     * If the license is a single string, returns it directly.
69     * If it's an array with one element, extracts that element.
70     * Returns null if no license is set or if multiple licenses are specified.
71     *
72     * @return string|null the license string, or null if not set or unsupported
73     */
74    public function getLicense(): ?string
75    {
76        $license = $this->data['license'] ?? [];
77
78        if (\is_string($license)) {
79            return $license;
80        }
81
82        return $this->extractLicense($license);
83    }
84
85    /**
86     * Retrieves the package name from composer.json.
87     *
88     * @return string the full package name (vendor/package), or empty string if not set
89     */
90    public function getPackageName(): string
91    {
92        return $this->data['name'] ?? '';
93    }
94
95    /**
96     * Retrieves the list of authors from composer.json.
97     *
98     * Each author is normalized to include name, email, homepage, and role fields.
99     * Returns an empty array if no authors are defined.
100     *
101     * @return array<int, array{name: string, email: string, homepage: string, role: string}>
102     */
103    public function getAuthors(): array
104    {
105        $authors = $this->data['authors'] ?? [];
106
107        if ([] === $authors) {
108            return [];
109        }
110
111        return array_map(
112            static fn(array $author): array => [
113                'name' => $author['name'] ?? '',
114                'email' => $author['email'] ?? '',
115                'homepage' => $author['homepage'] ?? '',
116                'role' => $author['role'] ?? '',
117            ],
118            $authors
119        );
120    }
121
122    /**
123     * Extracts the vendor name from the package name.
124     *
125     * The package name is expected in vendor/package format.
126     * Returns null if no package name is set or if the package has no vendor prefix.
127     *
128     * @return string|null the vendor name, or null if package has no vendor prefix
129     */
130    public function getVendor(): ?string
131    {
132        $packageName = $this->getPackageName();
133
134        if ('' === $packageName) {
135            return null;
136        }
137
138        $parts = explode('/', $packageName, 2);
139
140        if (! isset($parts[1])) {
141            return null;
142        }
143
144        return $parts[0];
145    }
146
147    /**
148     * Returns the current year for copyright notices.
149     *
150     * @return int the current year as an integer
151     */
152    public function getYear(): int
153    {
154        return (int) date('Y');
155    }
156
157    /**
158     * Extracts a single license from an array of licenses.
159     *
160     * Returns the first license if exactly one element exists.
161     * Returns null if the array is empty or contains multiple licenses.
162     *
163     * @param array<string> $license The license array to extract from
164     *
165     * @return string|null a single license string, or null if extraction is not possible
166     */
167    private function extractLicense(array $license): ?string
168    {
169        if ([] === $license) {
170            return null;
171        }
172
173        if (1 === \count($license)) {
174            return $license[0];
175        }
176
177        return null;
178    }
179}