Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.76% covered (warning)
82.76%
48 / 58
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContentHandlerFactory
82.76% covered (warning)
82.76%
48 / 58
80.00% covered (warning)
80.00%
8 / 10
19.66
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
 getContentHandler
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 defineContentHandler
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 getContentModels
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getAllContentFormats
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 isDefinedModel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createForModelID
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 validateContentHandler
36.36% covered (danger)
36.36%
4 / 11
0.00% covered (danger)
0.00%
0 / 1
8.12
 createContentHandlerFromHandlerSpec
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 createContentHandlerFromHook
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Content;
8
9use InvalidArgumentException;
10use MediaWiki\HookContainer\HookContainer;
11use MediaWiki\HookContainer\HookRunner;
12use Psr\Log\LoggerInterface;
13use Wikimedia\ObjectFactory\ObjectFactory;
14
15/**
16 * @since 1.35
17 * @ingroup Content
18 * @author Art Baltai
19 */
20final class ContentHandlerFactory implements IContentHandlerFactory {
21
22    /**
23     * @var ContentHandler[] Registry of ContentHandler instances by model id
24     */
25    private $handlersByModel = [];
26    private readonly HookRunner $hookRunner;
27
28    /**
29     * @since 1.35
30     * @internal Please use MediaWikiServices::getContentHandlerFactory instead
31     *
32     * @param string[]|callable[] $handlerSpecs An associative array mapping each known
33     *   content model to the ObjectFactory spec used to construct its ContentHandler.
34     *   This array typically comes from $wgContentHandlers.
35     * @param ObjectFactory $objectFactory
36     * @param HookContainer $hookContainer
37     * @param LoggerInterface $logger
38     */
39    public function __construct(
40        private array $handlerSpecs,
41        private readonly ObjectFactory $objectFactory,
42        HookContainer $hookContainer,
43        private readonly LoggerInterface $logger,
44    ) {
45        $this->hookRunner = new HookRunner( $hookContainer );
46    }
47
48    /**
49     * @param string $modelID
50     *
51     * @return ContentHandler
52     * @throws UnknownContentModelException If no handler is known for the model ID.
53     */
54    public function getContentHandler( string $modelID ): ContentHandler {
55        if ( empty( $this->handlersByModel[$modelID] ) ) {
56            $contentHandler = $this->createForModelID( $modelID );
57
58            $this->logger->info(
59                "Registered handler for {$modelID}" . get_class( $contentHandler )
60            );
61            $this->handlersByModel[$modelID] = $contentHandler;
62        }
63
64        return $this->handlersByModel[$modelID];
65    }
66
67    /**
68     * Define HandlerSpec for ModelID.
69     * @param string $modelID
70     * @param callable|string $handlerSpec
71     *
72     * @internal
73     */
74    public function defineContentHandler( string $modelID, $handlerSpec ): void {
75        if ( !is_callable( $handlerSpec ) && !is_string( $handlerSpec ) ) {
76            throw new InvalidArgumentException(
77                "ContentHandler Spec for modelID '{$modelID}' must be callable or class name"
78            );
79        }
80        unset( $this->handlersByModel[$modelID] );
81        $this->handlerSpecs[$modelID] = $handlerSpec;
82    }
83
84    /**
85     * Get defined ModelIDs
86     *
87     * @return string[]
88     */
89    public function getContentModels(): array {
90        $modelsFromHook = [];
91        $this->hookRunner->onGetContentModels( $modelsFromHook );
92        $models = array_merge( // auto-registered from config and MediaWikiServices or manual
93            array_keys( $this->handlerSpecs ),
94
95            // incorrect registered and called: without HOOK_NAME_GET_CONTENT_MODELS
96            array_keys( $this->handlersByModel ),
97
98            // correct registered: as HOOK_NAME_GET_CONTENT_MODELS
99            $modelsFromHook );
100
101        return array_unique( $models );
102    }
103
104    /**
105     * @return string[]
106     */
107    public function getAllContentFormats(): array {
108        $formats = [];
109        foreach ( $this->handlerSpecs as $model => $class ) {
110            $formats += array_fill_keys(
111                $this->getContentHandler( $model )->getSupportedFormats(),
112                true );
113        }
114
115        return array_keys( $formats );
116    }
117
118    public function isDefinedModel( string $modelID ): bool {
119        return in_array( $modelID, $this->getContentModels(), true );
120    }
121
122    /**
123     * Create ContentHandler for ModelID
124     *
125     * @param string $modelID The ID of the content model for which to get a handler.
126     * Use CONTENT_MODEL_XXX constants.
127     *
128     * @return ContentHandler The ContentHandler singleton for handling the model given by the ID.
129     *
130     * @throws UnknownContentModelException If no handler is known for the model ID.
131     */
132    private function createForModelID( string $modelID ): ContentHandler {
133        $handlerSpec = $this->handlerSpecs[$modelID] ?? null;
134        if ( $handlerSpec !== null ) {
135            return $this->createContentHandlerFromHandlerSpec( $modelID, $handlerSpec );
136        }
137
138        return $this->createContentHandlerFromHook( $modelID );
139    }
140
141    /**
142     * @param string $modelID
143     * @param ContentHandler|null $contentHandler
144     *
145     * @throws UnknownContentModelException
146     */
147    private function validateContentHandler( string $modelID, $contentHandler ): void {
148        if ( $contentHandler === null ) {
149            throw new UnknownContentModelException( $modelID );
150        }
151
152        if ( !is_object( $contentHandler ) ) {
153            throw new InvalidArgumentException(
154                "ContentHandler for model {$modelID} wrong: non-object given."
155            );
156        }
157
158        if ( !$contentHandler instanceof ContentHandler ) {
159            throw new InvalidArgumentException(
160                "ContentHandler for model {$modelID} must supply a ContentHandler instance, "
161                . get_class( $contentHandler ) . ' given.'
162            );
163        }
164    }
165
166    /**
167     * @param string $modelID
168     * @param callable|string $handlerSpec
169     *
170     * @return ContentHandler
171     * @throws UnknownContentModelException
172     */
173    private function createContentHandlerFromHandlerSpec(
174        string $modelID, $handlerSpec
175    ): ContentHandler {
176        /**
177         * @var ContentHandler $contentHandler
178         */
179        $contentHandler = $this->objectFactory->createObject(
180            $handlerSpec,
181            [
182                'assertClass' => ContentHandler::class,
183                'allowCallable' => true,
184                'allowClassName' => true,
185                'extraArgs' => [ $modelID ],
186            ]
187        );
188
189        $this->validateContentHandler( $modelID, $contentHandler );
190
191        return $contentHandler;
192    }
193
194    /**
195     * @param string $modelID
196     *
197     * @return ContentHandler
198     * @throws UnknownContentModelException
199     */
200    private function createContentHandlerFromHook( string $modelID ): ContentHandler {
201        $contentHandler = null;
202        $this->hookRunner->onContentHandlerForModelID( $modelID, $contentHandler );
203        $this->validateContentHandler( $modelID, $contentHandler );
204
205        '@phan-var ContentHandler $contentHandler';
206
207        return $contentHandler;
208    }
209}