Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
94 / 94
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
Config
100.00% covered (success)
100.00%
94 / 94
100.00% covered (success)
100.00%
12 / 12
38
100.00% covered (success)
100.00%
1 / 1
 setComposerJsonPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOutput
n/a
0 / 0
n/a
0 / 0
1
 load
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
 base
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addBase
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 changelogFile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 changesDir
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 link
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 ordering
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 types
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getPlugin
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
8
 formatterPlugin
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 versioningPlugin
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Configuration loader for the changelogger tool.
4 *
5 * @package automattic/jetpack-changelogger
6 */
7
8namespace Automattic\Jetpack\Changelogger;
9
10use Symfony\Component\Console\Output\OutputInterface;
11
12/**
13 * Configuration loader for the changelogger tool.
14 */
15class Config {
16
17    /**
18     * Default config settings.
19     *
20     * @var array
21     */
22    private static $defaultConfig = array(
23        'changelog'     => 'CHANGELOG.md',
24        'changes-dir'   => 'changelog',
25        'link-template' => null,
26        'ordering'      => array( 'subheading', 'content' ),
27        'formatter'     => 'keepachangelog',
28        'types'         => array(
29            'security'   => 'Security',
30            'added'      => 'Added',
31            'changed'    => 'Changed',
32            'deprecated' => 'Deprecated',
33            'removed'    => 'Removed',
34            'fixed'      => 'Fixed',
35        ),
36        'versioning'    => 'semver',
37    );
38
39    /**
40     * Active config settings.
41     *
42     * @var array
43     */
44    private static $config = array();
45
46    /**
47     * Cached config settings.
48     *
49     * @var array
50     */
51    private static $cache = array();
52
53    /**
54     * Whether `load()` was called already.
55     *
56     * @var bool
57     */
58    private static $loaded = false;
59
60    /**
61     * Location of composer.json, overriding any COMPOSER environment variable.
62     *
63     * @var string|null
64     */
65    private static $composerJsonPath;
66
67    /**
68     * Set the location of composer.json, overriding auto-detection.
69     *
70     * @since 1.2.0
71     * @param string|null $path Path to composer.json, or null to re-enable auto-detection.
72     */
73    public static function setComposerJsonPath( $path ) {
74        self::$composerJsonPath = $path;
75    }
76
77    /**
78     * Former method used to set an OutputInterface. No longer used.
79     *
80     * @deprecated since 1.2.0, no longer needed.
81     * @param OutputInterface $out Ignored.
82     * @codeCoverageIgnore
83     */
84    public static function setOutput( OutputInterface $out ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
85    }
86
87    /**
88     * Load the configuration.
89     *
90     * @throws \DomainException If the path to composer.json exists but can't be `realpath`-ed.
91     * @throws ConfigException If composer.json is not found or is invalid (since 1.2.0).
92     */
93    private static function load() {
94        if ( self::$loaded ) {
95            return;
96        }
97        self::$loaded = true;
98
99        self::$config         = self::$defaultConfig;
100        self::$config['base'] = getcwd();
101
102        $from = '';
103        if ( self::$composerJsonPath !== null ) {
104            $composer = (string) self::$composerJsonPath;
105        } else {
106            $composer = getenv( 'COMPOSER' );
107            if ( $composer ) {
108                $from = ' (as specified by the COMPOSER environment variable)';
109            } else {
110                $composer = getcwd() . DIRECTORY_SEPARATOR . 'composer.json';
111            }
112        }
113        if ( ! file_exists( $composer ) ) {
114            throw new ConfigException( "File {$composer}{$from} is not found." );
115        }
116        $data = json_decode( file_get_contents( $composer ), true );
117        if ( ! is_array( $data ) ) {
118            throw new ConfigException( "File {$composer}{$from} could not be parsed." );
119        }
120
121        $dir = realpath( $composer );
122        if ( false === $dir ) {
123            throw new \DomainException( "Path $composer is not valid" ); // @codeCoverageIgnore
124        }
125        self::$config['base'] = dirname( $dir );
126        if ( isset( $data['extra']['changelogger'] ) ) {
127            self::$config = array_merge( self::$config, $data['extra']['changelogger'] );
128        }
129    }
130
131    /**
132     * Get the base directory.
133     *
134     * @return string
135     */
136    public static function base() {
137        self::load();
138        return self::$config['base'];
139    }
140
141    /**
142     * Add the base directory to a path, if necessary.
143     *
144     * @param string $path Path.
145     * @return string
146     */
147    private static function addBase( $path ) {
148        // Stupid Windows requires a regex.
149        if ( ! preg_match( '#^(?:/|' . preg_quote( DIRECTORY_SEPARATOR, '#' ) . '|[a-zA-Z]:\\\\)#', $path ) ) {
150            $path = self::base() . DIRECTORY_SEPARATOR . $path;
151        }
152        return $path;
153    }
154
155    /**
156     * Get the changelog filename.
157     *
158     * @return string
159     */
160    public static function changelogFile() {
161        self::load();
162        if ( ! isset( self::$cache['changelog'] ) ) {
163            self::$cache['changelog'] = self::addBase( self::$config['changelog'] );
164        }
165        return self::$cache['changelog'];
166    }
167
168    /**
169     * Get the changes directory.
170     *
171     * @return string
172     */
173    public static function changesDir() {
174        self::load();
175        if ( ! isset( self::$cache['changes-dir'] ) ) {
176            self::$cache['changes-dir'] = self::addBase( self::$config['changes-dir'] );
177        }
178        return self::$cache['changes-dir'];
179    }
180
181    /**
182     * Get the link.
183     *
184     * @param string $old Old version number.
185     * @param string $new New version number.
186     * @return string|null
187     */
188    public static function link( $old, $new ) {
189        self::load();
190        if ( null !== self::$config['link-template'] ) {
191            return strtr(
192                self::$config['link-template'],
193                array(
194                    '${old}' => rawurlencode( $old ),
195                    '${new}' => rawurlencode( $new ),
196                )
197            );
198        }
199        return null;
200    }
201
202    /**
203     * Get change entry ordering.
204     *
205     * @return string[]
206     */
207    public static function ordering() {
208        self::load();
209        if ( ! isset( self::$cache['ordering'] ) ) {
210            self::$cache['ordering'] = array_map( 'strval', (array) self::$config['ordering'] );
211        }
212        return self::$cache['ordering'];
213    }
214
215    /**
216     * Get change types.
217     *
218     * @return array
219     */
220    public static function types() {
221        self::load();
222        if ( ! isset( self::$cache['types'] ) ) {
223            self::$cache['types'] = array();
224            foreach ( self::$config['types'] as $k => $v ) {
225                self::$cache['types'][ strtolower( $k ) ] = $v;
226            }
227        }
228        return self::$cache['types'];
229    }
230
231    /**
232     * Get a plugin.
233     *
234     * @param string|array $config Plugin name or configuration array.
235     * @param string       $suffix Plugin class suffix.
236     * @param string       $interface Expected interface name.
237     * @return object|null Object, or null if the plugin was not found.
238     */
239    private static function getPlugin( $config, $suffix, $interface ) {
240        if ( is_string( $config ) ) {
241            $config = array( 'name' => $config );
242        }
243
244        if ( isset( $config['name'] ) ) {
245            $class = __NAMESPACE__ . '\\Plugins\\' . ucfirst( $config['name'] ) . $suffix;
246        } elseif ( isset( $config['class'] ) ) {
247            $class = $config['class'];
248        } elseif ( isset( $config['filename'] ) ) {
249            $classes = get_declared_classes();
250            require $config['filename'];
251            $classes = array_filter(
252                array_diff( get_declared_classes(), $classes ),
253                function ( $class ) use ( $interface ) {
254                    return is_a( $class, $interface, true );
255                }
256            );
257            if ( count( $classes ) !== 1 ) {
258                return null;
259            }
260            $class = array_pop( $classes );
261        } else {
262            return null;
263        }
264        if ( ! class_exists( $class ) || ! is_a( $class, $interface, true ) ) {
265            return null;
266        }
267        return $class::instantiate( $config );
268    }
269
270    /**
271     * Get formatting plugin.
272     *
273     * @return FormatterPlugin
274     * @throws \RuntimeException If the configured formatter is unknown.
275     */
276    public static function formatterPlugin() {
277        self::load();
278        if ( ! isset( self::$cache['formatter'] ) ) {
279            $obj = self::getPlugin( self::$config['formatter'], 'Formatter', FormatterPlugin::class );
280            if ( ! $obj instanceof FormatterPlugin ) {
281                $info = json_encode( self::$config['formatter'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
282                throw new \RuntimeException( "Unknown formatter plugin $info" );
283            }
284            self::$cache['formatter'] = $obj;
285        }
286        return self::$cache['formatter'];
287    }
288
289    /**
290     * Get verisoning plugin.
291     *
292     * @return VersioningPlugin
293     * @throws \RuntimeException If the configured versioning plugin is unknown.
294     */
295    public static function versioningPlugin() {
296        self::load();
297        if ( ! isset( self::$cache['versioning'] ) ) {
298            $obj = self::getPlugin( self::$config['versioning'], 'Versioning', VersioningPlugin::class );
299            if ( ! $obj instanceof VersioningPlugin ) {
300                $info = json_encode( self::$config['versioning'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
301                throw new \RuntimeException( "Unknown versioning plugin $info" );
302            }
303            self::$cache['versioning'] = $obj;
304        }
305        return self::$cache['versioning'];
306    }
307}