$mappings
6 | $body
9 | $mappings
10 | $array
15 | $array
18 | join
23 | join
24 | join
25 | (array)$services->getMainConfig()->get( 'AvailableRights' )
30 | WikibaseRepo::getSettings()->getSetting( 'string-limits' )['PT:url']
33 | MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( ILoadBalancer::DB_REPLICA )
36 | ***
31 |
32 | Users with editing permissions can add, edit, or remove mappings.
33 |
34 | A mapping consists of a predicate and a URL. The predicate can only be one out of a preconfigured set of values. The URL has to be valid.
35 |
36 |
37 |
38 | Mapping predicates can be configured via the `MediaWiki:MappingPredicates` page by anyone with interface-admin permissions.
39 | You can also configure mapping predicates via [PHP Configuration](#php-configuration).
40 |
41 |
42 |
43 |
44 |
45 | ### REST API
46 |
47 | This extension provides REST API endpoints for getting and setting the RDF mappings for a Wikibase entity.
48 |
49 | For more information, refer to the [REST API documentation](docs/rest.md).
50 |
51 | ## Installation
52 |
53 | Platform requirements:
54 |
55 | - [PHP] 8.1 or later (tested up to 8.4)
56 | - [MediaWiki] 1.43 or later (tested up to 1.43)
57 | - [Wikibase] 1.43 or later (tested up to 1.43)
58 |
59 | The recommended way to install Wikibase RDF is using [Composer] with
60 | [MediaWiki's built-in support for Composer][Composer install].
61 |
62 | On the command line, go to your wiki's root directory. Then run these two commands:
63 |
64 | ```shell script
65 | COMPOSER=composer.local.json composer require --no-update professional-wiki/wikibase-rdf:~2.0
66 | ```
67 | ```shell script
68 | composer update professional-wiki/wikibase-rdf --no-dev -o
69 | ```
70 |
71 | Then enable the extension by adding the following to the bottom of your wikis [LocalSettings.php] file:
72 |
73 | ```php
74 | wfLoadExtension( 'WikibaseRDF' );
75 | ```
76 |
77 | You can verify the extension was enabled successfully by opening your wiki's Special:Version page in your browser.
78 |
79 | ## PHP Configuration
80 |
81 | Configuration can be changed via [LocalSettings.php].
82 |
83 | ### Allowed predicates
84 |
85 | List of allowed predicates.
86 |
87 | Variable: `$wgWikibaseRdfPredicates`
88 |
89 | Default: `[]`
90 |
91 | Example:
92 |
93 | ```php
94 | $wgWikibaseRdfPredicates = [
95 | 'owl:sameAs',
96 | 'owl:SymmetricProperty',
97 | 'rdfs:subClassOf',
98 | 'rdfs:subPropertyOf',
99 | ];
100 | ```
101 |
102 | You can also configure what predicates are allowed via the `MediaWiki:MappingPredicates` page.
103 |
104 | ## Development
105 |
106 | To ensure the dev dependencies get installed, have this in your `composer.local.json`:
107 |
108 | ```json
109 | {
110 | "require": {
111 | "vimeo/psalm": "^4",
112 | "phpstan/phpstan": "^1.4.9"
113 | },
114 | "extra": {
115 | "merge-plugin": {
116 | "include": [
117 | "extensions/WikibaseRDF/composer.json"
118 | ]
119 | }
120 | }
121 | }
122 | ```
123 |
124 | ### Running tests and CI checks
125 |
126 | You can use the `Makefile` by running make commands in the `WikibaseRDF` directory.
127 |
128 | * `make ci`: Run everything
129 | * `make test`: Run all tests
130 | * `make cs`: Run all style checks and static analysis
131 |
132 | Alternatively, you can execute commands from the MediaWiki root directory:
133 |
134 | * PHPUnit: `php tests/phpunit/phpunit.php -c extensions/WikibaseRDF/`
135 | * Style checks: `vendor/bin/phpcs -p -s --standard=extensions/WikibaseRDF/phpcs.xml`
136 | * PHPStan: `vendor/bin/phpstan analyse --configuration=extensions/WikibaseRDF/phpstan.neon --memory-limit=2G`
137 | * Psalm: `php vendor/bin/psalm --config=extensions/WikibaseRDF/psalm.xml`
138 |
139 | ## Release notes
140 |
141 | ### Version 2.0.0 - 2025-07-07
142 |
143 | - Raised minimum MediaWiki version to 1.43
144 | - Raised minimum PHP version to 8.1
145 | - Translation updates for system messages (thanks @translatewiki and its translator community)
146 |
147 | ### Version 1.1.0 - 2022-11-25
148 |
149 | - Added notification about SPARQL store behavior that shows on first edit
150 | - Translation updates for system messages (thanks @translatewiki and its translator community)
151 |
152 | ### Version 1.0.0 - 2022-10-04
153 |
154 | Initial release for Wikibase 1.37 with these features:
155 |
156 | - Ability to add mappings to Items and Properties via an on-page UI
157 | - Inclusion of mappings in the RDF output
158 | - Configurable relationships (predicates), including configuration UI on `MediaWiki:MappingPredicates`
159 | - API endpoint to retrieve or update the mappings for an Entity
160 | - API endpoint to retrieve all mappings defined on the wiki
161 | - TranslateWiki integration
162 | - Support for PHP 8.0 and 8.1
163 |
164 | [Professional Wiki]: https://professional.wiki
165 | [Wikibase]: https://wikibase.consulting/what-is-wikibase/
166 | [Wikibase hosting]: https://professional.wiki/en/hosting/wikibase
167 | [Wikibase development]: https://professional.wiki/en/wikibase-software-development
168 | [Wikibase consulting]: https://wikibase.consulting/
169 | [MediaWiki]: https://www.mediawiki.org
170 | [PHP]: https://www.php.net
171 | [Composer]: https://getcomposer.org
172 | [Composer install]: https://professional.wiki/en/articles/installing-mediawiki-extensions-with-composer
173 | [LocalSettings.php]: https://www.pro.wiki/help/mediawiki-localsettings-php-guide
174 | [Wikibase Stakeholder Group]:https://wbstakeholder.group/
175 |
--------------------------------------------------------------------------------
/tests/Application/SaveMappings/SaveMappingsUseCaseTest.php:
--------------------------------------------------------------------------------
1 | presenter = new SpySaveMappingsPresenter();
39 | $this->repository = new InMemoryMappingRepository();
40 | }
41 |
42 | private function newSucceedingAuthorizer(): EntityMappingsAuthorizer {
43 | return new SucceedingEntityMappingsAuthorizer();
44 | }
45 |
46 | private function newFailingAuthorizer(): EntityMappingsAuthorizer {
47 | return new FailingEntityMappingsAuthorizer();
48 | }
49 |
50 | private function newUseCase( EntityMappingsAuthorizer $authorizer ): SaveMappingsUseCase {
51 | return new SaveMappingsUseCase(
52 | $this->presenter,
53 | $this->repository,
54 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ),
55 | new BasicEntityIdParser(),
56 | new MappingListSerializer(),
57 | $authorizer,
58 | new SucceedingObjectValidator()
59 | );
60 | }
61 |
62 | public function testShouldShowSuccess(): void {
63 | $useCase = $this->newUseCase( $this->newSucceedingAuthorizer() );
64 |
65 | $useCase->saveMappings(
66 | 'Q1',
67 | [
68 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ]
69 | ]
70 | );
71 |
72 | $this->assertTrue( $this->presenter->showedSuccess );
73 | $this->assertSame( [], $this->presenter->invalidMappings );
74 | $this->assertFalse( $this->presenter->showedSaveFailed );
75 | }
76 |
77 | public function testMappingIsPersisted(): void {
78 | $useCase = $this->newUseCase( $this->newSucceedingAuthorizer() );
79 |
80 | $useCase->saveMappings(
81 | 'Q2',
82 | [
83 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ]
84 | ]
85 | );
86 |
87 | $mappings = $this->repository->getMappings( new ItemId( 'Q2' ) );
88 |
89 | $this->assertSame(
90 | self::VALID_PREDICATE,
91 | $mappings->asArray()[0]->predicate
92 | );
93 | $this->assertSame(
94 | self::VALID_OBJECT,
95 | $mappings->asArray()[0]->object
96 | );
97 | }
98 |
99 | public function testShouldShowInvalidMappingsWhenPredicateIsInvalid(): void {
100 | $useCase = $this->newUseCase( $this->newSucceedingAuthorizer() );
101 |
102 | $useCase->saveMappings(
103 | 'Q3',
104 | [
105 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ],
106 | [ 'predicate' => self::INVALID_PREDICATE, 'object' => self::VALID_OBJECT ],
107 | ]
108 | );
109 |
110 | $this->assertFalse( $this->presenter->showedSuccess );
111 | $this->assertSame(
112 | self::INVALID_PREDICATE,
113 | $this->presenter->invalidMappings[0]['predicate']
114 | );
115 | $this->assertSame(
116 | self::VALID_OBJECT,
117 | $this->presenter->invalidMappings[0]['object']
118 | );
119 | $this->assertFalse( $this->presenter->showedSaveFailed );
120 | }
121 |
122 | public function testShouldShowInvalidMappingsWhenObjectIsInvalid(): void {
123 | $useCase = new SaveMappingsUseCase(
124 | $this->presenter,
125 | $this->repository,
126 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ),
127 | new BasicEntityIdParser(),
128 | new MappingListSerializer(),
129 | $this->newSucceedingAuthorizer(),
130 | new FailingObjectValidator()
131 | );
132 |
133 | $useCase->saveMappings(
134 | 'Q3',
135 | [
136 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::INVALID_OBJECT ],
137 | ]
138 | );
139 |
140 | $this->assertFalse( $this->presenter->showedSuccess );
141 | $this->assertSame(
142 | self::VALID_PREDICATE,
143 | $this->presenter->invalidMappings[0]['predicate']
144 | );
145 | $this->assertSame(
146 | self::INVALID_OBJECT,
147 | $this->presenter->invalidMappings[0]['object']
148 | );
149 | $this->assertFalse( $this->presenter->showedSaveFailed );
150 | }
151 |
152 | public function testShouldShowSaveFailed(): void {
153 | $useCase = new SaveMappingsUseCase(
154 | $this->presenter,
155 | new ThrowingMappingRepository(),
156 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ),
157 | new BasicEntityIdParser(),
158 | new MappingListSerializer(),
159 | $this->newSucceedingAuthorizer(),
160 | new SucceedingObjectValidator()
161 | );
162 |
163 | $useCase->saveMappings(
164 | 'Q4',
165 | [
166 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ]
167 | ]
168 | );
169 |
170 | $this->assertFalse( $this->presenter->showedSuccess );
171 | $this->assertSame( [], $this->presenter->invalidMappings );
172 | $this->assertTrue( $this->presenter->showedSaveFailed );
173 | }
174 |
175 | public function testShouldShowInvalidEntityId(): void {
176 | $useCase = new SaveMappingsUseCase(
177 | $this->presenter,
178 | new ThrowingMappingRepository(),
179 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ),
180 | new BasicEntityIdParser(),
181 | new MappingListSerializer(),
182 | $this->newSucceedingAuthorizer(),
183 | new SucceedingObjectValidator()
184 | );
185 |
186 | $useCase->saveMappings(
187 | 'NotId',
188 | [
189 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ]
190 | ]
191 | );
192 |
193 | $this->assertTrue( $this->presenter->showedInvalidEntityId );
194 | }
195 |
196 | public function testShouldShowPermissionDenied(): void {
197 | $useCase = new SaveMappingsUseCase(
198 | $this->presenter,
199 | new PermissionDeniedMappingRepository(),
200 | new PredicateList( [ new Predicate( self::VALID_PREDICATE ) ] ),
201 | new BasicEntityIdParser(),
202 | new MappingListSerializer(),
203 | $this->newFailingAuthorizer(),
204 | new SucceedingObjectValidator()
205 | );
206 |
207 | $useCase->saveMappings(
208 | 'Q1',
209 | [
210 | [ 'predicate' => self::VALID_PREDICATE, 'object' => self::VALID_OBJECT ]
211 | ]
212 | );
213 |
214 | $this->assertTrue( $this->presenter->showedPermissionDenied );
215 | }
216 |
217 | }
218 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | test:
9 | name: "PHPUnit: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}"
10 |
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | include:
15 | - mw: 'REL1_43'
16 | php: 8.1
17 | experimental: false
18 | - mw: 'REL1_43'
19 | php: 8.2
20 | experimental: false
21 | - mw: 'REL1_43'
22 | php: 8.3
23 | experimental: false
24 | - mw: 'master'
25 | php: 8.4
26 | experimental: true
27 |
28 | runs-on: ubuntu-latest
29 |
30 | defaults:
31 | run:
32 | working-directory: mediawiki
33 |
34 | steps:
35 | - name: Setup PHP
36 | uses: shivammathur/setup-php@v2
37 | with:
38 | php-version: ${{ matrix.php }}
39 | extensions: mbstring, intl
40 | tools: composer
41 |
42 | - name: Cache MediaWiki
43 | id: cache-mediawiki
44 | uses: actions/cache@v4
45 | with:
46 | path: |
47 | mediawiki
48 | !mediawiki/extensions/
49 | !mediawiki/vendor/
50 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4
51 |
52 | - name: Cache Composer cache
53 | uses: actions/cache@v4
54 | with:
55 | path: ~/.composer/cache
56 | key: composer-php${{ matrix.php }}
57 |
58 | - uses: actions/checkout@v4
59 | with:
60 | path: EarlyCopy
61 |
62 | - name: Install MediaWiki
63 | if: steps.cache-mediawiki.outputs.cache-hit != 'true'
64 | working-directory: ~
65 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF
66 |
67 | - uses: actions/checkout@v4
68 | with:
69 | path: mediawiki/extensions/WikibaseRDF
70 |
71 | - name: Composer allow-plugins
72 | run: composer config --no-plugins allow-plugins.composer/installers true
73 |
74 | - run: composer update
75 |
76 | - name: Run update.php
77 | run: php maintenance/update.php --quick
78 |
79 | - name: Run PHPUnit
80 | run: php tests/phpunit/phpunit.php -c extensions/WikibaseRDF/
81 | if: matrix.mw != 'master'
82 |
83 | - name: Run PHPUnit with code coverage
84 | run: php tests/phpunit/phpunit.php -c extensions/WikibaseRDF/ --coverage-clover coverage.xml
85 | if: matrix.mw != 'master'
86 |
87 | - name: Upload code coverage
88 | run: bash <(curl -s https://codecov.io/bash)
89 | if: matrix.mw == 'master'
90 |
91 |
92 |
93 |
94 | PHPStan:
95 | name: "PHPStan"
96 |
97 | strategy:
98 | matrix:
99 | include:
100 | - mw: 'REL1_43'
101 | php: '8.3'
102 |
103 | runs-on: ubuntu-latest
104 |
105 | defaults:
106 | run:
107 | working-directory: mediawiki
108 |
109 | steps:
110 | - name: Setup PHP
111 | uses: shivammathur/setup-php@v2
112 | with:
113 | php-version: ${{ matrix.php }}
114 | extensions: mbstring
115 | tools: composer, cs2pr
116 |
117 | - name: Cache MediaWiki
118 | id: cache-mediawiki
119 | uses: actions/cache@v4
120 | with:
121 | path: |
122 | mediawiki
123 | mediawiki/extensions/
124 | mediawiki/vendor/
125 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4
126 |
127 | - name: Cache Composer cache
128 | uses: actions/cache@v4
129 | with:
130 | path: ~/.composer/cache
131 | key: composer_static_analysis
132 |
133 | - uses: actions/checkout@v4
134 | with:
135 | path: EarlyCopy
136 |
137 | - name: Install MediaWiki
138 | if: steps.cache-mediawiki.outputs.cache-hit != 'true'
139 | working-directory: ~
140 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF
141 |
142 | - uses: actions/checkout@v4
143 | with:
144 | path: mediawiki/extensions/WikibaseRDF
145 |
146 | - name: Composer allow-plugins
147 | run: composer config --no-plugins allow-plugins.composer/installers true
148 |
149 | - run: composer update
150 |
151 | - name: Composer install
152 | run: cd extensions/WikibaseRDF && composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
153 |
154 | - name: PHPStan
155 | run: cd extensions/WikibaseRDF && php vendor/bin/phpstan analyse --error-format=checkstyle --no-progress | cs2pr
156 |
157 |
158 |
159 |
160 | Psalm:
161 | name: "Psalm: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}"
162 |
163 | strategy:
164 | matrix:
165 | include:
166 | - mw: 'REL1_43'
167 | php: '8.3'
168 |
169 | runs-on: ubuntu-latest
170 |
171 | defaults:
172 | run:
173 | working-directory: mediawiki
174 |
175 | steps:
176 | - name: Setup PHP
177 | uses: shivammathur/setup-php@v2
178 | with:
179 | php-version: ${{ matrix.php }}
180 | extensions: mbstring
181 | tools: composer, cs2pr
182 |
183 | - name: Cache MediaWiki
184 | id: cache-mediawiki
185 | uses: actions/cache@v4
186 | with:
187 | path: |
188 | mediawiki
189 | mediawiki/extensions/
190 | mediawiki/vendor/
191 | key: mw_${{ matrix.mw }}-php${{ matrix.php }}_v4
192 |
193 | - name: Cache Composer cache
194 | uses: actions/cache@v4
195 | with:
196 | path: ~/.composer/cache
197 | key: composer_static_analysis
198 |
199 | - uses: actions/checkout@v4
200 | with:
201 | path: EarlyCopy
202 |
203 | - name: Install MediaWiki
204 | if: steps.cache-mediawiki.outputs.cache-hit != 'true'
205 | working-directory: ~
206 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF
207 |
208 | - uses: actions/checkout@v4
209 | with:
210 | path: mediawiki/extensions/WikibaseRDF
211 |
212 | - name: Composer allow-plugins
213 | run: composer config --no-plugins allow-plugins.composer/installers true
214 |
215 | - run: composer update
216 |
217 | - name: Composer install
218 | run: cd extensions/WikibaseRDF && composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
219 |
220 | - name: Psalm
221 | run: cd extensions/WikibaseRDF && pwd && php vendor/bin/psalm --config=psalm.xml --shepherd --stats
222 |
223 |
224 |
225 |
226 | code-style:
227 | name: "Code style: MW ${{ matrix.mw }}, PHP ${{ matrix.php }}"
228 |
229 | strategy:
230 | matrix:
231 | include:
232 | - mw: 'REL1_43'
233 | php: '8.3'
234 |
235 | runs-on: ubuntu-latest
236 |
237 | defaults:
238 | run:
239 | working-directory: mediawiki/extensions/WikibaseRDF
240 |
241 | steps:
242 | - name: Setup PHP
243 | uses: shivammathur/setup-php@v2
244 | with:
245 | php-version: ${{ matrix.php }}
246 | extensions: mbstring, intl, php-ast
247 | tools: composer
248 |
249 | - name: Cache MediaWiki
250 | id: cache-mediawiki
251 | uses: actions/cache@v4
252 | with:
253 | path: |
254 | mediawiki
255 | !mediawiki/extensions/
256 | !mediawiki/vendor/
257 | key: mw_static_analysis
258 |
259 | - name: Cache Composer cache
260 | uses: actions/cache@v4
261 | with:
262 | path: ~/.composer/cache
263 | key: composer_static_analysis
264 |
265 | - uses: actions/checkout@v4
266 | with:
267 | path: EarlyCopy
268 |
269 | - name: Install MediaWiki
270 | if: steps.cache-mediawiki.outputs.cache-hit != 'true'
271 | working-directory: ~
272 | run: bash EarlyCopy/.github/workflows/installMediaWiki.sh ${{ matrix.mw }} WikibaseRDF
273 |
274 | - uses: actions/checkout@v4
275 | with:
276 | path: mediawiki/extensions/WikibaseRDF
277 |
278 | - name: Composer install
279 | run: composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader
280 |
281 | - run: vendor/bin/phpcs -p -s
282 |
283 | linting:
284 | name: "Linting"
285 |
286 | runs-on: ubuntu-latest
287 |
288 | steps:
289 | - uses: actions/checkout@v4
290 | - uses: actions/setup-node@v4
291 | with:
292 | node-version: 22
293 | - run: |
294 | echo "::remove-matcher owner=eslint-compact::"
295 | echo "::remove-matcher owner=eslint-stylish::"
296 | - run: npm install
297 | - run: npm run test
298 |
--------------------------------------------------------------------------------