Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.05% covered (warning)
77.05%
47 / 61
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConfigurationProviderFactory
77.05% covered (warning)
77.05%
47 / 61
44.44% covered (danger)
44.44%
4 / 9
30.96
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getConstructType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getConstructArgs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getConstructOptions
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getProviderClassSpec
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 constructProvider
72.73% covered (warning)
72.73%
24 / 33
0.00% covered (danger)
0.00%
0 / 1
6.73
 newProvider
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getSupportedKeys
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 initList
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
1<?php
2
3namespace MediaWiki\Extension\CommunityConfiguration\Provider;
4
5use InvalidArgumentException;
6use LogicException;
7use MediaWiki\Config\Config;
8use MediaWiki\Extension\CommunityConfiguration\Hooks\HookRunner;
9use MediaWiki\Extension\CommunityConfiguration\Store\StoreFactory;
10use MediaWiki\Extension\CommunityConfiguration\Utils;
11use MediaWiki\Extension\CommunityConfiguration\Validation\ValidatorFactory;
12use MediaWiki\Logger\LoggerFactory;
13use MediaWiki\MediaWikiServices;
14
15/**
16 * Create a configuration provider
17 * @see IConfigurationProvider for further documentation
18 */
19class ConfigurationProviderFactory {
20
21    /** @var string */
22    private const DEFAULT_PROVIDER_TYPE = 'data';
23
24    /** Lazy loaded in initList */
25    private ?array $providerSpecs = null;
26    private ?array $classSpecs = null;
27    private array $providers = [];
28    private StoreFactory $storeFactory;
29    private ValidatorFactory $validatorFactory;
30    /** Used to create the services associated to a provider */
31    private MediaWikiServices $services;
32    private HookRunner $hookRunner;
33    private Config $config;
34
35    /**
36     * @param StoreFactory $storeFactory
37     * @param ValidatorFactory $validatorFactory
38     * @param Config $config
39     * @param HookRunner $hookRunner
40     * @param MediaWikiServices $services
41     */
42    public function __construct(
43        StoreFactory $storeFactory,
44        ValidatorFactory $validatorFactory,
45        Config $config,
46        HookRunner $hookRunner,
47        MediaWikiServices $services
48    ) {
49        $this->storeFactory = $storeFactory;
50        $this->validatorFactory = $validatorFactory;
51        $this->config = $config;
52        $this->services = $services;
53        $this->hookRunner = $hookRunner;
54    }
55
56    /**
57     * @param array $spec
58     * @param string $constructName
59     * @return mixed|string|null
60     */
61    private function getConstructType( array $spec, string $constructName ) {
62        return is_string( $spec[ $constructName ] ) ? $spec[ $constructName ] : ( is_array( $spec[ $constructName ] ) ?
63            $spec[ $constructName ]['type'] : null );
64    }
65
66    /**
67     * @param array $spec
68     * @param string $constructName
69     * @return mixed|string|null
70     */
71    private function getConstructArgs( array $spec, string $constructName ) {
72        return is_string( $spec[ $constructName ] ) ? $spec[ $constructName ] : ( is_array( $spec[ $constructName ] ) ?
73            ( $spec[ $constructName ]['args'] ?? [] ) : [] );
74    }
75
76    private function getConstructOptions( array $spec, string $constructName ): array {
77        if ( !is_array( $spec[$constructName] ) ) {
78            return [];
79        }
80        return $spec[$constructName]['options'] ?? [];
81    }
82
83    private function getProviderClassSpec( string $className ): array {
84        if ( !array_key_exists( $className, $this->classSpecs ?? [] ) ) {
85            throw new InvalidArgumentException( "Provider class $className is not supported" );
86        }
87        // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
88        return $this->classSpecs[$className];
89    }
90
91    /**
92     * Unconditionally construct a provider
93     *
94     * @param string $providerId The provider's key as set in extension.json
95     * @return IConfigurationProvider
96     * @throws InvalidArgumentException when the definition of provider is invalid
97     */
98    private function constructProvider( string $providerId ): IConfigurationProvider {
99        if ( !array_key_exists( $providerId, $this->providerSpecs ) ) {
100            throw new InvalidArgumentException( "Provider $providerId is not supported" );
101        }
102        $spec = $this->providerSpecs[$providerId];
103        $storeType = $this->getConstructType( $spec, 'store' );
104
105        $validatorType = $this->getConstructType( $spec, 'validator' );
106        if ( $storeType === null ) {
107            throw new InvalidArgumentException(
108                "Wrong type for \"store\" property for \"$providerId\" provider. Allowed types are: string, object"
109            );
110        }
111        if ( $validatorType === null ) {
112            throw new InvalidArgumentException(
113                "Wrong type for \"validator\" property for \"$providerId\" provider. Allowed types are: string, object"
114            );
115        }
116        $storeArgs = $this->getConstructArgs( $spec, 'store' );
117        $validatorArgs = $this->getConstructArgs( $spec, 'validator' );
118
119        $store = $this->storeFactory->newStore( $providerId, $storeType, $storeArgs );
120        $store->setOptions( $this->getConstructOptions( $spec, 'store' ) );
121        $ctorArgs = [
122            $providerId,
123            $spec['options'] ?? [],
124            $store,
125            $this->validatorFactory->newValidator( $providerId, $validatorType, $validatorArgs )
126        ];
127
128        $classSpec = $this->getProviderClassSpec( $spec['type'] ?? self::DEFAULT_PROVIDER_TYPE );
129
130        foreach ( $spec['services'] ?? [] as $serviceName ) {
131            $ctorArgs[] = $this->services->getService( $serviceName );
132        }
133        $ctorArgs = array_merge( $ctorArgs, $spec['args'] ?? [] );
134
135        $className = $classSpec['class'];
136        $provider = new $className( ...$ctorArgs );
137        if ( !$provider instanceof IConfigurationProvider ) {
138            throw new LogicException( "$className is not an instance of IConfigurationProvider" );
139        }
140        $provider->setLogger( LoggerFactory::getInstance( 'CommunityConfiguration' ) );
141        return $provider;
142    }
143
144    /**
145     * @param string $providerId The provider's key as set in extension.json
146     * @return IConfigurationProvider
147     * @throws InvalidArgumentException when provider $name is not registered
148     */
149    public function newProvider( string $providerId ): IConfigurationProvider {
150        $this->initList();
151        if ( !array_key_exists( $providerId, $this->providerSpecs ) ) {
152            throw new InvalidArgumentException( "Provider $providerId is not supported" );
153        }
154        if ( !array_key_exists( $providerId, $this->providers ) ) {
155            $this->providers[$providerId] = $this->constructProvider( $providerId );
156        }
157        return $this->providers[$providerId];
158    }
159
160    /**
161     * Return a list of supported provider names
162     *
163     * @return string[] List of provider names (supported by newProvider)
164     */
165    public function getSupportedKeys(): array {
166        $this->initList();
167        return array_keys( $this->providerSpecs );
168    }
169
170    /**
171     * Build the list of provider specs by reading CommunityConfigurationProviders from
172     * main config and give a chance to extensions to modify it by running _initList hook.
173     */
174    private function initList() {
175        if ( is_array( $this->providerSpecs ) && is_array( $this->classSpecs ) ) {
176            return;
177        }
178        $this->providerSpecs = Utils::getMergedAttribute( $this->config, 'CommunityConfigurationProviders' );
179        $this->classSpecs = Utils::getMergedAttribute( $this->config, 'CommunityConfigurationProviderClasses' );
180        // This hook can be used to disable unwanted providers
181        // or conditionally register providers.
182        $this->hookRunner->onCommunityConfigurationProvider_initList( $this->providerSpecs );
183    }
184}