├── COPYING ├── README.md ├── RELEASE-NOTES.md └── src ├── ByPropertyIdArray.php ├── Entity ├── BasicEntityIdParser.php ├── ClearableEntity.php ├── DispatchingEntityIdParser.php ├── EntityDocument.php ├── EntityId.php ├── EntityIdParser.php ├── EntityIdParsingException.php ├── EntityIdValue.php ├── EntityRedirect.php ├── Int32EntityId.php ├── Item.php ├── ItemId.php ├── ItemIdParser.php ├── ItemIdSet.php ├── NumericPropertyId.php ├── Property.php ├── PropertyId.php ├── SerializableEntityId.php └── StatementListProvidingEntity.php ├── Exception ├── PropertyChangedException.php ├── StatementGuidChangedException.php └── StatementNotFoundException.php ├── Internal └── MapValueHasher.php ├── LegacyIdInterpreter.php ├── PropertyIdProvider.php ├── Reference.php ├── ReferenceList.php ├── SiteLink.php ├── SiteLinkList.php ├── Snak ├── DerivedPropertyValueSnak.php ├── PropertyNoValueSnak.php ├── PropertySomeValueSnak.php ├── PropertyValueSnak.php ├── Snak.php ├── SnakList.php ├── SnakObject.php ├── SnakRole.php └── TypedSnak.php ├── Statement ├── ReferencedStatementFilter.php ├── Statement.php ├── StatementByGuidMap.php ├── StatementFilter.php ├── StatementGuid.php ├── StatementList.php ├── StatementListHolder.php └── StatementListProvider.php └── Term ├── AliasGroup.php ├── AliasGroupFallback.php ├── AliasGroupList.php ├── AliasesProvider.php ├── DescriptionsProvider.php ├── Fingerprint.php ├── FingerprintProvider.php ├── LabelsProvider.php ├── Term.php ├── TermFallback.php ├── TermList.php └── TermTypes.php /COPYING: -------------------------------------------------------------------------------- 1 | The license text below "----" applies to all files within this distribution, other 2 | than those that are in a directory which contains files named "LICENSE" or 3 | "COPYING", or a subdirectory thereof. For those files, the license text contained in 4 | said file overrides any license information contained in directories of smaller depth. 5 | Alternative licenses are typically used for software that is provided by external 6 | parties, and merely packaged with the Wikibase release for convenience. 7 | ---- 8 | 9 | GNU GENERAL PUBLIC LICENSE 10 | Version 2, June 1991 11 | 12 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 13 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 14 | Everyone is permitted to copy and distribute verbatim copies 15 | of this license document, but changing it is not allowed. 16 | 17 | Preamble 18 | 19 | The licenses for most software are designed to take away your 20 | freedom to share and change it. By contrast, the GNU General Public 21 | License is intended to guarantee your freedom to share and change free 22 | software--to make sure the software is free for all its users. This 23 | General Public License applies to most of the Free Software 24 | Foundation's software and to any other program whose authors commit to 25 | using it. (Some other Free Software Foundation software is covered by 26 | the GNU Lesser General Public License instead.) You can apply it to 27 | your programs, too. 28 | 29 | When we speak of free software, we are referring to freedom, not 30 | price. Our General Public Licenses are designed to make sure that you 31 | have the freedom to distribute copies of free software (and charge for 32 | this service if you wish), that you receive source code or can get it 33 | if you want it, that you can change the software or use pieces of it 34 | in new free programs; and that you know you can do these things. 35 | 36 | To protect your rights, we need to make restrictions that forbid 37 | anyone to deny you these rights or to ask you to surrender the rights. 38 | These restrictions translate to certain responsibilities for you if you 39 | distribute copies of the software, or if you modify it. 40 | 41 | For example, if you distribute copies of such a program, whether 42 | gratis or for a fee, you must give the recipients all the rights that 43 | you have. You must make sure that they, too, receive or can get the 44 | source code. And you must show them these terms so they know their 45 | rights. 46 | 47 | We protect your rights with two steps: (1) copyright the software, and 48 | (2) offer you this license which gives you legal permission to copy, 49 | distribute and/or modify the software. 50 | 51 | Also, for each author's protection and ours, we want to make certain 52 | that everyone understands that there is no warranty for this free 53 | software. If the software is modified by someone else and passed on, we 54 | want its recipients to know that what they have is not the original, so 55 | that any problems introduced by others will not reflect on the original 56 | authors' reputations. 57 | 58 | Finally, any free program is threatened constantly by software 59 | patents. We wish to avoid the danger that redistributors of a free 60 | program will individually obtain patent licenses, in effect making the 61 | program proprietary. To prevent this, we have made it clear that any 62 | patent must be licensed for everyone's free use or not licensed at all. 63 | 64 | The precise terms and conditions for copying, distribution and 65 | modification follow. 66 | 67 | GNU GENERAL PUBLIC LICENSE 68 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 69 | 70 | 0. This License applies to any program or other work which contains 71 | a notice placed by the copyright holder saying it may be distributed 72 | under the terms of this General Public License. The "Program", below, 73 | refers to any such program or work, and a "work based on the Program" 74 | means either the Program or any derivative work under copyright law: 75 | that is to say, a work containing the Program or a portion of it, 76 | either verbatim or with modifications and/or translated into another 77 | language. (Hereinafter, translation is included without limitation in 78 | the term "modification".) Each licensee is addressed as "you". 79 | 80 | Activities other than copying, distribution and modification are not 81 | covered by this License; they are outside its scope. The act of 82 | running the Program is not restricted, and the output from the Program 83 | is covered only if its contents constitute a work based on the 84 | Program (independent of having been made by running the Program). 85 | Whether that is true depends on what the Program does. 86 | 87 | 1. You may copy and distribute verbatim copies of the Program's 88 | source code as you receive it, in any medium, provided that you 89 | conspicuously and appropriately publish on each copy an appropriate 90 | copyright notice and disclaimer of warranty; keep intact all the 91 | notices that refer to this License and to the absence of any warranty; 92 | and give any other recipients of the Program a copy of this License 93 | along with the Program. 94 | 95 | You may charge a fee for the physical act of transferring a copy, and 96 | you may at your option offer warranty protection in exchange for a fee. 97 | 98 | 2. You may modify your copy or copies of the Program or any portion 99 | of it, thus forming a work based on the Program, and copy and 100 | distribute such modifications or work under the terms of Section 1 101 | above, provided that you also meet all of these conditions: 102 | 103 | a) You must cause the modified files to carry prominent notices 104 | stating that you changed the files and the date of any change. 105 | 106 | b) You must cause any work that you distribute or publish, that in 107 | whole or in part contains or is derived from the Program or any 108 | part thereof, to be licensed as a whole at no charge to all third 109 | parties under the terms of this License. 110 | 111 | c) If the modified program normally reads commands interactively 112 | when run, you must cause it, when started running for such 113 | interactive use in the most ordinary way, to print or display an 114 | announcement including an appropriate copyright notice and a 115 | notice that there is no warranty (or else, saying that you provide 116 | a warranty) and that users may redistribute the program under 117 | these conditions, and telling the user how to view a copy of this 118 | License. (Exception: if the Program itself is interactive but 119 | does not normally print such an announcement, your work based on 120 | the Program is not required to print an announcement.) 121 | 122 | These requirements apply to the modified work as a whole. If 123 | identifiable sections of that work are not derived from the Program, 124 | and can be reasonably considered independent and separate works in 125 | themselves, then this License, and its terms, do not apply to those 126 | sections when you distribute them as separate works. But when you 127 | distribute the same sections as part of a whole which is a work based 128 | on the Program, the distribution of the whole must be on the terms of 129 | this License, whose permissions for other licensees extend to the 130 | entire whole, and thus to each and every part regardless of who wrote it. 131 | 132 | Thus, it is not the intent of this section to claim rights or contest 133 | your rights to work written entirely by you; rather, the intent is to 134 | exercise the right to control the distribution of derivative or 135 | collective works based on the Program. 136 | 137 | In addition, mere aggregation of another work not based on the Program 138 | with the Program (or with a work based on the Program) on a volume of 139 | a storage or distribution medium does not bring the other work under 140 | the scope of this License. 141 | 142 | 3. You may copy and distribute the Program (or a work based on it, 143 | under Section 2) in object code or executable form under the terms of 144 | Sections 1 and 2 above provided that you also do one of the following: 145 | 146 | a) Accompany it with the complete corresponding machine-readable 147 | source code, which must be distributed under the terms of Sections 148 | 1 and 2 above on a medium customarily used for software interchange; or, 149 | 150 | b) Accompany it with a written offer, valid for at least three 151 | years, to give any third party, for a charge no more than your 152 | cost of physically performing source distribution, a complete 153 | machine-readable copy of the corresponding source code, to be 154 | distributed under the terms of Sections 1 and 2 above on a medium 155 | customarily used for software interchange; or, 156 | 157 | c) Accompany it with the information you received as to the offer 158 | to distribute corresponding source code. (This alternative is 159 | allowed only for noncommercial distribution and only if you 160 | received the program in object code or executable form with such 161 | an offer, in accord with Subsection b above.) 162 | 163 | The source code for a work means the preferred form of the work for 164 | making modifications to it. For an executable work, complete source 165 | code means all the source code for all modules it contains, plus any 166 | associated interface definition files, plus the scripts used to 167 | control compilation and installation of the executable. However, as a 168 | special exception, the source code distributed need not include 169 | anything that is normally distributed (in either source or binary 170 | form) with the major components (compiler, kernel, and so on) of the 171 | operating system on which the executable runs, unless that component 172 | itself accompanies the executable. 173 | 174 | If distribution of executable or object code is made by offering 175 | access to copy from a designated place, then offering equivalent 176 | access to copy the source code from the same place counts as 177 | distribution of the source code, even though third parties are not 178 | compelled to copy the source along with the object code. 179 | 180 | 4. You may not copy, modify, sublicense, or distribute the Program 181 | except as expressly provided under this License. Any attempt 182 | otherwise to copy, modify, sublicense or distribute the Program is 183 | void, and will automatically terminate your rights under this License. 184 | However, parties who have received copies, or rights, from you under 185 | this License will not have their licenses terminated so long as such 186 | parties remain in full compliance. 187 | 188 | 5. You are not required to accept this License, since you have not 189 | signed it. However, nothing else grants you permission to modify or 190 | distribute the Program or its derivative works. These actions are 191 | prohibited by law if you do not accept this License. Therefore, by 192 | modifying or distributing the Program (or any work based on the 193 | Program), you indicate your acceptance of this License to do so, and 194 | all its terms and conditions for copying, distributing or modifying 195 | the Program or works based on it. 196 | 197 | 6. Each time you redistribute the Program (or any work based on the 198 | Program), the recipient automatically receives a license from the 199 | original licensor to copy, distribute or modify the Program subject to 200 | these terms and conditions. You may not impose any further 201 | restrictions on the recipients' exercise of the rights granted herein. 202 | You are not responsible for enforcing compliance by third parties to 203 | this License. 204 | 205 | 7. If, as a consequence of a court judgment or allegation of patent 206 | infringement or for any other reason (not limited to patent issues), 207 | conditions are imposed on you (whether by court order, agreement or 208 | otherwise) that contradict the conditions of this License, they do not 209 | excuse you from the conditions of this License. If you cannot 210 | distribute so as to satisfy simultaneously your obligations under this 211 | License and any other pertinent obligations, then as a consequence you 212 | may not distribute the Program at all. For example, if a patent 213 | license would not permit royalty-free redistribution of the Program by 214 | all those who receive copies directly or indirectly through you, then 215 | the only way you could satisfy both it and this License would be to 216 | refrain entirely from distribution of the Program. 217 | 218 | If any portion of this section is held invalid or unenforceable under 219 | any particular circumstance, the balance of the section is intended to 220 | apply and the section as a whole is intended to apply in other 221 | circumstances. 222 | 223 | It is not the purpose of this section to induce you to infringe any 224 | patents or other property right claims or to contest validity of any 225 | such claims; this section has the sole purpose of protecting the 226 | integrity of the free software distribution system, which is 227 | implemented by public license practices. Many people have made 228 | generous contributions to the wide range of software distributed 229 | through that system in reliance on consistent application of that 230 | system; it is up to the author/donor to decide if he or she is willing 231 | to distribute software through any other system and a licensee cannot 232 | impose that choice. 233 | 234 | This section is intended to make thoroughly clear what is believed to 235 | be a consequence of the rest of this License. 236 | 237 | 8. If the distribution and/or use of the Program is restricted in 238 | certain countries either by patents or by copyrighted interfaces, the 239 | original copyright holder who places the Program under this License 240 | may add an explicit geographical distribution limitation excluding 241 | those countries, so that distribution is permitted only in or among 242 | countries not thus excluded. In such case, this License incorporates 243 | the limitation as if written in the body of this License. 244 | 245 | 9. The Free Software Foundation may publish revised and/or new versions 246 | of the General Public License from time to time. Such new versions will 247 | be similar in spirit to the present version, but may differ in detail to 248 | address new problems or concerns. 249 | 250 | Each version is given a distinguishing version number. If the Program 251 | specifies a version number of this License which applies to it and "any 252 | later version", you have the option of following the terms and conditions 253 | either of that version or of any later version published by the Free 254 | Software Foundation. If the Program does not specify a version number of 255 | this License, you may choose any version ever published by the Free Software 256 | Foundation. 257 | 258 | 10. If you wish to incorporate parts of the Program into other free 259 | programs whose distribution conditions are different, write to the author 260 | to ask for permission. For software which is copyrighted by the Free 261 | Software Foundation, write to the Free Software Foundation; we sometimes 262 | make exceptions for this. Our decision will be guided by the two goals 263 | of preserving the free status of all derivatives of our free software and 264 | of promoting the sharing and reuse of software generally. 265 | 266 | NO WARRANTY 267 | 268 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 269 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 270 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 271 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 272 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 273 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 274 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 275 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 276 | REPAIR OR CORRECTION. 277 | 278 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 279 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 280 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 281 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 282 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 283 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 284 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 285 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 286 | POSSIBILITY OF SUCH DAMAGES. 287 | 288 | END OF TERMS AND CONDITIONS 289 | 290 | How to Apply These Terms to Your New Programs 291 | 292 | If you develop a new program, and you want it to be of the greatest 293 | possible use to the public, the best way to achieve this is to make it 294 | free software which everyone can redistribute and change under these terms. 295 | 296 | To do so, attach the following notices to the program. It is safest 297 | to attach them to the start of each source file to most effectively 298 | convey the exclusion of warranty; and each file should have at least 299 | the "copyright" line and a pointer to where the full notice is found. 300 | 301 | 302 | Copyright (C) 303 | 304 | This program is free software; you can redistribute it and/or modify 305 | it under the terms of the GNU General Public License as published by 306 | the Free Software Foundation; either version 2 of the License, or 307 | (at your option) any later version. 308 | 309 | This program is distributed in the hope that it will be useful, 310 | but WITHOUT ANY WARRANTY; without even the implied warranty of 311 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 312 | GNU General Public License for more details. 313 | 314 | You should have received a copy of the GNU General Public License along 315 | with this program; if not, write to the Free Software Foundation, Inc., 316 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 317 | 318 | Also add information on how to contact you by electronic and paper mail. 319 | 320 | If the program is interactive, make it output a short notice like this 321 | when it starts in an interactive mode: 322 | 323 | Gnomovision version 69, Copyright (C) year name of author 324 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 325 | This is free software, and you are welcome to redistribute it 326 | under certain conditions; type `show c' for details. 327 | 328 | The hypothetical commands `show w' and `show c' should show the appropriate 329 | parts of the General Public License. Of course, the commands you use may 330 | be called something other than `show w' and `show c'; they could even be 331 | mouse-clicks or menu items--whatever suits your program. 332 | 333 | You should also get your employer (if you work as a programmer) or your 334 | school, if any, to sign a "copyright disclaimer" for the program, if 335 | necessary. Here is a sample; alter the names: 336 | 337 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 338 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 339 | 340 | , 1 April 1989 341 | Ty Coon, President of Vice 342 | 343 | This General Public License does not permit incorporating your program into 344 | proprietary programs. If your program is a subroutine library, you may 345 | consider it more useful to permit linking proprietary applications with the 346 | library. If this is what you want to do, use the GNU Lesser General 347 | Public License instead of this License. 348 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wikibase DataModel 2 | 3 | [![Build Status](https://github.com/wmde/WikibaseDataModel/actions/workflows/lint-and-test.yaml/badge.svg?branch=master)](https://github.com/wmde/WikibaseDataModel/actions/workflows/lint-and-test.yaml) 4 | [![Download count](https://poser.pugx.org/wikibase/data-model/d/total.png)](https://packagist.org/packages/wikibase/data-model) 5 | [![License](https://poser.pugx.org/wikibase/data-model/license.svg)](https://packagist.org/packages/wikibase/data-model) 6 | 7 | [![Latest Stable Version](https://poser.pugx.org/wikibase/data-model/version.png)](https://packagist.org/packages/wikibase/data-model) 8 | [![Latest Unstable Version](https://poser.pugx.org/wikibase/data-model/v/unstable.svg)](//packagist.org/packages/wikibase/data-model) 9 | 10 | **Wikibase DataModel** is the canonical PHP implementation of the 11 | [Data Model](https://www.mediawiki.org/wiki/Wikibase/DataModel) 12 | at the heart of the [Wikibase software](http://wikiba.se/). 13 | 14 | It is primarily used by the Wikibase MediaWiki extensions, though 15 | has no dependencies whatsoever on these or on MediaWiki itself. 16 | 17 | Recent changes can be found in the [release notes](RELEASE-NOTES.md). 18 | 19 | Note that this repository is a mirror of part of the upstream [Wikibase project](https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/Wikibase/+/refs/heads/master/lib/packages/wikibase/data-model/) on Gerrit. 20 | Contributions should be made to the directories there using MediaWiki's [Gerrit process](https://www.mediawiki.org/wiki/Gerrit). 21 | 22 | ## Installation 23 | 24 | You can use [Composer](http://getcomposer.org/) to download and install 25 | this package as well as its dependencies. Alternatively you can simply clone 26 | the git repository and take care of loading yourself. 27 | 28 | ### Composer 29 | 30 | To add this package as a local, per-project dependency to your project, simply add a 31 | dependency on `wikibase/data-model` to your project's `composer.json` file. 32 | Here is a minimal example of a `composer.json` file that just defines a dependency on 33 | Wikibase DataModel 9.x: 34 | 35 | ```js 36 | { 37 | "require": { 38 | "wikibase/data-model": "~9.0" 39 | } 40 | } 41 | ``` 42 | 43 | ### Manual 44 | 45 | Get the Wikibase DataModel code, either via git, or some other means. Also get all dependencies. 46 | You can find a list of the dependencies in the "require" section of the composer.json file. 47 | The "autoload" section of this file specifies how to load the resources provide by this library. 48 | 49 | ## Library contents 50 | 51 | This library contains domain objects that implement the concepts part of the 52 | [Wikibase DataModel](https://www.mediawiki.org/wiki/Wikibase/DataModel). 53 | This mainly includes simple value objects, though also contains core domain 54 | logic either bound to such objects or encapsulated as service objects. 55 | 56 | ## Tests 57 | 58 | This library comes with a set up PHPUnit tests that cover all non-trivial code. Additionally, code 59 | style checks by PHPCS and PHPMD are supported. The configuration for all 3 these tools can be found 60 | in the root directory. You can use the tools in their standard manner, though can run all checks 61 | required by our CI by executing `composer ci`. To just run tests use `composer test`, and to just 62 | run style checks use `composer cs`. 63 | 64 | ## Contributing 65 | 66 | This repository is a mirror of part of the upstream [Wikibase project](https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/Wikibase/+/refs/heads/master/lib/packages/wikibase/data-model/) on Gerrit. 67 | Contributions should be made to the directories there using MediaWiki's [Gerrit process](https://www.mediawiki.org/wiki/Gerrit). 68 | 69 | ## Credits 70 | 71 | ### Development 72 | 73 | Wikibase DataModel has been written by [Jeroen De Dauw](https://www.EntropyWins.wtf) 74 | and Thiemo Kreuz as [Wikimedia Germany](https://wikimedia.de) employees for the [Wikidata project](https://wikidata.org/). 75 | 76 | Contributions were also made by [several other people](https://www.ohloh.net/p/wikibasedatamodel/contributors?sort=commits). 77 | 78 | ### Concept 79 | 80 | The initial [conceptual specification](https://www.mediawiki.org/wiki/Wikibase/DataModel) 81 | for the DataModel was created by [Markus Krötzsch](http://korrekt.org/) 82 | and [Denny Vrandečić](http://simia.net/wiki/Denny), with minor contributions by 83 | Daniel Kinzler and Jeroen De Dauw. 84 | 85 | ## Links 86 | 87 | * [Wikibase DataModel on Packagist](https://packagist.org/packages/wikibase/data-model) 88 | * [Wikibase DataModel on Ohloh](https://www.ohloh.net/p/wikibasedatamodel/) 89 | * [Wikibase DataModel on TravisCI](https://travis-ci.org/wmde/WikibaseDataModel) 90 | * [Wikibase DataModel on Wikimedia's Phabricator](https://phabricator.wikimedia.org/project/view/920/) 91 | 92 | ## See also 93 | 94 | * [Blog posts on Wikibase DataModel](http://www.bn2vs.com/blog/tag/wikibase-datamodel/) 95 | * [Wikibase DataModel Services](https://github.com/wmde/WikibaseDataModelServices) 96 | * [Wikibase DataModel Serialization](https://github.com/wmde/WikibaseDataModelSerialization) 97 | * [Wikibase Internal Serialization](https://github.com/wmde/WikibaseInternalSerialization) 98 | -------------------------------------------------------------------------------- /RELEASE-NOTES.md: -------------------------------------------------------------------------------- 1 | # Wikibase DataModel release notes 2 | 3 | ## Version 10.0.0 (TBD) 4 | 5 | * Removed support for calling `Statement::addNewReference()` and `StatementList` constructor with a 6 | single array argument, which was deprecated in `Version 9.6.0 (2021-03-31)`. These should now be 7 | called with a variadic argument list. 8 | * Added `__serialize()` and `__unserialize()` methods to the `EntityId` interface, 9 | and removed the `serialize()` and `unserialize()` methods and the `Serializable` interface. 10 | * Added native type hints to the `Statement` and `StatementList` classes 11 | * Added `strict_types=1` to `Statement.php`, `StatementList.php`, and related test files 12 | * Removed support for repository names in entity IDs (e.g. `foo:Q1234`) 13 | 14 | ## Version 9.6.1 (2021-04-01) 15 | 16 | * `Snak` now declares `getHash()` and `equals()` methods again, 17 | which it used to inherit from the `Hashable` and `Immutable` interfaces prior to version 9.6.0. 18 | (The methods were never removed from any specific classes, 19 | but since `Snak` is an interface, Phan started complaining that the methods were unknown.) 20 | 21 | ## Version 9.6.0 (2021-03-31) 22 | 23 | * `ReferenceList::addNewReference()`, `Statement::addNewReference()` and the `StatementList` constructor 24 | supported being called with a variadic argument list, with a single array argument, 25 | or (in the case of `StatementList`) with a single `Traversable` argument. 26 | The latter two forms are now deprecated (though they still work); 27 | please update your code: 28 | for instance, change `->addNewReference( [ $x, $y ] )` to `->addNewReference( $x, $y )`, 29 | and `->addNewReference( $snaks )` to `->addNewReference( ...$snaks )`. 30 | * `Statement`, `Reference`, `SnakList` and `Snak` no longer implement the `Hashable` and `Immutable` interfaces from `DataValues/DataValues`. 31 | * Removed usages of the `Comparable` interface 32 | * Made the library installable together with DataValues 3.x 33 | 34 | ## Version 9.5.1 (2020-06-03) 35 | 36 | * Updated release notes 37 | 38 | ## Version 9.5.0 (2020-06-02) 39 | 40 | * Added PHP 7.4 support 41 | 42 | ## Version 9.4.0 (2020-04-03) 43 | 44 | * Added `getGuidPart` to `StatementGuid` 45 | 46 | ## Version 9.3.0 (2020-03-10) 47 | 48 | * Raised minimum PHP version to 7.1 49 | * Added `TermTypes` with term type constants 50 | * Allow installing with wikimedia/assert 0.5.0 51 | 52 | ## Version 9.2.0 (2020-01-24) 53 | 54 | * `TermList` now throws `InvalidArgumentException` when given non-iterable rather than failing silently 55 | * `SiteLinkList` now throws `InvalidArgumentException` when given non-iterable rather than failing silently 56 | * Slightly optimized `EntityId::isForeign` 57 | 58 | ## Version 9.1.0 (2019-01-24) 59 | 60 | * Raised minimum PHP version to 7.0 or HHVM 61 | * Redirecting an entity to itself now causes an exception 62 | 63 | ## Version 9.0.1 (2018-11-09) 64 | 65 | * `Item` and `Property` now implement `ClearableEntity` again 66 | 67 | ## Version 9.0.0 (2018-11-01) 68 | 69 | * Breaking change: `EntityDocument` no longer extends `ClearableEntity` (8.0.0 revert) 70 | * The `TermList` constructor now takes any `iterable` instead of just `array` 71 | * The `SiteLinkList` constructor now takes any `iterable` instead of just `array` 72 | * Added `TermList::addAll` 73 | 74 | ## Version 8.0.0 (2018-08-03) 75 | 76 | #### Breaking changes 77 | 78 | * `Item::setId` and `Property::setId` no longer accept integers 79 | * Removed `Item::getSiteLinks` and `Item::hasSiteLinks` 80 | * Removed `HashArray` 81 | * `SnakList` no longer extends `HashArray` and no longer has these public and protected methods: 82 | * `addElement` 83 | * `getByElementHash` 84 | * `getNewOffset` 85 | * `getObjectType` 86 | * `hasElement` 87 | * `hasElementHash` 88 | * `hasValidType` 89 | * `preSetElement` 90 | * `removeByElementHash` 91 | * `removeElement` 92 | * `setElement` 93 | * Removed `WIKIBASE_DATAMODEL_VERSION` constant 94 | * Added periods to the list of disallowed characters in `RepositoryNameAssert` 95 | * `EntityDocument` now extends `ClearableEntity` 96 | 97 | #### Other changes 98 | 99 | * Added `StatementListProvidingEntity` 100 | * Un-deprecated several sitelink related shortcuts from `Item`: 101 | * `addSiteLink` 102 | * `getSiteLink` 103 | * `hasLinkToSite` 104 | * `removeSiteLink` 105 | * Installation together with DataValues 2.x is now supported 106 | 107 | ## Version 7.5.0 (2018-05-02) 108 | 109 | * Introduce `ClearableEntity` interface. 110 | 111 | ## Version 7.4.1 (2018-05-02) 112 | 113 | * Removed `clear` from `EntityDocument`. This was a compatibility break of the interface. 114 | 115 | ## Version 7.4.0 (2018-05-02) 116 | 117 | * Added `clear` to `EntityDocument` 118 | 119 | ## Version 7.3.0 (2017-11-13) 120 | 121 | * Performance optimizations on `EntityId`: 122 | * Added protected `$repositoryName` and `$localPart` properties 123 | * Added protected `extractRepositoryNameAndLocalPart` 124 | 125 | ## Version 7.2.0 (2017-10-23) 126 | 127 | * Performance optimizations on methods critical for dump generation: 128 | * `DispatchingEntityIdParser::parse` 129 | * `SnakList::orderByProperty` 130 | 131 | ## Version 7.1.0 (2017-09-01) 132 | 133 | * Changed `EntityIdValue::getArrayValue` to allow it handle foreign entity IDs and entity IDs that 134 | do not have a numeric representation. 135 | * Fixed exception handling in `EntityIdValue` not always forwarding the full stack trace. 136 | * Deprecated `EntityIdValue::newFromArray` 137 | * Deprecated `StatementGuid::getSerialization` 138 | * Improved documentation of `EntityDocument::isEmpty` 139 | * Removed MediaWiki integration files 140 | 141 | ## Version 7.0.0 (2017-03-15) 142 | 143 | This release adds support for custom entity types to `EntityIdValue`, and thus changes the hashes of 144 | snaks, qualifiers, and references. 145 | 146 | * Changed the internal `serialize()` format of several `EntityId` related classes. In all cases 147 | `unserialize()` still supports the previous format. 148 | * Serialization of `SnakObject` (includes `PropertyNoValueSnak` and `PropertySomeValueSnak`) 149 | does not use numeric IDs any more 150 | * Serialization of `PropertyValueSnak` (includes `DerivedPropertyValueSnak`) does not use 151 | numeric IDs any more 152 | * Serialization of `EntityIdValue` does not use numeric IDs any more 153 | * `EntityIdValue` can now serialize and unserialize `EntityId`s other than `ItemId` and 154 | `PropertyId` 155 | * Minimized serialization of `ItemId` and `PropertyId` to not include the entity type any more 156 | 157 | #### Other breaking changes 158 | 159 | * Removed `FingerprintHolder`. Use `TermList::clear` and `AliasGroupList::clear` instead. `Item` and 160 | `Property` also still implement `setFingerprint`. 161 | * Removed class aliases deprecated since 3.0: 162 | * `Wikibase\DataModel\Claim\Claim` 163 | * `Wikibase\DataModel\Claim\ClaimGuid` 164 | * `Wikibase\DataModel\StatementListProvider` 165 | * Added a `SnakList` constructor that is not compatible with the `ArrayList` constructor any more, 166 | and does not accept null any more. 167 | * Removed `HashArray::equals`, and `HashArray` does not implement `Comparable` any more 168 | * Removed `HashArray::getHash`, and `HashArray` does not implement `Hashable` any more 169 | * Removed `HashArray::rebuildIndices` 170 | * Removed `HashArray::indicesAreUpToDate` 171 | * Removed `HashArray::removeDuplicates` 172 | * Removed `$acceptDuplicates` feature from `HashArray` 173 | 174 | #### Additions 175 | 176 | * Added `clear` to `TermList`, `AliasGroupList` and `StatementList` 177 | * Added `newFromRepositoryAndNumber` to `ItemId` and `PropertyId` 178 | 179 | #### Other changes 180 | 181 | * Fixed `ReferenceList::addReference` sometimes moving existing references around 182 | * Fixed exceptions in `DispatchingEntityIdParser` and `ItemIdParser` not forwarding the previous 183 | exception 184 | 185 | ## Version 6.3.1 (2016-11-30) 186 | 187 | * `ItemId::getNumericId` and `PropertyId::getNumericId` no longer throw exceptions for foreign IDs 188 | 189 | ## Version 6.3.0 (2016-11-03) 190 | 191 | * Added `RepositoryNameAssert` class 192 | 193 | ## Version 6.2.0 (2016-10-14) 194 | 195 | * Raised minimum PHP version to 5.5 196 | * Added basic support for foreign EntityIds 197 | * Added `isForeign`, `getRepository` and `getLocalPart` to `EntityId` 198 | * The constructor of `EntityId` was made public 199 | * Added static `EntityId::splitSerialization` and `EntityId::joinSerialization` 200 | * `getNumericId` throws an exception for foreign EntityIds 201 | * Added documentation for foreign EntityIds 202 | 203 | ## Version 6.1.0 (2016-07-15) 204 | 205 | * Added optional index parameter to `Statement::addStatement`. 206 | * Added `Int32EntityId` interface. 207 | * `ItemId` and `PropertyId` now implement `Int32EntityId`. 208 | * `ItemId` and `PropertyId` construction now fails for numbers larger than 2147483647. 209 | * Added an `id` element containing the full ID string to the `EntityIdValue::getArrayValue` 210 | serialization. 211 | * Fixed `ByPropertyIdArray` iterating the properties of non-traversable objects. 212 | 213 | ## Version 6.0.1 (2016-04-25) 214 | 215 | * Fixed `ItemId` and `PropertyId` not rejecting strings with a newline at the end. 216 | 217 | ## Version 6.0.0 (2016-03-10) 218 | 219 | This release removes the long deprecated Entity base class in favor of much more narrow interfaces. 220 | 221 | #### Breaking changes 222 | 223 | * Removed `Entity` class (deprecated since 1.0) 224 | * `Item` and `Property` no longer extend `Entity` 225 | * Removed `getLabel`, `getDescription`, `getAliases`, `getAllAliases`, 226 | `setLabels`, `setDescriptions`, `addAliases`, `setAllAliases`, 227 | `removeLabel`, `removeDescription` and `removeAliases` methods 228 | * `Item::getLabels` and `Property::getLabels` now return a `TermList` 229 | * `Item::getDescriptions` and `Property::getDescriptions` now return a `TermList` 230 | * Removed `clear` methods from `Item` and `Property` 231 | * `StatementListProvider`, `LabelsProvider`, `DescriptionsProvider`, `AliasesProvider` and 232 | `FingerprintProvider` now give the guarantee to return objects by reference 233 | * `TermList` and `AliasGroupList` no longer throw an `InvalidArgumentException` for invalid language codes. 234 | * `getByLanguage` throws an `OutOfBoundsException` instead. 235 | * `removeByLanguage` does nothing for invalid values. 236 | * `hasTermForLanguage` and `hasGroupForLanguage` return false instead. 237 | 238 | #### Additions 239 | 240 | * `Item` and `Property` now implement `LabelsProvider`, `DescriptionsProvider` and `AliasesProvider` 241 | * Added `Item::getAliasGroups` and `Property::getAliasGroups` 242 | 243 | ## Version 5.1.0 (2016-03-08) 244 | 245 | This release significantly reduces the memory footprint when entities are cloned. 246 | 247 | * `Item::copy` and `Property::copy` do not clone immutable objects any more 248 | * Deprecated `FingerprintHolder` and `StatementListHolder` 249 | 250 | ## Version 5.0.2 (2016-02-23) 251 | 252 | * Fixed regression in `ReferenceList::addReference` and the constructor possibly adding too many objects 253 | 254 | ## Version 5.0.1 (2016-02-18) 255 | 256 | * Fixed regression in `ReferenceList::removeReferenceHash` possibly removing too many objects 257 | * `ReferenceList::unserialize` no longer calls the constructor 258 | 259 | ## Version 5.0.0 (2016-02-15) 260 | 261 | This release removes the last remaining mentions of claims. Claims are still a concept in the mental 262 | data model, but not modelled in code any more. 263 | 264 | * Removed `Claims` class (deprecated since 1.0) 265 | * Removed `getClaims` and `setClaims` methods from `Entity`, `Item` and `Property` (deprecated since 1.0) 266 | * Removed `HashableObjectStorage` class (deprecated since 4.4) 267 | * `ReferenceList` no longer derives from `SplObjectStorage` 268 | * Removed `addAll`, `attach`, `contains`, `detach`, `getHash`, `getInfo`, `removeAll`, 269 | `removeAllExcept` and `setInfo` methods 270 | * `ReferenceList` no longer implements `ArrayAccess` 271 | * Removed `offsetExists`, `offsetGet`, `offsetSet` and `offsetUnset` methods 272 | * `ReferenceList` no longer implements `Iterator` 273 | * Removed `current`, `key`, `next`, `rewind` and `valid` methods 274 | * `ReferenceList` now implements `IteratorAggregate` 275 | * Added `getIterator` method 276 | * Removed `ReferenceList::removeDuplicates` 277 | * `ReferenceList::addReference` now throws an `InvalidArgumentException` for negative indices 278 | * Added `EntityDocument::equals`, and `EntityDocument` now implements `Comparable` 279 | * Added `EntityDocument::copy` 280 | * Fixed `Property::clear` not clearing statements 281 | * `TermList` now skips and removes empty terms 282 | * Deprecated `ByPropertyIdArray` 283 | 284 | ## Version 4.4.0 (2016-01-20) 285 | 286 | * Added `ItemIdParser` 287 | * Added `ReferenceList::isEmpty` 288 | * Added `ReferencedStatementFilter::FILTER_TYPE` constant 289 | * Added `EntityRedirect::__toString` 290 | * Deprecated `HashableObjectStorage` 291 | * `SnakRole` enum is not an interface any more but a private class 292 | 293 | ## Version 4.3.0 (2015-09-02) 294 | 295 | * Added `isEmpty` to `EntityDocument` 296 | 297 | ## Version 4.2.0 (2015-08-26) 298 | 299 | * Added `EntityRedirect` 300 | * Added `EntityIdParser` and `EntityIdParsingException` 301 | * Added `BasicEntityIdParser` 302 | * Added `DispatchingEntityIdParser` 303 | * Removed no longer needed dependency on `diff/diff` 304 | 305 | ## Version 4.1.0 (2015-08-04) 306 | 307 | * Added `StatementList::filter` 308 | * Added `StatementFilter` and `ReferencedStatementFilter` 309 | * Added `LabelsProvider`, `DescriptionsProvider` and `AliasesProvider` 310 | * Added `FingerprintHolder` 311 | 312 | ## Version 4.0.0 (2015-07-28) 313 | 314 | #### Breaking changes 315 | 316 | The services that resided in this component have been moved to the new 317 | Wikibase DataModel Services library. These symbols have been removed: 318 | 319 | * `Entity::getDiff` and `Entity::patch` 320 | * `EntityIdParser` and derivatives 321 | * `EntityDiffer` and associated services 322 | * `EntityPatcher` and associated services 323 | * `EntityDiff` and derivatives 324 | * `ItemLookup` and `ItemNotFoundException` 325 | * `PropertyLookup` and `PropertyNotFoundException` 326 | * `PropertyDataTypeLookup` 327 | * `BestStatementsFinder` 328 | * `ByPropertyIdGrouper` 329 | * `StatementGuidParser` and alias `ClaimGuidParser` 330 | * `StatementGuidParsingException` and alias `ClaimGuidParsingException` 331 | * `StatementList::getBestStatementPerProperty` 332 | 333 | #### Additions 334 | 335 | * Added `DerivedPropertyValueSnak` 336 | 337 | ## Version 3.0.1 (2015-07-01) 338 | 339 | * Fixed out of bounds bug in `SnakList::orderByProperty` 340 | 341 | ## Version 3.0.0 (2015-06-06) 342 | 343 | #### Breaking changes 344 | 345 | The concept of `Claim` is no longer modelled: 346 | 347 | * The `Claim` class itself has been removed, though `Claim` is now a temporary alias for `Statement` 348 | * `Claim::RANK_TRUTH` have been removed 349 | * `Statement` no longer takes a `Claim` in its constructor 350 | * `Statement::setClaim` and `Statement::getClaim` have been removed 351 | * Removed `ClaimList` 352 | * Removed `ClaimListAccess` 353 | * Removed `addClaim`, `hasClaims` and `newClaim` from all entity classes 354 | 355 | Phasing out of `Claims`: 356 | 357 | * `Claims::addClaim` no longer supports setting an index 358 | * Removed `Claims::getBestClaims`, use `StatementList::getBestStatements` instead 359 | * Removed `Claims::getByRank` and `Claims::getByRanks`, use `StatementList::getByRank` instead 360 | * Removed `Claims::getMainSnaks`, use `StatementList::getMainSnaks` instead 361 | * Removed `Claims::getClaimsForProperty`, use `StatementList::getWithPropertyId` instead 362 | * Removed `Claims::getHashes` 363 | * Removed `Claims::getGuids` 364 | * Removed `Claims::equals` (and `Claims` no longer implements `Comparable`) 365 | * Removed `Claims::getHash` (and `Claims` no longer implements `Hashable`) 366 | * Removed `Claims::hasClaim` 367 | * Removed `Claims::isEmpty`, use `StatementList::isEmpty` instead 368 | * Removed `Claims::indexOf`, use `StatementList::getFirstStatementWithGuid` or `StatementByGuidMap` instead 369 | * Removed `Claims::removeClaim` 370 | 371 | Other breaking changes: 372 | 373 | * Removed `Snaks` interface, use `SnakList` instead 374 | * Removed previously deprecated `Entity::getAllSnaks`, use `StatementList::getAllSnaks` instead 375 | * Removed previously deprecated `EntityId::getPrefixedId`, use `EntityId::getSerialization` instead 376 | * Removed previously deprecated `Property::newEmpty`, use `Property::newFromType` or `new Property()` instead 377 | * Renamed `StatementList::getWithPropertyId` to `StatementList::getByPropertyId` 378 | * Renamed `StatementList::getWithRank` to `StatementList::getByRank` 379 | * Added `EntityDocument::setId` 380 | * `Entity::setLabel` and `Entity::setDescription` no longer return anything 381 | * `Reference` and `ReferenceList`s no longer can be instantiated with `null` 382 | 383 | #### Additions 384 | 385 | * Added `StatementByGuidMap` 386 | * Added `StatementListHolder` 387 | * Added `StatementList::getFirstStatementWithGuid` 388 | * Added `StatementList::removeStatementsWithGuid` 389 | * `ReferenceList::addNewReference` and `Statement::addNewReference` support an array of Snaks now 390 | * Added PHPMD support 391 | 392 | #### Deprecations 393 | 394 | * Renamed `Claim\ClaimGuid` to `Statement\StatementGuid`, leaving a b/c alias in place 395 | * Renamed `Claim\ClaimGuidParser` to `Statement\StatementGuidParser`, leaving a b/c alias in place 396 | * Renamed `Claim\ClaimGuidParsingException` to `Statement\StatementGuidParsingException`, leaving a b/c alias in place 397 | * Renamed `StatementListProvider` to `Statement\StatementListProvider`, leaving a b/c alias in place 398 | 399 | #### Other changes 400 | 401 | * `Item::setLabel`, `Item::setDescription` and `Item::setAliases` are no longer deprecated 402 | * `Property::setLabel`, `Property::setDescription` and `Property::setAliases` are no longer deprecated 403 | 404 | ## Version 2.6.1 (2015-04-25) 405 | 406 | * Allow installation together with Diff 2.x. 407 | 408 | ## Version 2.6.0 (2015-03-08) 409 | 410 | * Added `Reference::isEmpty` 411 | * Empty strings are now detected as invalid in the `SiteLink` constructor 412 | * Empty References are now ignored when added to `ReferenceList` 413 | * The `ReferenceList` constructor now throws an `InvalidArgumentException` when getting a non-iterable input 414 | * The `SnakList` constructor now throws an `InvalidArgumentException` when getting a non-iterable input 415 | * The `AliasGroup::equals` and `Term::equals` methods no longer incorrectly return true for fallback objects 416 | 417 | ## Version 2.5.0 (2014-01-20) 418 | 419 | * Added `ItemLookup` and `PropertyLookup` interfaces 420 | * Added `ItemNotFoundException` 421 | * Added `AliasGroupList::getWithLanguages` 422 | * Added `AliasGroupList::toTextArray` 423 | * Added `ItemIdSet::getSerializations` 424 | * Added `SiteLinkList::setNewSiteLink` 425 | * Added `SiteLinkList::setSiteLink` 426 | * Added `SiteLinkList::toArray` 427 | * Added `TermList::getWithLanguages` 428 | * Empty strings are now detected as invalid language codes in the term classes 429 | * Made all `Fingerprint` constructor parameters optional 430 | * Made all `Item` constructor parameters optional 431 | * Made the `Property` constructor's fingerprint parameter nullable 432 | * The `StatementList` constructor now accepts `Statement` objects in variable-length argument list format 433 | * Deprecated `Fingerprint::newEmpty` in favour of `new Fingerprint()` 434 | * Deprecated `Item::newEmpty` in favour of `new Item()` 435 | * Added PHPCS support 436 | 437 | ## Version 2.4.1 (2014-11-26) 438 | 439 | * Fixed `StatementList` not reindexing array keys 440 | 441 | ## Version 2.4.0 (2014-11-23) 442 | 443 | * `Property` now implements the deprecated claim related methods defined in `Entity` 444 | * Added `AliasGroupList::isEmpty` 445 | * Added `StatementList::getBestStatements` 446 | * Added `StatementList::getWithRank` 447 | * Added `TermList::isEmpty` 448 | * Added `AliasGroupFallback` 449 | * Added `TermFallback` 450 | 451 | ## Version 2.3.0 (2014-11-18) 452 | 453 | * Added `AliasGroupList::toArray` 454 | * Added `StatementList::getMainSnaks` 455 | * Added `StatementList::getWithPropertyId` 456 | * `BestStatementsFinder::getBestStatementsForProperty` no longer throws an `OutOfBounds` exception 457 | 458 | ## Version 2.2.0 (2014-11-10) 459 | 460 | * `Item` and `Property` now implement `StatementListProvider` 461 | * Introduced the `StatementListProvider` interface for classes containing a `StatementList` 462 | * Added rank comparison to `Statement::equals` 463 | 464 | ## Version 2.1.0 (2014-10-27) 465 | 466 | * `ReferenceList` now implements `Serializable` 467 | * Enhanced 32 bit compatibility for numeric ids 468 | 469 | ## Version 2.0.2 (2014-10-23) 470 | 471 | * Fixed handling of numeric ids as string in `LegacyIdInterpreter` which was broken in 2.0.1. 472 | 473 | ## Version 2.0.1 (2014-10-23) 474 | 475 | * Fixed last remaining HHVM issue (caused by calling `reset` on an `ArrayObject` subclass) 476 | * `EntityIdValue::unserialize` now throws the correct type of exception 477 | * Improved performance of `BasicEntityIdParser` and `LegacyIdInterpreter` 478 | 479 | ## Version 2.0.0 (2014-10-14) 480 | 481 | #### Breaking changes 482 | 483 | * Removed all class aliases 484 | * Removed support for deserializing `EntityId` instances serialized with version 0.4 or earlier 485 | * Removed `References` interface in favour of `ReferenceList` 486 | * The `Statement` constructor no longer supports a `Snak` parameter 487 | 488 | #### Additions 489 | 490 | * Added `Statement::RANK_` enum 491 | * Added `Statement::addNewReference` 492 | 493 | #### Deprecations 494 | 495 | * Deprecated `Claim::RANK_` enum in favour of `Statement::RANK_` enum 496 | * Deprecated `Claim::getRank` 497 | 498 | ## Version 1.1.0 (2014-09-29) 499 | 500 | #### Additions 501 | 502 | * The `Property` constructor now accepts an optional `StatementList` parameter 503 | * Added `Property::getStatements` and `Property::setStatements` 504 | * Added `PropertyIdProvider` interface 505 | * Added `ByPropertyIdGrouper` 506 | * Added `BestStatementsFinder` 507 | * Added `EntityPatcher` and `EntityPatcherStrategy` 508 | * Added `StatementList::getAllSnaks` to use instead of `Entity::getAllSnaks` 509 | * The `Statement` constructor now also accepts a `Claim` parameter 510 | * Added `Statement::setClaim` 511 | * The `Reference` constructor now accepts a `Snak` array 512 | * Added `ReferenceList::addNewReference` 513 | 514 | ## Version 1.0.0 (2014-09-02) 515 | 516 | #### Breaking changes 517 | 518 | Changes in the `Entity` hierarchy: 519 | 520 | * Changed the constructor signature of `Item` 521 | * Changed the constructor signature of `Property` 522 | * Removed `Entity::setClaims` (`Item::setClaims` has been retained) 523 | * Removed `Entity::stub` 524 | * Removed `Entity::getIdFromClaimGuid` 525 | * `Entity::removeLabel` no longer accepts an array of language codes 526 | * `Entity::removeDescription` no longer accepts an array of language codes 527 | * `Entity` no longer implements `Serializable` 528 | * Protected method `Entity::patchSpecificFields` no longer has a second parameter 529 | * `Entity::getFingerprint` is now returned by reference 530 | 531 | Removal of `toArray` and `newFromArray`: 532 | 533 | * Removed `Entity::toArray`, `Item::newFromArray` and `Property::newFromArray` 534 | * Removed `Claim::toArray` and `Statement::toArray` 535 | * Removed `Claim::newFromArray` and `Statement::newFromArray` 536 | * Removed `ReferenceList::toArray` and `ReferenceList::newFromArray` 537 | * Removed `toArray` from the `References` interface 538 | * Removed `SiteLink::toArray` and `SiteLink::newFromArray` 539 | * Removed `toArray` from the `Snak` and `Snaks` interfaces 540 | * Removed `PropertyValueSnak::toArray` 541 | * Removed `SnakList::toArray` and `SnakList::newFromArray` 542 | * Removed `SnakObject::toArray` and `SnakObject::newFromArray` 543 | * Removed `SnakObject::newFromType` 544 | 545 | Other breaking changes: 546 | 547 | * `Item` now has an array of `Statement` rather than an array of `Claim` 548 | * `Property` no longer has an array of `Claim` 549 | * `Claim` and `Statement` no longer implement `Serializable` 550 | * Protected method `Entity::entityToDiffArray` got renamed to `Entity::getDiffArray` 551 | * Removed `Fingerprint::getAliases` 552 | * Removed `EntityId::newFromPrefixedId` 553 | * The constructor of `EntityId` is no longer public 554 | * `Claims::getDiff` no longer takes a second optional parameter 555 | * `Claims::getDiff` now throws an `UnexpectedValueException` rather than an `InvalidArgumentException` 556 | * Removed these class aliases deprecated since 0.4: 557 | `ItemObject`, `ReferenceObject`, `ClaimObject`, `StatementObject` 558 | * `HashArray` and `SnakList` no longer take an optional parameter in `getHash` 559 | * Calling `clear` on an `Item` will now cause its statements to be removed 560 | * `SiteLinkList::addNewSiteLink` no longer returns a `SiteLinkList` instance 561 | * Removed the global variable `evilDataValueMap` 562 | * Removed `ClaimAggregate` interface, which is thus no longer implemented by `Entity` 563 | * `HashableObjectStorage::getValueHash` no longer accepts a first optional parameter 564 | * `MapHasher` and `MapValueHasher` are now package private 565 | * Removed `Claims::getDiff` 566 | 567 | #### Additions 568 | 569 | * Added `ClaimList` 570 | * Added `StatementList` 571 | * Added `StatementListDiffer` 572 | * Added `PropertyDataTypeLookup` and trivial implementation `InMemoryDataTypeLookup` 573 | * Added `PropertyNotFoundException` 574 | * Added `ItemDiffer` and `PropertyDiffer` 575 | * Added `EntityDiffer` and `EntityDifferStrategy` 576 | * Added `Statement::getClaim` 577 | * Added `Item::getStatements` 578 | * Added `Item::setStatements` 579 | 580 | #### Deprecations 581 | 582 | * Deprecated `Entity` (but not the derivatives) 583 | * Deprecated `Claims` 584 | * Deprecated `Entity::setId` 585 | * Deprecated `Entity::newClaim` 586 | * Deprecated `Entity::getAllSnaks` 587 | * Deprecated `Entity::getDiff` in favour of `EntityDiffer` and more specific differs 588 | * Deprecated `Item::getClaims` in favour of `Item::getStatements` 589 | * Deprecated `Item::setClaims` in favour of `Item::setStatements` 590 | * Deprecated `Item::hasClaims` in favour of `Item::getStatements()->count` 591 | * Deprecated `Item::addClaim` in favour of `Item::getStatements()->add*` 592 | 593 | #### Other changes 594 | 595 | * Undeprecated passing an integer to `Item::setId` and `Property::setId` 596 | * The FQN of `Statement` is now `Wikibase\DataModel\Statement\Statement`. The old FQN is deprecated. 597 | 598 | ## Version 0.9.1 (2014-08-26) 599 | 600 | * Fixed error caused by redeclaration of getType in `Entity`, after it already got defined in `EntityDocument` 601 | 602 | ## Version 0.9.0 (2014-08-15) 603 | 604 | * Changed the signatures of `setLabel`, `setDescription` and `setAliasGroup` in `Fingerprint` 605 | * Added `hasLabel`, `hasDescription` and `hasAliasGroup` to `Fingerprint` 606 | 607 | ## Version 0.8.2 (2014-07-25) 608 | 609 | * Added `EntityDocument` interface, which is implemented by `Entity` 610 | * Added `LegacyIdInterpreter` 611 | * Undeprecated `Entity::isEmpty` 612 | * Undeprecated `Entity::clear` 613 | 614 | ## Version 0.8.1 (2014-06-06) 615 | 616 | * Fixed fatal error when calling `Item::getSiteLinkList` on an `Item` right after constructing it 617 | 618 | ## Version 0.8.0 (2014-06-05) 619 | 620 | #### Breaking changes 621 | 622 | * `Item::removeSiteLink` no longer takes an optional second parameter and no longer returns a boolean 623 | * Shallow clones of `Item` will now share the same list of site links 624 | * `SiteLinkList` is now mutable 625 | 626 | #### Additions 627 | 628 | * `AliasGroupList::hasGroupForLanguage` 629 | * `AliasGroupList::setAliasesForLanguage` 630 | * `SiteLinkList::addSiteLink` 631 | * `SiteLinkList::addNewSiteLink` 632 | * `SiteLinkList::removeLinkWithSiteId` 633 | * `SiteLinkList::isEmpty` 634 | * `SiteLinkList::removeLinkWithSiteId` 635 | * `Item::getSiteLinkList` 636 | * `Item::setSiteLinkList` 637 | * `TermList::setTextForLanguage` 638 | 639 | #### Deprecations 640 | 641 | * `Item::addSiteLink` 642 | * `Item::removeSiteLink` 643 | * `Item::getSiteLinks` 644 | * `Item::getSiteLink` 645 | * `Item::hasLinkToSite` 646 | * `Item::hasSiteLinks` 647 | 648 | #### Improvements 649 | 650 | * An empty `TermList` can now be constructed with no constructor arguments 651 | * An empty `AliasGroupList` can now be constructed with no constructor arguments 652 | 653 | ## Version 0.7.4 (2014-04-24) 654 | 655 | #### Additions 656 | 657 | * Made these classes implement `Comparable`: 658 | * `TermList` 659 | * `AliasGroupList` 660 | * `Fingerprint` 661 | * `SiteLink` 662 | * `SiteLinkList` 663 | * `Claim` 664 | * `Claims` 665 | * `Statement` 666 | * Added methods to `Fingerprint`: 667 | * `getLabel` 668 | * `setLabel` 669 | * `removeLabel` 670 | * `setLabels` 671 | * `getDescription` 672 | * `setDescription` 673 | * `removeDescription` 674 | * `setDescriptions` 675 | * `getAliasGroup` 676 | * `setAliasGroup` 677 | * `removeAliasGroup` 678 | * `setAliasGroups` 679 | * `getAliasGroups` 680 | * `isEmpty` 681 | * Added `ItemIdSet` 682 | 683 | #### Deprecations 684 | 685 | * ~~`Entity::clear` (to be removed in 1.0)~~ 686 | * ~~`Entity::isEmpty` (to be removed in 1.0)~~ 687 | * `Entity::stub` (to be removed in 1.0) 688 | * `Fingerprint::getAliases` (in favour of `Fingerprint::getAliasGroups`) 689 | 690 | #### Removals 691 | 692 | * This library no longer uses the MediaWiki i18n system when MediaWiki is loaded. 693 | No description will be shown as part of its entry on Special:Version. 694 | 695 | ## Version 0.7.3 (2014-04-11) 696 | 697 | #### Additions 698 | 699 | * Added `Wikibase\DataModel\Term` namespace with these constructs: 700 | * Term\AliasGroup 701 | * Term\AliasGroupList 702 | * Term\Fingerprint 703 | * Term\FingerprintProvider 704 | * Term\Term 705 | * Term\TermList 706 | * Added `Entity::getFingerprint` 707 | * Added `Entity::setFingerprint` 708 | 709 | #### Deprecations 710 | 711 | * Deprecated `Property::newEmpty` in favor of `Property::newFromType` 712 | * Deprecated old fingerprint related methods in `Entity`: 713 | * setLabel 714 | * setDescription 715 | * removeLabel 716 | * removeDescription 717 | * getAliases 718 | * getAllAliases 719 | * setAliases 720 | * addAliases 721 | * removeAliases 722 | * getDescriptions 723 | * getLabels 724 | * getDescription 725 | * getLabel 726 | * setLabels 727 | * setDescriptions 728 | * setAllAliases 729 | * Deprecated `SnakList::newFromArray` 730 | * Deprecated `Statement::newFromArray` 731 | * Deprecated `Claim::newFromArray` 732 | * Deprecated `ReferenceList::newFromArray` 733 | 734 | ## Version 0.7.2 (2014-03-13) 735 | 736 | * Added Claims::getByRanks 737 | 738 | ## Version 0.7.1 (2014-03-12) 739 | 740 | * Removed DataValues Geo, DataValues Time and DataValues Number from the dependency list. 741 | They where no longer needed. 742 | 743 | ## Version 0.7.0 (2014-03-07) 744 | 745 | #### Additions 746 | 747 | * Added TypedSnak value object 748 | * Added SiteLinkList value object 749 | * Added Claims::getBestClaims 750 | * Added Claims::getByRank 751 | 752 | #### Improvements 753 | 754 | * The PHPUnit bootstrap file now works again on Windows 755 | * Changed class loading from PSR-0 to PSR-4 756 | 757 | #### Deprecations 758 | 759 | * Deprecated SiteLink::toArray(), SiteLink::newFromArray(), SiteLink::getBadgesFromArray() 760 | 761 | #### Removals 762 | 763 | * Removed PropertySnak interface 764 | * Removed Claims::getObjectType 765 | 766 | ## Version 0.6.0 (2013-12-23) 767 | 768 | #### Improvements 769 | 770 | * Wikibase DataModel now uses the "new" DataValues components. This means binding to other code has 771 | decreased and several design issues have been tackled. 772 | * Wikibase DataModel is now PSR-0 compliant. 773 | 774 | #### Deprecations 775 | 776 | * All classes and interfaces not yet in the Wikibase\DataModel namespace got moved. The old names 777 | remain as aliases, and should be considered as deprecated. 778 | * SimpleSiteLink was renamed to SiteLink. The old name remains as deprecated alias. 779 | * Item::addSimpleSiteLink and Item::getSimpleSiteLinks where renamed to Item::adSiteLink and 780 | Item::getSiteLinks. The old names remains as deprecated aliases. 781 | 782 | #### Removals 783 | 784 | * Entity::getTerms was removed, as it returned objects of type Term, which is defined by a component 785 | Wikibase DataModel does not depend upon. 786 | 787 | ## Version 0.5.0 (2013-12-11) 788 | 789 | Note that this list is incomplete. In particular, not all breaking changes are listed. 790 | 791 | #### Additions 792 | 793 | * Added ItemId and PropertyId classes. 794 | * Added BasicEntityIdParser that allows for parsing of serializations of entity ids defined 795 | by Wikibase DataModel. 796 | * Added ClaimGuid and ClaimGuidParser. 797 | 798 | #### Improvements 799 | 800 | * EntityId no longer is a DataValue. A new EntityIdValue was added to function as a DataValue 801 | representing the identity of an entity. 802 | 803 | #### Removals 804 | 805 | * ObjectComparer has been removed from the public namespace. 806 | * SnakFactory has been moved out of this component. 807 | 808 | #### Deprecations 809 | 810 | * Constructing an EntityId (rather then one of its derivatives) is now deprecated. 811 | * Wikibase\EntityId has been renamed to Wikibase\DataModel\Entity\EntityId. The old name is deprecated. 812 | 813 | ## Version 0.4.0 (2013-06-17) 814 | 815 | Initial release as Wikibase DataModel component. 816 | 817 | ## Version 0.1.0 (2012-11-01) 818 | 819 | Initial release as part of Wikibase. 820 | -------------------------------------------------------------------------------- /src/ByPropertyIdArray.php: -------------------------------------------------------------------------------- 1 | o3 (p2) 35 | * o3 (p2) ---> move to index 1 -/ o2 (p2) 36 | * 37 | * Example of moving an object that triggers moving the whole "property group": 38 | * o1 (p1) /-> o3 (p2) 39 | * o2 (p2) | o2 (p2) 40 | * o3 (p2) ---> move to index 0 -/ o1 (p1) 41 | * 42 | * @since 0.2 43 | * @deprecated since 5.0, use a DataModel Service instead 44 | * 45 | * @license GPL-2.0-or-later 46 | * @author H. Snater < mediawiki@snater.com > 47 | */ 48 | class ByPropertyIdArray extends ArrayObject { 49 | 50 | /** 51 | * @var array[]|null 52 | */ 53 | private $byId = null; 54 | 55 | /** 56 | * @deprecated since 5.0, use a DataModel Service instead 57 | * @see ArrayObject::__construct 58 | * 59 | * @param PropertyIdProvider[]|Traversable|null $input 60 | * 61 | * @throws InvalidArgumentException 62 | */ 63 | public function __construct( $input = null ) { 64 | if ( $input && !is_iterable( $input ) ) { 65 | throw new InvalidArgumentException( '$input must be an array, Traversable or null' ); 66 | } 67 | 68 | parent::__construct( (array)$input ); 69 | } 70 | 71 | /** 72 | * Builds the index for doing look-ups by property id. 73 | * 74 | * @since 0.2 75 | */ 76 | public function buildIndex() { 77 | $this->byId = []; 78 | 79 | /** @var PropertyIdProvider $object */ 80 | foreach ( $this as $object ) { 81 | $propertyId = $object->getPropertyId()->getSerialization(); 82 | 83 | if ( !array_key_exists( $propertyId, $this->byId ) ) { 84 | $this->byId[$propertyId] = []; 85 | } 86 | 87 | $this->byId[$propertyId][] = $object; 88 | } 89 | } 90 | 91 | /** 92 | * Checks whether id indexed array has been generated. 93 | * 94 | * @throws RuntimeException 95 | */ 96 | private function assertIndexIsBuild() { 97 | if ( $this->byId === null ) { 98 | throw new RuntimeException( 'Index not build, call buildIndex first' ); 99 | } 100 | } 101 | 102 | /** 103 | * Returns the property ids used for indexing. 104 | * 105 | * @since 0.2 106 | * 107 | * @return PropertyId[] 108 | * @throws RuntimeException 109 | */ 110 | public function getPropertyIds() { 111 | $this->assertIndexIsBuild(); 112 | 113 | return array_map( 114 | static function( $serializedPropertyId ) { 115 | return new NumericPropertyId( $serializedPropertyId ); 116 | }, 117 | array_keys( $this->byId ) 118 | ); 119 | } 120 | 121 | /** 122 | * Returns the objects featuring the provided property id in the index. 123 | * 124 | * @since 0.2 125 | * 126 | * @param PropertyId $propertyId 127 | * 128 | * @throws OutOfBoundsException 129 | * @throws RuntimeException 130 | * @return PropertyIdProvider[] 131 | */ 132 | public function getByPropertyId( PropertyId $propertyId ) { 133 | $this->assertIndexIsBuild(); 134 | 135 | if ( !( array_key_exists( $propertyId->getSerialization(), $this->byId ) ) ) { 136 | throw new OutOfBoundsException( "Object with propertyId \"$propertyId\" not found" ); 137 | } 138 | 139 | return $this->byId[$propertyId->getSerialization()]; 140 | } 141 | 142 | /** 143 | * Returns the absolute index of an object or false if the object could not be found. 144 | * @since 0.5 145 | * 146 | * @param PropertyIdProvider $object 147 | * 148 | * @return bool|int 149 | * @throws RuntimeException 150 | */ 151 | public function getFlatArrayIndexOfObject( $object ) { 152 | $this->assertIndexIsBuild(); 153 | 154 | $i = 0; 155 | foreach ( $this as $o ) { 156 | if ( $o === $object ) { 157 | return $i; 158 | } 159 | $i++; 160 | } 161 | return false; 162 | } 163 | 164 | /** 165 | * Returns the objects in a flat array (using the indexed form for generating the array). 166 | * @since 0.5 167 | * 168 | * @return PropertyIdProvider[] 169 | * @throws RuntimeException 170 | */ 171 | public function toFlatArray() { 172 | $this->assertIndexIsBuild(); 173 | 174 | $array = []; 175 | foreach ( $this->byId as $objects ) { 176 | $array = array_merge( $array, $objects ); 177 | } 178 | return $array; 179 | } 180 | 181 | /** 182 | * Returns the absolute numeric indices of objects featuring the same property id. 183 | * 184 | * @param PropertyId $propertyId 185 | * 186 | * @throws RuntimeException 187 | * @return int[] 188 | */ 189 | private function getFlatArrayIndices( PropertyId $propertyId ) { 190 | $this->assertIndexIsBuild(); 191 | 192 | $propertyIndices = []; 193 | $i = 0; 194 | 195 | foreach ( $this->byId as $serializedPropertyId => $objects ) { 196 | if ( $serializedPropertyId === $propertyId->getSerialization() ) { 197 | $propertyIndices = range( $i, $i + count( $objects ) - 1 ); 198 | break; 199 | } else { 200 | $i += count( $objects ); 201 | } 202 | } 203 | 204 | return $propertyIndices; 205 | } 206 | 207 | /** 208 | * Moves an object within its "property group". 209 | * 210 | * @param PropertyIdProvider $object 211 | * @param int $toIndex Absolute index within a "property group". 212 | * 213 | * @throws OutOfBoundsException 214 | */ 215 | private function moveObjectInPropertyGroup( $object, $toIndex ) { 216 | $currentIndex = $this->getFlatArrayIndexOfObject( $object ); 217 | 218 | if ( $toIndex === $currentIndex ) { 219 | return; 220 | } 221 | 222 | $propertyId = $object->getPropertyId(); 223 | 224 | $numericIndices = $this->getFlatArrayIndices( $propertyId ); 225 | $lastIndex = end( $numericIndices ); 226 | 227 | if ( $toIndex > $lastIndex + 1 || $toIndex < $numericIndices[0] ) { 228 | throw new OutOfBoundsException( 'Object cannot be moved to ' . $toIndex ); 229 | } 230 | 231 | if ( $toIndex >= $lastIndex ) { 232 | $this->moveObjectToEndOfPropertyGroup( $object ); 233 | } else { 234 | $this->removeObject( $object ); 235 | 236 | $propertyGroup = array_combine( 237 | $this->getFlatArrayIndices( $propertyId ), 238 | $this->getByPropertyId( $propertyId ) 239 | ); 240 | 241 | $insertBefore = $propertyGroup[$toIndex]; 242 | $this->insertObjectAtIndex( $object, $this->getFlatArrayIndexOfObject( $insertBefore ) ); 243 | } 244 | } 245 | 246 | /** 247 | * Moves an object to the end of its "property group". 248 | * 249 | * @param PropertyIdProvider $object 250 | */ 251 | private function moveObjectToEndOfPropertyGroup( $object ) { 252 | $this->removeObject( $object ); 253 | 254 | $propertyId = $object->getPropertyId(); 255 | $propertyIdSerialization = $propertyId->getSerialization(); 256 | 257 | $propertyGroup = in_array( $propertyIdSerialization, $this->getPropertyIds() ) 258 | ? $this->getByPropertyId( $propertyId ) 259 | : []; 260 | 261 | $propertyGroup[] = $object; 262 | $this->byId[$propertyIdSerialization] = $propertyGroup; 263 | 264 | $this->exchangeArray( $this->toFlatArray() ); 265 | } 266 | 267 | /** 268 | * Removes an object from the array structures. 269 | * 270 | * @param PropertyIdProvider $object 271 | */ 272 | private function removeObject( $object ) { 273 | $flatArray = $this->toFlatArray(); 274 | $this->exchangeArray( $flatArray ); 275 | $this->offsetUnset( array_search( $object, $flatArray ) ); 276 | $this->buildIndex(); 277 | } 278 | 279 | /** 280 | * Inserts an object at a specific index. 281 | * 282 | * @param PropertyIdProvider $object 283 | * @param int $index Absolute index within the flat list of objects. 284 | */ 285 | private function insertObjectAtIndex( $object, $index ) { 286 | $flatArray = $this->toFlatArray(); 287 | 288 | $this->exchangeArray( array_merge( 289 | array_slice( $flatArray, 0, $index ), 290 | [ $object ], 291 | array_slice( $flatArray, $index ) 292 | ) ); 293 | 294 | $this->buildIndex(); 295 | } 296 | 297 | /** 298 | * @param PropertyId $propertyId 299 | * @param int $toIndex 300 | */ 301 | private function movePropertyGroup( PropertyId $propertyId, $toIndex ) { 302 | if ( $this->getPropertyGroupIndex( $propertyId ) === $toIndex ) { 303 | return; 304 | } 305 | 306 | $insertBefore = null; 307 | 308 | $oldIndex = $this->getPropertyGroupIndex( $propertyId ); 309 | $byIdClone = $this->byId; 310 | if ( $byIdClone === null ) { 311 | throw new RuntimeException( 'LogicError - index should have been built' ); 312 | } 313 | 314 | // Remove "property group" to calculate the groups new index: 315 | unset( $this->byId[$propertyId->getSerialization()] ); 316 | 317 | if ( $toIndex > $oldIndex ) { 318 | // If the group shall be moved towards the bottom, the number of objects within the 319 | // group needs to be subtracted from the absolute toIndex: 320 | $toIndex -= count( $byIdClone[$propertyId->getSerialization()] ); 321 | } 322 | 323 | foreach ( $this->getPropertyIds() as $pId ) { 324 | // Accepting other than the exact index by using <= letting the "property group" "latch" 325 | // in the next slot. 326 | if ( $toIndex <= $this->getPropertyGroupIndex( $pId ) ) { 327 | $insertBefore = $pId; 328 | break; 329 | } 330 | } 331 | 332 | $serializedPropertyId = $propertyId->getSerialization(); 333 | $this->byId = []; 334 | 335 | foreach ( $byIdClone as $serializedPId => $objects ) { 336 | $pId = new NumericPropertyId( $serializedPId ); 337 | if ( $pId->equals( $propertyId ) ) { 338 | continue; 339 | } elseif ( $pId->equals( $insertBefore ) ) { 340 | $this->byId[$serializedPropertyId] = $byIdClone[$serializedPropertyId]; 341 | } 342 | $this->byId[$serializedPId] = $objects; 343 | } 344 | 345 | if ( $insertBefore === null ) { 346 | $this->byId[$serializedPropertyId] = $byIdClone[$serializedPropertyId]; 347 | } 348 | 349 | $this->exchangeArray( $this->toFlatArray() ); 350 | } 351 | 352 | /** 353 | * Returns the index of a "property group" (the first object in the flat array that features 354 | * the specified property). Returns false if property id could not be found. 355 | * 356 | * @param PropertyId $propertyId 357 | * 358 | * @return bool|int 359 | */ 360 | private function getPropertyGroupIndex( PropertyId $propertyId ) { 361 | $i = 0; 362 | 363 | foreach ( $this->byId as $serializedPropertyId => $objects ) { 364 | $pId = new NumericPropertyId( $serializedPropertyId ); 365 | if ( $pId->equals( $propertyId ) ) { 366 | return $i; 367 | } 368 | $i += count( $objects ); 369 | } 370 | 371 | return false; 372 | } 373 | 374 | /** 375 | * Moves an existing object to a new index. Specifying an index outside the object's "property 376 | * group" will move the object to the edge of the "property group" and shift the whole group 377 | * to achieve the designated index for the object to move. 378 | * @since 0.5 379 | * 380 | * @param PropertyIdProvider $object 381 | * @param int $toIndex Absolute index where to move the object to. 382 | * 383 | * @throws OutOfBoundsException 384 | * @throws RuntimeException 385 | */ 386 | public function moveObjectToIndex( $object, $toIndex ) { 387 | $this->assertIndexIsBuild(); 388 | 389 | if ( !in_array( $object, $this->toFlatArray() ) ) { 390 | throw new OutOfBoundsException( 'Object not present in array' ); 391 | } elseif ( $toIndex < 0 || $toIndex > count( $this ) ) { 392 | throw new OutOfBoundsException( 'Specified index is out of bounds' ); 393 | } elseif ( $this->getFlatArrayIndexOfObject( $object ) === $toIndex ) { 394 | return; 395 | } 396 | 397 | // Determine whether to simply reindex the object within its "property group": 398 | $propertyIndices = $this->getFlatArrayIndices( $object->getPropertyId() ); 399 | 400 | if ( in_array( $toIndex, $propertyIndices ) ) { 401 | $this->moveObjectInPropertyGroup( $object, $toIndex ); 402 | } else { 403 | $edgeIndex = ( $toIndex <= $propertyIndices[0] ) 404 | ? $propertyIndices[0] 405 | : end( $propertyIndices ); 406 | 407 | $this->moveObjectInPropertyGroup( $object, $edgeIndex ); 408 | $this->movePropertyGroup( $object->getPropertyId(), $toIndex ); 409 | } 410 | 411 | $this->exchangeArray( $this->toFlatArray() ); 412 | } 413 | 414 | /** 415 | * Adds an object at a specific index. If no index is specified, the object will be append to 416 | * the end of its "property group" or - if no objects featuring the same property exist - to the 417 | * absolute end of the array. 418 | * Specifying an index outside a "property group" will place the new object at the specified 419 | * index with the existing "property group" objects being shifted towards the new object. 420 | * 421 | * @since 0.5 422 | * 423 | * @param PropertyIdProvider $object 424 | * @param int|null $index Absolute index where to place the new object. 425 | * 426 | * @throws OutOfBoundsException 427 | * @throws RuntimeException 428 | */ 429 | public function addObjectAtIndex( $object, $index = null ) { 430 | $this->assertIndexIsBuild(); 431 | 432 | $propertyId = $object->getPropertyId(); 433 | $validIndices = $this->getFlatArrayIndices( $propertyId ); 434 | 435 | if ( count( $this ) === 0 ) { 436 | // Array is empty, just append object. 437 | $this->append( $object ); 438 | } elseif ( $validIndices === [] ) { 439 | // No objects featuring that property exist. The object may be inserted at a place 440 | // between existing "property groups". 441 | $this->append( $object ); 442 | if ( $index !== null ) { 443 | $this->buildIndex(); 444 | $this->moveObjectToIndex( $object, $index ); 445 | } 446 | } else { 447 | // Objects featuring the same property as the object which is about to be added already 448 | // exist in the array. 449 | $this->addObjectToPropertyGroup( $object, $index ); 450 | } 451 | 452 | $this->buildIndex(); 453 | } 454 | 455 | /** 456 | * Adds an object to an existing property group at the specified absolute index. 457 | * 458 | * @param PropertyIdProvider $object 459 | * @param int|null $index 460 | * 461 | * @throws OutOfBoundsException 462 | */ 463 | private function addObjectToPropertyGroup( $object, $index = null ) { 464 | $propertyId = $object->getPropertyId(); 465 | $validIndices = $this->getFlatArrayIndices( $propertyId ); 466 | 467 | if ( $validIndices === [] ) { 468 | throw new OutOfBoundsException( 'No objects featuring the object\'s property exist' ); 469 | } 470 | 471 | // Add index to allow placing object after the last object of the "property group": 472 | $validIndices[] = end( $validIndices ) + 1; 473 | 474 | if ( $index === null ) { 475 | // If index is null, append object to "property group". 476 | $index = end( $validIndices ); 477 | } 478 | 479 | if ( $this->byId === null ) { 480 | throw new RuntimeException( 'LogicError - index should have been built' ); 481 | } 482 | 483 | if ( in_array( $index, $validIndices ) ) { 484 | // Add object at index within "property group". 485 | $this->byId[$propertyId->getSerialization()][] = $object; 486 | $this->exchangeArray( $this->toFlatArray() ); 487 | $this->moveObjectToIndex( $object, $index ); 488 | 489 | } else { 490 | // Index is out of the "property group"; The whole group needs to be moved. 491 | $this->movePropertyGroup( $propertyId, $index ); 492 | 493 | // Move new object to the edge of the "property group" to receive its designated 494 | // index: 495 | if ( $index < $validIndices[0] ) { 496 | array_unshift( $this->byId[$propertyId->getSerialization()], $object ); 497 | } else { 498 | $this->byId[$propertyId->getSerialization()][] = $object; 499 | } 500 | } 501 | 502 | $this->exchangeArray( $this->toFlatArray() ); 503 | } 504 | 505 | } 506 | -------------------------------------------------------------------------------- /src/Entity/BasicEntityIdParser.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class BasicEntityIdParser implements EntityIdParser { 14 | 15 | private DispatchingEntityIdParser $idParser; 16 | 17 | public function __construct() { 18 | $this->idParser = new DispatchingEntityIdParser( self::getBuilders() ); 19 | } 20 | 21 | /** 22 | * @param string $idSerialization 23 | * 24 | * @return EntityId 25 | * @throws EntityIdParsingException 26 | */ 27 | public function parse( $idSerialization ) { 28 | return $this->idParser->parse( $idSerialization ); 29 | } 30 | 31 | /** 32 | * Returns an id builders array. 33 | * Keys are preg_match patterns, values are callables. 34 | * (See the DispatchingEntityIdParser constructor for more details.) 35 | * 36 | * This method returns builders for the ids of all entity types 37 | * defined by WikibaseDataModel. It is intended to be used by 38 | * applications that allow for registration of additional entity 39 | * types, and thus want to extend upon this list. The extended 40 | * list can then be used to construct a DispatchingEntityIdParser instance. 41 | * 42 | * @return callable[] 43 | */ 44 | public static function getBuilders() { 45 | return [ 46 | ItemId::PATTERN => static function( $serialization ) { 47 | return new ItemId( $serialization ); 48 | }, 49 | NumericPropertyId::PATTERN => static function( $serialization ) { 50 | return new NumericPropertyId( $serialization ); 51 | }, 52 | ]; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/Entity/ClearableEntity.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DispatchingEntityIdParser implements EntityIdParser { 14 | 15 | /** 16 | * @var callable[] 17 | */ 18 | private $idBuilders; 19 | 20 | /** 21 | * Takes an array in which each key is a preg_match pattern. 22 | * The first pattern the id matches against will be picked. 23 | * The value this key points to has to be a builder function 24 | * that takes as only required argument the id serialization 25 | * (string) and returns an EntityId instance. 26 | * 27 | * @param callable[] $idBuilders 28 | */ 29 | public function __construct( array $idBuilders ) { 30 | $this->idBuilders = $idBuilders; 31 | } 32 | 33 | /** 34 | * @param string $idSerialization 35 | * 36 | * @throws EntityIdParsingException 37 | * @return EntityId 38 | */ 39 | public function parse( $idSerialization ) { 40 | $this->assertIdIsString( $idSerialization ); 41 | 42 | if ( $this->idBuilders === [] ) { 43 | throw new EntityIdParsingException( 'No id builders are configured' ); 44 | } 45 | 46 | foreach ( $this->idBuilders as $idPattern => $idBuilder ) { 47 | if ( preg_match( $idPattern, $idSerialization ) ) { 48 | return $this->buildId( $idBuilder, $idSerialization ); 49 | } 50 | } 51 | 52 | throw new EntityIdParsingException( 53 | "The serialization \"$idSerialization\" is not recognized by the configured id builders" 54 | ); 55 | } 56 | 57 | /** 58 | * @param string $idSerialization 59 | * 60 | * @throws EntityIdParsingException 61 | */ 62 | private function assertIdIsString( $idSerialization ) { 63 | if ( !is_string( $idSerialization ) ) { 64 | throw new EntityIdParsingException( 65 | '$idSerialization must be a string, got ' . ( is_object( $idSerialization ) 66 | ? get_class( $idSerialization ) 67 | : getType( $idSerialization ) ) 68 | ); 69 | } 70 | } 71 | 72 | /** 73 | * @param callable $idBuilder 74 | * @param string $idSerialization 75 | * 76 | * @throws EntityIdParsingException 77 | * @return EntityId 78 | */ 79 | private function buildId( $idBuilder, $idSerialization ) { 80 | try { 81 | return $idBuilder( $idSerialization ); 82 | } catch ( InvalidArgumentException $ex ) { 83 | // Should not happen, but if it does, re-throw the original message 84 | throw new EntityIdParsingException( $ex->getMessage(), 0, $ex ); 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Entity/EntityDocument.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Bene* < benestar.wikimedia@gmail.com > 16 | */ 17 | interface EntityDocument { 18 | 19 | /** 20 | * Returns a type identifier for the entity, e.g. "item" or "property". 21 | * 22 | * @since 0.8.2 23 | * 24 | * @return string 25 | */ 26 | public function getType(); 27 | 28 | /** 29 | * Returns the id of the entity or null if it does not have one. 30 | * 31 | * @since 0.8.2 32 | * 33 | * @return EntityId|null 34 | */ 35 | public function getId(); 36 | 37 | /** 38 | * Sets the id of the entity. A specific derivative of EntityId is always supported. 39 | * 40 | * @since 3.0 41 | * 42 | * @param EntityId $id 43 | * 44 | * @throws InvalidArgumentException if the id is not of the correct type. 45 | */ 46 | public function setId( $id ); 47 | 48 | /** 49 | * An entity is considered empty if it does not contain any content that can be removed. Having 50 | * an ID set never counts as having content. 51 | * 52 | * Knowing if an entity is empty is relevant when, for example, moving or merging entities and 53 | * code wants to make sure all content is transferred from the old to the new entity. 54 | * 55 | * @since 4.3 56 | * 57 | * @return bool 58 | */ 59 | public function isEmpty(); 60 | 61 | /** 62 | * 63 | * Two entities are considered equal if they are of the same type and have the same value. The 64 | * value does not include the id, so entities with the same value but different id are 65 | * considered equal. 66 | * 67 | * @since 5.0 68 | * 69 | * @param mixed $target 70 | * 71 | * @return bool 72 | */ 73 | public function equals( $target ); 74 | 75 | /** 76 | * Returns a deep clone of the entity. The clone must be equal in all details, including the id. 77 | * No change done to the clone is allowed to interfere with the original object. Only properties 78 | * containing immutable objects are allowed to (and should) reference the original object. 79 | * 80 | * Since EntityDocuments are mutable (at least the id can be set) the method is not allowed to 81 | * return $this. 82 | * 83 | * @since 5.0 84 | * 85 | * @return self 86 | */ 87 | public function copy(); 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Entity/EntityId.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class EntityIdParsingException extends RuntimeException { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Entity/EntityIdValue.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Thiemo Kreuz 16 | * @author Daniel Kinzler 17 | */ 18 | class EntityIdValue extends DataValueObject { 19 | 20 | private EntityId $entityId; 21 | 22 | public function __construct( EntityId $entityId ) { 23 | $this->entityId = $entityId; 24 | } 25 | 26 | public function getHash(): string { 27 | return md5( $this->getSerializationForHash() ); 28 | } 29 | 30 | /** 31 | * The serialization to use for hashing, for compatibility reasons this is 32 | * equivalent to the old (pre 7.4) PHP serialization. 33 | */ 34 | public function getSerializationForHash(): string { 35 | $data = $this->entityId->getSerialization(); 36 | $innerSerialization = 'C:' . strlen( get_class( $this->entityId ) ) . ':"' . get_class( $this->entityId ) . 37 | '":' . strlen( $data ) . ':{' . $data . '}'; 38 | 39 | return 'C:' . strlen( static::class ) . ':"' . static::class . 40 | '":' . strlen( $innerSerialization ) . ':{' . $innerSerialization . '}'; 41 | } 42 | 43 | public function __serialize(): array { 44 | return [ 'entityId' => $this->entityId ]; 45 | } 46 | 47 | /** 48 | * @see Serializable::serialize 49 | * 50 | * @since 7.0 serialization format changed in an incompatible way 51 | * 52 | * @note Do not use PHP serialization for persistence! Use a DataValueSerializer instead. 53 | * 54 | * @return string 55 | */ 56 | public function serialize() { 57 | return serialize( $this->entityId ); 58 | } 59 | 60 | public function __unserialize( array $data ): void { 61 | $this->__construct( $data['entityId'] ); 62 | } 63 | 64 | /** 65 | * @see Serializable::unserialize 66 | * 67 | * @param string $serialized 68 | * 69 | * @throws IllegalValueException 70 | */ 71 | public function unserialize( $serialized ) { 72 | $array = json_decode( $serialized ); 73 | 74 | if ( !is_array( $array ) ) { 75 | $this->__construct( unserialize( $serialized ) ); 76 | return; 77 | } 78 | 79 | [ $entityType, $numericId ] = $array; 80 | 81 | try { 82 | $entityId = LegacyIdInterpreter::newIdFromTypeAndNumber( $entityType, $numericId ); 83 | } catch ( InvalidArgumentException $ex ) { 84 | throw new IllegalValueException( 'Invalid EntityIdValue serialization.', 0, $ex ); 85 | } 86 | 87 | $this->__construct( $entityId ); 88 | } 89 | 90 | /** 91 | * @see DataValue::getType 92 | * 93 | * @return string 94 | */ 95 | public static function getType() { 96 | return 'wikibase-entityid'; 97 | } 98 | 99 | /** 100 | * @deprecated Kept for compatibility with older DataValues versions. 101 | * Do not use. 102 | * 103 | * @return string|float|int 104 | */ 105 | public function getSortKey() { 106 | return $this->entityId->getSerialization(); 107 | } 108 | 109 | /** 110 | * @see DataValue::getValue 111 | * 112 | * @return self 113 | */ 114 | public function getValue() { 115 | return $this; 116 | } 117 | 118 | /** 119 | * @return EntityId 120 | */ 121 | public function getEntityId() { 122 | return $this->entityId; 123 | } 124 | 125 | /** 126 | * @see DataValue::getArrayValue 127 | * 128 | * @return array 129 | */ 130 | public function getArrayValue() { 131 | $array = [ 132 | 'entity-type' => $this->entityId->getEntityType(), 133 | ]; 134 | 135 | if ( $this->entityId instanceof Int32EntityId ) { 136 | $array['numeric-id'] = $this->entityId->getNumericId(); 137 | } 138 | 139 | $array['id'] = $this->entityId->getSerialization(); 140 | return $array; 141 | } 142 | 143 | /** 144 | * Constructs a new instance from the provided data. Required for @see DataValueDeserializer. 145 | * This is expected to round-trip with @see getArrayValue. 146 | * 147 | * @deprecated since 7.1. Static DataValue::newFromArray constructors like this are 148 | * underspecified (not in the DataValue interface), and misleadingly named (should be named 149 | * newFromArrayValue). Instead, use DataValue builder callbacks in @see DataValueDeserializer. 150 | * 151 | * @param mixed $data Warning! Even if this is expected to be a value as returned by 152 | * @see getArrayValue, callers of this specific newFromArray implementation can not guarantee 153 | * this. This is not even guaranteed to be an array! 154 | * 155 | * @throws IllegalValueException if $data is not in the expected format. Subclasses of 156 | * InvalidArgumentException are expected and properly handled by @see DataValueDeserializer. 157 | * @return self 158 | */ 159 | public static function newFromArray( $data ) { 160 | if ( !is_array( $data ) ) { 161 | throw new IllegalValueException( '$data must be an array' ); 162 | } 163 | 164 | if ( array_key_exists( 'entity-type', $data ) && array_key_exists( 'numeric-id', $data ) ) { 165 | return self::newIdFromTypeAndNumber( $data['entity-type'], $data['numeric-id'] ); 166 | } elseif ( array_key_exists( 'id', $data ) ) { 167 | throw new IllegalValueException( 168 | 'Not able to parse "id" strings, use callbacks in DataValueDeserializer instead' 169 | ); 170 | } 171 | 172 | throw new IllegalValueException( 'Either "id" or "entity-type" and "numeric-id" fields required' ); 173 | } 174 | 175 | /** 176 | * @param string $entityType 177 | * @param int|float|string $numericId 178 | * 179 | * @throws IllegalValueException 180 | * @return self 181 | */ 182 | private static function newIdFromTypeAndNumber( $entityType, $numericId ) { 183 | try { 184 | return new self( LegacyIdInterpreter::newIdFromTypeAndNumber( $entityType, $numericId ) ); 185 | } catch ( InvalidArgumentException $ex ) { 186 | throw new IllegalValueException( $ex->getMessage(), 0, $ex ); 187 | } 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/Entity/EntityRedirect.php: -------------------------------------------------------------------------------- 1 | getEntityType() !== $targetId->getEntityType() ) { 35 | throw new InvalidArgumentException( 36 | '$entityId (' . $entityId . ') and $targetId (' . $targetId . ') must refer to the same kind of entity.' 37 | ); 38 | } 39 | 40 | if ( $entityId->getSerialization() === $targetId->getSerialization() ) { 41 | throw new InvalidArgumentException( '$entityId (' . $entityId . ') and $targetId can not be the same.' ); 42 | } 43 | 44 | $this->entityId = $entityId; 45 | $this->targetId = $targetId; 46 | } 47 | 48 | /** 49 | * @return EntityId 50 | */ 51 | public function getEntityId() { 52 | return $this->entityId; 53 | } 54 | 55 | /** 56 | * @return EntityId 57 | */ 58 | public function getTargetId() { 59 | return $this->targetId; 60 | } 61 | 62 | /** 63 | * @param mixed $that 64 | * 65 | * @return bool 66 | */ 67 | public function equals( $that ) { 68 | if ( $that === $this ) { 69 | return true; 70 | } 71 | 72 | return is_object( $that ) 73 | && get_class( $that ) === get_called_class() 74 | && $this->entityId->equals( $that->entityId ) 75 | && $this->targetId->equals( $that->targetId ); 76 | } 77 | 78 | /** 79 | * @since 4.4 80 | * 81 | * @return string 82 | */ 83 | public function __toString() { 84 | return $this->entityId . '->' . $this->targetId; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/Entity/Int32EntityId.php: -------------------------------------------------------------------------------- 1 | 27 | * @author Bene* < benestar.wikimedia@gmail.com > 28 | */ 29 | class Item implements 30 | StatementListProvidingEntity, 31 | FingerprintProvider, 32 | StatementListHolder, 33 | LabelsProvider, 34 | DescriptionsProvider, 35 | AliasesProvider, 36 | ClearableEntity 37 | { 38 | 39 | public const ENTITY_TYPE = 'item'; 40 | 41 | /** 42 | * @var ItemId|null 43 | */ 44 | private $id; 45 | 46 | /** 47 | * @var Fingerprint 48 | */ 49 | private $fingerprint; 50 | 51 | /** 52 | * @var SiteLinkList 53 | */ 54 | private $siteLinks; 55 | 56 | /** 57 | * @var StatementList 58 | */ 59 | private $statements; 60 | 61 | /** 62 | * @since 1.0 63 | */ 64 | public function __construct( 65 | ?ItemId $id = null, 66 | ?Fingerprint $fingerprint = null, 67 | ?SiteLinkList $siteLinks = null, 68 | ?StatementList $statements = null 69 | ) { 70 | $this->id = $id; 71 | $this->fingerprint = $fingerprint ?: new Fingerprint(); 72 | $this->siteLinks = $siteLinks ?: new SiteLinkList(); 73 | $this->statements = $statements ?: new StatementList(); 74 | } 75 | 76 | /** 77 | * Returns the id of the entity or null if it does not have one. 78 | * 79 | * @since 0.1 return type changed in 0.3 80 | * 81 | * @return ItemId|null 82 | */ 83 | public function getId() { 84 | return $this->id; 85 | } 86 | 87 | /** 88 | * @since 0.5, can be null since 1.0 89 | * 90 | * @param EntityId|null $id 91 | * 92 | * @throws InvalidArgumentException 93 | */ 94 | public function setId( $id ) { 95 | if ( !( $id instanceof ItemId ) && $id !== null ) { 96 | throw new InvalidArgumentException( '$id must be an ItemId or null' ); 97 | } 98 | 99 | $this->id = $id; 100 | } 101 | 102 | /** 103 | * @since 0.7.3 104 | * 105 | * @return Fingerprint 106 | */ 107 | public function getFingerprint() { 108 | return $this->fingerprint; 109 | } 110 | 111 | /** 112 | * @since 0.7.3 113 | * 114 | * @param Fingerprint $fingerprint 115 | */ 116 | public function setFingerprint( Fingerprint $fingerprint ) { 117 | $this->fingerprint = $fingerprint; 118 | } 119 | 120 | /** 121 | * @see LabelsProvider::getLabels 122 | * 123 | * @since 6.0 124 | * 125 | * @return TermList 126 | */ 127 | public function getLabels() { 128 | return $this->fingerprint->getLabels(); 129 | } 130 | 131 | /** 132 | * @see DescriptionsProvider::getDescriptions 133 | * 134 | * @since 6.0 135 | * 136 | * @return TermList 137 | */ 138 | public function getDescriptions() { 139 | return $this->fingerprint->getDescriptions(); 140 | } 141 | 142 | /** 143 | * @see AliasesProvider::getAliasGroups 144 | * 145 | * @since 6.0 146 | * 147 | * @return AliasGroupList 148 | */ 149 | public function getAliasGroups() { 150 | return $this->fingerprint->getAliasGroups(); 151 | } 152 | 153 | /** 154 | * @param string $languageCode 155 | * @param string $value 156 | * 157 | * @throws InvalidArgumentException 158 | */ 159 | public function setLabel( $languageCode, $value ) { 160 | $this->fingerprint->setLabel( $languageCode, $value ); 161 | } 162 | 163 | /** 164 | * @param string $languageCode 165 | * @param string $value 166 | * 167 | * @throws InvalidArgumentException 168 | */ 169 | public function setDescription( $languageCode, $value ) { 170 | $this->fingerprint->setDescription( $languageCode, $value ); 171 | } 172 | 173 | /** 174 | * @param string $languageCode 175 | * @param string[] $aliases 176 | * 177 | * @throws InvalidArgumentException 178 | */ 179 | public function setAliases( $languageCode, array $aliases ) { 180 | $this->fingerprint->setAliasGroup( $languageCode, $aliases ); 181 | } 182 | 183 | /** 184 | * @since 0.8 185 | * 186 | * @return SiteLinkList 187 | */ 188 | public function getSiteLinkList() { 189 | return $this->siteLinks; 190 | } 191 | 192 | /** 193 | * @since 0.8 194 | * 195 | * @param SiteLinkList $siteLinks 196 | */ 197 | public function setSiteLinkList( SiteLinkList $siteLinks ) { 198 | $this->siteLinks = $siteLinks; 199 | } 200 | 201 | /** 202 | * Adds a site link to the list of site links. 203 | * If there already is a site link with the site id of the provided site link, 204 | * then that one will be overridden by the provided one. 205 | * 206 | * @since 0.6 207 | * 208 | * @param SiteLink $siteLink 209 | */ 210 | public function addSiteLink( SiteLink $siteLink ) { 211 | if ( $this->siteLinks->hasLinkWithSiteId( $siteLink->getSiteId() ) ) { 212 | $this->siteLinks->removeLinkWithSiteId( $siteLink->getSiteId() ); 213 | } 214 | 215 | $this->siteLinks->addSiteLink( $siteLink ); 216 | } 217 | 218 | /** 219 | * Removes the sitelink with specified site ID if the Item has such a sitelink. 220 | * 221 | * @since 0.1 222 | * 223 | * @param string $siteId the target site's id 224 | */ 225 | public function removeSiteLink( $siteId ) { 226 | $this->siteLinks->removeLinkWithSiteId( $siteId ); 227 | } 228 | 229 | /** 230 | * @since 0.6 231 | * 232 | * @param string $siteId 233 | * 234 | * @return SiteLink 235 | * @throws OutOfBoundsException 236 | */ 237 | public function getSiteLink( $siteId ) { 238 | return $this->siteLinks->getBySiteId( $siteId ); 239 | } 240 | 241 | /** 242 | * @since 0.4 243 | * 244 | * @param string $siteId 245 | * 246 | * @return bool 247 | */ 248 | public function hasLinkToSite( $siteId ) { 249 | return $this->siteLinks->hasLinkWithSiteId( $siteId ); 250 | } 251 | 252 | /** 253 | * @deprecated since 2.5, use new Item() instead. 254 | * 255 | * @return self 256 | */ 257 | public static function newEmpty() { 258 | return new self(); 259 | } 260 | 261 | /** 262 | * @see Entity::getType 263 | * 264 | * @since 0.1 265 | * 266 | * @return string Returns the entity type "item". 267 | */ 268 | public function getType() { 269 | return self::ENTITY_TYPE; 270 | } 271 | 272 | /** 273 | * Returns if the Item has no content. 274 | * Having an id set does not count as having content. 275 | * 276 | * @since 0.1 277 | * 278 | * @return bool 279 | */ 280 | public function isEmpty() { 281 | return $this->fingerprint->isEmpty() 282 | && $this->statements->isEmpty() 283 | && $this->siteLinks->isEmpty(); 284 | } 285 | 286 | /** 287 | * @since 1.0 288 | * 289 | * @return StatementList 290 | */ 291 | public function getStatements() { 292 | return $this->statements; 293 | } 294 | 295 | /** 296 | * @since 1.0 297 | * 298 | * @param StatementList $statements 299 | */ 300 | public function setStatements( StatementList $statements ): void { 301 | $this->statements = $statements; 302 | } 303 | 304 | /** 305 | * @see EntityDocument::equals 306 | * 307 | * @since 0.1 308 | * 309 | * @param mixed $target 310 | * 311 | * @return bool 312 | */ 313 | public function equals( $target ) { 314 | if ( $this === $target ) { 315 | return true; 316 | } 317 | 318 | return $target instanceof self 319 | && $this->fingerprint->equals( $target->fingerprint ) 320 | && $this->siteLinks->equals( $target->siteLinks ) 321 | && $this->statements->equals( $target->statements ); 322 | } 323 | 324 | /** 325 | * @see EntityDocument::copy 326 | * 327 | * @since 0.1 328 | * 329 | * @return self 330 | */ 331 | public function copy() { 332 | return clone $this; 333 | } 334 | 335 | /** 336 | * @see http://php.net/manual/en/language.oop5.cloning.php 337 | * 338 | * @since 5.1 339 | */ 340 | public function __clone() { 341 | $this->fingerprint = clone $this->fingerprint; 342 | // SiteLinkList is mutable, but SiteLink is not. No deeper cloning necessary. 343 | $this->siteLinks = clone $this->siteLinks; 344 | $this->statements = clone $this->statements; 345 | } 346 | 347 | /** 348 | * @since 7.5 349 | */ 350 | public function clear() { 351 | $this->fingerprint = new Fingerprint(); 352 | $this->siteLinks = new SiteLinkList(); 353 | $this->statements = new StatementList(); 354 | } 355 | 356 | } 357 | -------------------------------------------------------------------------------- /src/Entity/ItemId.php: -------------------------------------------------------------------------------- 1 | assertValidIdFormat( $idSerialization ); 26 | parent::__construct( strtoupper( $idSerialization ) ); 27 | } 28 | 29 | /** 30 | * @param string $idSerialization 31 | */ 32 | private function assertValidIdFormat( $idSerialization ) { 33 | if ( !is_string( $idSerialization ) ) { 34 | throw new InvalidArgumentException( '$idSerialization must be a string' ); 35 | } 36 | 37 | if ( !preg_match( self::PATTERN, $idSerialization ) ) { 38 | throw new InvalidArgumentException( '$idSerialization must match ' . self::PATTERN ); 39 | } 40 | 41 | if ( strlen( $idSerialization ) > 10 42 | && substr( $idSerialization, 1 ) > Int32EntityId::MAX 43 | ) { 44 | throw new InvalidArgumentException( '$idSerialization can not exceed ' 45 | . Int32EntityId::MAX ); 46 | } 47 | } 48 | 49 | /** 50 | * @see Int32EntityId::getNumericId 51 | * 52 | * @return int Guaranteed to be a distinct integer in the range [1..2147483647]. 53 | */ 54 | public function getNumericId() { 55 | return (int)substr( $this->serialization, 1 ); 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function getEntityType() { 62 | return 'item'; 63 | } 64 | 65 | public function __serialize(): array { 66 | return [ 'serialization' => $this->serialization ]; 67 | } 68 | 69 | public function __unserialize( array $data ): void { 70 | $this->__construct( $data['serialization'] ); 71 | if ( $this->serialization !== $data['serialization'] ) { 72 | throw new InvalidArgumentException( '$data contained invalid serialization' ); 73 | } 74 | } 75 | 76 | /** 77 | * Construct an ItemId given the numeric part of its serialization. 78 | * 79 | * CAUTION: new usages of this method are discouraged. Typically you 80 | * should avoid dealing with just the numeric part, and use the whole 81 | * serialization. Not doing so in new code requires special justification. 82 | * 83 | * @param int|float|string $numericId 84 | * 85 | * @return self 86 | * @throws InvalidArgumentException 87 | */ 88 | public static function newFromNumber( $numericId ) { 89 | if ( !is_numeric( $numericId ) ) { 90 | throw new InvalidArgumentException( '$numericId must be numeric' ); 91 | } 92 | 93 | return new self( 'Q' . $numericId ); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/Entity/ItemIdParser.php: -------------------------------------------------------------------------------- 1 | getMessage(), 0, $ex ); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Entity/ItemIdSet.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class ItemIdSet implements IteratorAggregate, Countable { 21 | 22 | /** 23 | * @var ItemId[] 24 | */ 25 | private $ids = []; 26 | 27 | /** 28 | * @param ItemId[] $ids 29 | * 30 | * @throws InvalidArgumentException 31 | */ 32 | public function __construct( array $ids = [] ) { 33 | foreach ( $ids as $id ) { 34 | if ( !( $id instanceof ItemId ) ) { 35 | throw new InvalidArgumentException( 'Every element in $ids must be an instance of ItemId' ); 36 | } 37 | 38 | $this->ids[$id->getNumericId()] = $id; 39 | } 40 | } 41 | 42 | /** 43 | * @see Countable::count 44 | * 45 | * @return int 46 | */ 47 | public function count(): int { 48 | return count( $this->ids ); 49 | } 50 | 51 | /** 52 | * @see IteratorAggregate::getIterator 53 | * 54 | * @return Traversable 55 | */ 56 | public function getIterator(): Traversable { 57 | return new ArrayIterator( $this->ids ); 58 | } 59 | 60 | /** 61 | * @since 2.5 62 | * 63 | * @return string[] 64 | */ 65 | public function getSerializations() { 66 | return array_values( 67 | array_map( 68 | static function( ItemId $id ) { 69 | return $id->getSerialization(); 70 | }, 71 | $this->ids 72 | ) 73 | ); 74 | } 75 | 76 | /** 77 | * @param ItemId $id 78 | * 79 | * @return bool 80 | */ 81 | public function has( ItemId $id ) { 82 | return array_key_exists( $id->getNumericId(), $this->ids ); 83 | } 84 | 85 | /** 86 | * @see Countable::equals 87 | * 88 | * @since 0.1 89 | * 90 | * @param mixed $target 91 | * 92 | * @return bool 93 | */ 94 | public function equals( $target ) { 95 | if ( $this === $target ) { 96 | return true; 97 | } 98 | 99 | return $target instanceof self 100 | && $this->ids == $target->ids; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/Entity/NumericPropertyId.php: -------------------------------------------------------------------------------- 1 | assertValidIdFormat( $idSerialization ); 22 | parent::__construct( strtoupper( $idSerialization ) ); 23 | } 24 | 25 | /** 26 | * @param string $idSerialization 27 | */ 28 | private function assertValidIdFormat( $idSerialization ) { 29 | if ( !is_string( $idSerialization ) ) { 30 | throw new InvalidArgumentException( '$idSerialization must be a string' ); 31 | } 32 | 33 | if ( !preg_match( self::PATTERN, $idSerialization ) ) { 34 | throw new InvalidArgumentException( '$idSerialization must match ' . self::PATTERN ); 35 | } 36 | 37 | if ( strlen( $idSerialization ) > 10 38 | && substr( $idSerialization, 1 ) > Int32EntityId::MAX 39 | ) { 40 | throw new InvalidArgumentException( '$idSerialization can not exceed ' 41 | . Int32EntityId::MAX ); 42 | } 43 | } 44 | 45 | /** 46 | * @see Int32EntityId::getNumericId 47 | * 48 | * @return int Guaranteed to be a distinct integer in the range [1..2147483647]. 49 | */ 50 | public function getNumericId() { 51 | return (int)substr( $this->serialization, 1 ); 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getEntityType() { 58 | return 'property'; 59 | } 60 | 61 | public function __serialize(): array { 62 | return [ 'serialization' => $this->serialization ]; 63 | } 64 | 65 | public function __unserialize( array $data ): void { 66 | $this->__construct( $data['serialization'] ); 67 | if ( $this->serialization !== $data['serialization'] ) { 68 | throw new InvalidArgumentException( '$data contained invalid serialization' ); 69 | } 70 | } 71 | 72 | /** 73 | * Construct a NumericPropertyId given the numeric part of its serialization. 74 | * 75 | * CAUTION: new usages of this method are discouraged. Typically you 76 | * should avoid dealing with just the numeric part, and use the whole 77 | * serialization. Not doing so in new code requires special justification. 78 | * 79 | * @param int|float|string $numericId 80 | * 81 | * @return self 82 | * @throws InvalidArgumentException 83 | */ 84 | public static function newFromNumber( $numericId ) { 85 | if ( !is_numeric( $numericId ) ) { 86 | throw new InvalidArgumentException( '$numericId must be numeric' ); 87 | } 88 | 89 | return new self( 'P' . $numericId ); 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Entity/Property.php: -------------------------------------------------------------------------------- 1 | 24 | * @author Bene* < benestar.wikimedia@gmail.com > 25 | */ 26 | class Property implements 27 | StatementListProvidingEntity, 28 | FingerprintProvider, 29 | StatementListHolder, 30 | LabelsProvider, 31 | DescriptionsProvider, 32 | AliasesProvider, 33 | ClearableEntity 34 | { 35 | 36 | public const ENTITY_TYPE = 'property'; 37 | 38 | /** 39 | * @var PropertyId|null 40 | */ 41 | private $id; 42 | 43 | /** 44 | * @var Fingerprint 45 | */ 46 | private $fingerprint; 47 | 48 | /** 49 | * @var string The data type of the property. 50 | */ 51 | private $dataTypeId; 52 | 53 | /** 54 | * @var StatementList 55 | */ 56 | private $statements; 57 | 58 | /** 59 | * @since 1.0 60 | * 61 | * @param PropertyId|null $id 62 | * @param Fingerprint|null $fingerprint 63 | * @param string $dataTypeId The data type of the property. Not to be confused with the data 64 | * value type. 65 | * @param StatementList|null $statements Since 1.1 66 | */ 67 | public function __construct( 68 | ?PropertyId $id, 69 | ?Fingerprint $fingerprint, 70 | $dataTypeId, 71 | ?StatementList $statements = null 72 | ) { 73 | $this->id = $id; 74 | $this->fingerprint = $fingerprint ?: new Fingerprint(); 75 | $this->setDataTypeId( $dataTypeId ); 76 | $this->statements = $statements ?: new StatementList(); 77 | } 78 | 79 | /** 80 | * Returns the id of the entity or null if it does not have one. 81 | * 82 | * @since 0.1 return type changed in 0.3 83 | * 84 | * @return PropertyId|null 85 | */ 86 | public function getId() { 87 | return $this->id; 88 | } 89 | 90 | /** 91 | * @since 0.5, can be null since 1.0 92 | * 93 | * @param EntityId|null $id 94 | * 95 | * @throws InvalidArgumentException 96 | */ 97 | public function setId( $id ) { 98 | if ( !( $id instanceof PropertyId ) && $id !== null ) { 99 | throw new InvalidArgumentException( '$id must be a PropertyId or null' ); 100 | } 101 | 102 | $this->id = $id; 103 | } 104 | 105 | /** 106 | * @since 0.7.3 107 | * 108 | * @return Fingerprint 109 | */ 110 | public function getFingerprint() { 111 | return $this->fingerprint; 112 | } 113 | 114 | /** 115 | * @since 0.7.3 116 | * 117 | * @param Fingerprint $fingerprint 118 | */ 119 | public function setFingerprint( Fingerprint $fingerprint ) { 120 | $this->fingerprint = $fingerprint; 121 | } 122 | 123 | /** 124 | * @see LabelsProvider::getLabels 125 | * 126 | * @since 6.0 127 | * 128 | * @return TermList 129 | */ 130 | public function getLabels() { 131 | return $this->fingerprint->getLabels(); 132 | } 133 | 134 | /** 135 | * @see DescriptionsProvider::getDescriptions 136 | * 137 | * @since 6.0 138 | * 139 | * @return TermList 140 | */ 141 | public function getDescriptions() { 142 | return $this->fingerprint->getDescriptions(); 143 | } 144 | 145 | /** 146 | * @see AliasesProvider::getAliasGroups 147 | * 148 | * @since 6.0 149 | * 150 | * @return AliasGroupList 151 | */ 152 | public function getAliasGroups() { 153 | return $this->fingerprint->getAliasGroups(); 154 | } 155 | 156 | /** 157 | * @param string $languageCode 158 | * @param string $value 159 | * 160 | * @throws InvalidArgumentException 161 | */ 162 | public function setLabel( $languageCode, $value ) { 163 | $this->fingerprint->setLabel( $languageCode, $value ); 164 | } 165 | 166 | /** 167 | * @param string $languageCode 168 | * @param string $value 169 | * 170 | * @throws InvalidArgumentException 171 | */ 172 | public function setDescription( $languageCode, $value ) { 173 | $this->fingerprint->setDescription( $languageCode, $value ); 174 | } 175 | 176 | /** 177 | * @param string $languageCode 178 | * @param string[] $aliases 179 | * 180 | * @throws InvalidArgumentException 181 | */ 182 | public function setAliases( $languageCode, array $aliases ) { 183 | $this->fingerprint->setAliasGroup( $languageCode, $aliases ); 184 | } 185 | 186 | /** 187 | * @since 0.4 188 | * 189 | * @param string $dataTypeId The data type of the property. Not to be confused with the data 190 | * value type. 191 | * 192 | * @throws InvalidArgumentException 193 | */ 194 | public function setDataTypeId( $dataTypeId ) { 195 | if ( !is_string( $dataTypeId ) ) { 196 | throw new InvalidArgumentException( '$dataTypeId must be a string' ); 197 | } 198 | 199 | $this->dataTypeId = $dataTypeId; 200 | } 201 | 202 | /** 203 | * @since 0.4 204 | * 205 | * @return string Returns the data type of the property (property type). Not to be confused with 206 | * the data value type. 207 | */ 208 | public function getDataTypeId() { 209 | return $this->dataTypeId; 210 | } 211 | 212 | /** 213 | * @see Entity::getType 214 | * 215 | * @since 0.1 216 | * 217 | * @return string Returns the entity type "property". 218 | */ 219 | public function getType() { 220 | return self::ENTITY_TYPE; 221 | } 222 | 223 | /** 224 | * @since 0.3 225 | * 226 | * @param string $dataTypeId The data type of the property. Not to be confused with the data 227 | * value type. 228 | * 229 | * @return self 230 | */ 231 | public static function newFromType( $dataTypeId ) { 232 | return new self( null, null, $dataTypeId ); 233 | } 234 | 235 | /** 236 | * @see EntityDocument::equals 237 | * 238 | * @since 0.1 239 | * 240 | * @param mixed $target 241 | * 242 | * @return bool 243 | */ 244 | public function equals( $target ) { 245 | if ( $this === $target ) { 246 | return true; 247 | } 248 | 249 | return $target instanceof self 250 | && $this->dataTypeId === $target->dataTypeId 251 | && $this->fingerprint->equals( $target->fingerprint ) 252 | && $this->statements->equals( $target->statements ); 253 | } 254 | 255 | /** 256 | * Returns if the Property has no content. 257 | * Having an id and type set does not count as having content. 258 | * 259 | * @since 0.1 260 | * 261 | * @return bool 262 | */ 263 | public function isEmpty() { 264 | return $this->fingerprint->isEmpty() 265 | && $this->statements->isEmpty(); 266 | } 267 | 268 | /** 269 | * @since 1.1 270 | * 271 | * @return StatementList 272 | */ 273 | public function getStatements() { 274 | return $this->statements; 275 | } 276 | 277 | /** 278 | * @since 1.1 279 | * 280 | * @param StatementList $statements 281 | */ 282 | public function setStatements( StatementList $statements ): void { 283 | $this->statements = $statements; 284 | } 285 | 286 | /** 287 | * @see EntityDocument::copy 288 | * 289 | * @since 0.1 290 | * 291 | * @return self 292 | */ 293 | public function copy() { 294 | return clone $this; 295 | } 296 | 297 | /** 298 | * @see http://php.net/manual/en/language.oop5.cloning.php 299 | * 300 | * @since 5.1 301 | */ 302 | public function __clone() { 303 | $this->fingerprint = clone $this->fingerprint; 304 | $this->statements = clone $this->statements; 305 | } 306 | 307 | /** 308 | * @since 7.5 309 | */ 310 | public function clear() { 311 | $this->fingerprint = new Fingerprint(); 312 | $this->statements = new StatementList(); 313 | } 314 | 315 | } 316 | -------------------------------------------------------------------------------- /src/Entity/PropertyId.php: -------------------------------------------------------------------------------- 1 | serialization = self::normalizeIdSerialization( $serialization ); 26 | } 27 | 28 | /** 29 | * @param string $serialization 30 | */ 31 | private static function assertValidSerialization( $serialization ) { 32 | if ( !is_string( $serialization ) ) { 33 | throw new InvalidArgumentException( '$serialization must be a string' ); 34 | } 35 | 36 | if ( $serialization === '' ) { 37 | throw new InvalidArgumentException( '$serialization must not be an empty string' ); 38 | } 39 | 40 | if ( !preg_match( self::PATTERN, $serialization ) ) { 41 | throw new InvalidArgumentException( '$serialization must match ' . self::PATTERN ); 42 | } 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | abstract public function getEntityType(); 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function getSerialization() { 54 | return $this->serialization; 55 | } 56 | 57 | /** 58 | * @param string $id 59 | * 60 | * @return string 61 | */ 62 | private static function normalizeIdSerialization( $id ) { 63 | return ltrim( $id, ':' ); 64 | } 65 | 66 | /** 67 | * This is a human readable representation of the EntityId. 68 | * This format is allowed to change and should therefore not 69 | * be relied upon to be stable. 70 | * 71 | * @return string 72 | */ 73 | public function __toString() { 74 | return $this->serialization; 75 | } 76 | 77 | /** 78 | * @param mixed $target 79 | * 80 | * @return bool 81 | */ 82 | public function equals( $target ) { 83 | if ( $this === $target ) { 84 | return true; 85 | } 86 | 87 | return $target instanceof self 88 | && $target->serialization === $this->serialization; 89 | } 90 | 91 | abstract public function __serialize(): array; 92 | 93 | abstract public function __unserialize( array $serialized ): void; 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/Entity/StatementListProvidingEntity.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class MapValueHasher { 18 | 19 | private bool $isOrdered; 20 | 21 | public function __construct( bool $holdOrderIntoAccount = false ) { 22 | $this->isOrdered = $holdOrderIntoAccount; 23 | } 24 | 25 | /** 26 | * Computes and returns the hash of the provided map. 27 | * 28 | * @since 0.1 29 | * 30 | * @param Traversable|Reference[] $map 31 | * 32 | * @return string 33 | * @throws InvalidArgumentException 34 | */ 35 | public function hash( $map ) { 36 | if ( !is_iterable( $map ) ) { 37 | throw new InvalidArgumentException( '$map must be a Reference array or an instance of Traversable' ); 38 | } 39 | 40 | $hashes = []; 41 | 42 | foreach ( $map as $hashable ) { 43 | $hashes[] = $hashable->getHash(); 44 | } 45 | 46 | if ( !$this->isOrdered ) { 47 | sort( $hashes ); 48 | } 49 | 50 | return sha1( implode( '|', $hashes ) ); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/LegacyIdInterpreter.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class LegacyIdInterpreter { 23 | 24 | /** 25 | * @param string $entityType 26 | * @param int|float|string $numericId 27 | * 28 | * @return EntityId 29 | * @throws InvalidArgumentException 30 | */ 31 | public static function newIdFromTypeAndNumber( $entityType, $numericId ) { 32 | if ( $entityType === 'item' ) { 33 | return ItemId::newFromNumber( $numericId ); 34 | } elseif ( $entityType === 'property' ) { 35 | return NumericPropertyId::newFromNumber( $numericId ); 36 | } 37 | 38 | throw new InvalidArgumentException( 'Invalid entityType ' . $entityType ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/PropertyIdProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface PropertyIdProvider { 16 | 17 | /** 18 | * @since 1.1 19 | * 20 | * @return PropertyId 21 | */ 22 | public function getPropertyId(); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Reference.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Reference implements Countable { 20 | 21 | /** 22 | * @var SnakList 23 | */ 24 | private $snaks; 25 | 26 | /** 27 | * An array of Snak objects is only supported since version 1.1. 28 | * 29 | * @param Snak[]|SnakList $snaks 30 | * @throws InvalidArgumentException 31 | */ 32 | public function __construct( $snaks = [] ) { 33 | if ( is_array( $snaks ) ) { 34 | $snaks = new SnakList( $snaks ); 35 | } 36 | 37 | if ( !( $snaks instanceof SnakList ) ) { 38 | throw new InvalidArgumentException( '$snaks must be an array or an instance of SnakList' ); 39 | } 40 | 41 | $this->snaks = $snaks; 42 | } 43 | 44 | /** 45 | * Returns the property snaks that make up this reference. 46 | * Modification of the snaks should NOT happen through this getter. 47 | * 48 | * @since 0.1 49 | * 50 | * @return SnakList 51 | */ 52 | public function getSnaks() { 53 | return $this->snaks; 54 | } 55 | 56 | /** 57 | * @see Countable::count 58 | * 59 | * @since 0.3 60 | * 61 | * @return int 62 | */ 63 | public function count(): int { 64 | return count( $this->snaks ); 65 | } 66 | 67 | /** 68 | * @since 2.6 69 | * 70 | * @return bool 71 | */ 72 | public function isEmpty() { 73 | return $this->snaks->isEmpty(); 74 | } 75 | 76 | /** 77 | * @since 0.1 78 | * 79 | * @return string 80 | */ 81 | public function getHash() { 82 | // For considering the reference snaks' property order without actually manipulating the 83 | // reference snaks's order, a new SnakList is generated. The new SnakList is ordered 84 | // by property and its hash is returned. 85 | $orderedSnaks = new SnakList( $this->snaks ); 86 | 87 | $orderedSnaks->orderByProperty(); 88 | 89 | return $orderedSnaks->getHash(); 90 | } 91 | 92 | /** 93 | * 94 | * The comparison is done purely value based, ignoring the order of the snaks. 95 | * 96 | * @since 0.3 97 | * 98 | * @param mixed $target 99 | * 100 | * @return bool 101 | */ 102 | public function equals( $target ) { 103 | if ( $this === $target ) { 104 | return true; 105 | } 106 | 107 | return $target instanceof self 108 | && $this->snaks->equals( $target->snaks ); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/ReferenceList.php: -------------------------------------------------------------------------------- 1 | 23 | * @author H. Snater < mediawiki@snater.com > 24 | * @author Thiemo Kreuz 25 | * @author Bene* < benestar.wikimedia@gmail.com > 26 | */ 27 | class ReferenceList implements Countable, IteratorAggregate, Serializable { 28 | 29 | /** 30 | * @var Reference[] Ordered list or references, indexed by SPL object hash. 31 | */ 32 | private $references = []; 33 | 34 | /** 35 | * @param Reference[]|Traversable $references 36 | * 37 | * @throws InvalidArgumentException 38 | */ 39 | public function __construct( $references = [] ) { 40 | if ( !is_iterable( $references ) ) { 41 | throw new InvalidArgumentException( '$references must be an array or an instance of Traversable' ); 42 | } 43 | 44 | foreach ( $references as $reference ) { 45 | if ( !( $reference instanceof Reference ) ) { 46 | throw new InvalidArgumentException( 'Every element in $references must be an instance of Reference' ); 47 | } 48 | 49 | $this->addReference( $reference ); 50 | } 51 | } 52 | 53 | /** 54 | * Adds the provided reference to the list. 55 | * Empty references are ignored. 56 | * 57 | * @since 0.1 58 | * 59 | * @param Reference $reference 60 | * @param int|null $index New position of the added reference, or null to append. 61 | * 62 | * @throws InvalidArgumentException 63 | */ 64 | public function addReference( Reference $reference, $index = null ) { 65 | if ( $index !== null && ( !is_int( $index ) || $index < 0 ) ) { 66 | throw new InvalidArgumentException( '$index must be a non-negative integer or null' ); 67 | } 68 | 69 | if ( $reference->isEmpty() ) { 70 | return; 71 | } 72 | 73 | $splHash = spl_object_hash( $reference ); 74 | 75 | if ( array_key_exists( $splHash, $this->references ) ) { 76 | return; 77 | } 78 | 79 | if ( $index === null || $index >= count( $this->references ) ) { 80 | // Append object to the end of the reference list. 81 | $this->references[$splHash] = $reference; 82 | } else { 83 | $this->insertReferenceAtIndex( $reference, $index ); 84 | } 85 | } 86 | 87 | /** 88 | * @since 1.1 89 | * 90 | * @param Snak ...$snaks 91 | * (passing a single Snak[] is still supported but deprecated) 92 | * 93 | * @throws InvalidArgumentException 94 | */ 95 | public function addNewReference( ...$snaks ) { 96 | if ( count( $snaks ) === 1 && is_array( $snaks[0] ) ) { 97 | // TODO stop supporting this 98 | $snaks = $snaks[0]; 99 | } 100 | 101 | $this->addReference( new Reference( $snaks ) ); 102 | } 103 | 104 | /** 105 | * @param Reference $reference 106 | * @param int $index 107 | */ 108 | private function insertReferenceAtIndex( Reference $reference, $index ) { 109 | if ( !is_int( $index ) ) { 110 | throw new InvalidArgumentException( '$index must be an integer' ); 111 | } 112 | 113 | $splHash = spl_object_hash( $reference ); 114 | 115 | $this->references = array_merge( 116 | array_slice( $this->references, 0, $index ), 117 | [ $splHash => $reference ], 118 | array_slice( $this->references, $index ) 119 | ); 120 | } 121 | 122 | /** 123 | * Returns if the list contains a reference with the same hash as the provided reference. 124 | * 125 | * @since 0.1 126 | * 127 | * @param Reference $reference 128 | * 129 | * @return bool 130 | */ 131 | public function hasReference( Reference $reference ) { 132 | return $this->hasReferenceHash( $reference->getHash() ); 133 | } 134 | 135 | /** 136 | * Returns the index of the Reference object or false if the Reference could not be found. 137 | * 138 | * @since 0.5 139 | * 140 | * @param Reference $reference 141 | * 142 | * @return int|bool 143 | */ 144 | public function indexOf( Reference $reference ) { 145 | $index = 0; 146 | 147 | foreach ( $this->references as $ref ) { 148 | if ( $ref === $reference ) { 149 | return $index; 150 | } 151 | 152 | $index++; 153 | } 154 | 155 | return false; 156 | } 157 | 158 | /** 159 | * Removes the reference with the same hash as the provided reference if such a reference exists in the list. 160 | * 161 | * @since 0.1 162 | * 163 | * @param Reference $reference 164 | */ 165 | public function removeReference( Reference $reference ) { 166 | $this->removeReferenceHash( $reference->getHash() ); 167 | } 168 | 169 | /** 170 | * Returns if the list contains a reference with the provided hash. 171 | * 172 | * @since 0.3 173 | * 174 | * @param string $referenceHash 175 | * 176 | * @return bool 177 | */ 178 | public function hasReferenceHash( $referenceHash ) { 179 | return $this->getReference( $referenceHash ) !== null; 180 | } 181 | 182 | /** 183 | * Looks for the first Reference object in this list with the provided hash. 184 | * Removes all occurences of that object. 185 | * 186 | * @since 0.3 187 | * 188 | * @param string $referenceHash ` 189 | */ 190 | public function removeReferenceHash( $referenceHash ) { 191 | $reference = $this->getReference( $referenceHash ); 192 | 193 | if ( $reference === null ) { 194 | return; 195 | } 196 | 197 | foreach ( $this->references as $splObjectHash => $ref ) { 198 | if ( $ref === $reference ) { 199 | unset( $this->references[$splObjectHash] ); 200 | } 201 | } 202 | } 203 | 204 | /** 205 | * Returns the first Reference object with the provided hash, or 206 | * null if there is no such reference in the list. 207 | * 208 | * @since 0.3 209 | * 210 | * @param string $referenceHash 211 | * 212 | * @return Reference|null 213 | */ 214 | public function getReference( $referenceHash ) { 215 | foreach ( $this->references as $reference ) { 216 | if ( $reference->getHash() === $referenceHash ) { 217 | return $reference; 218 | } 219 | } 220 | 221 | return null; 222 | } 223 | 224 | /** 225 | * @see Serializable::serialize 226 | * 227 | * @since 2.1 228 | * 229 | * @return string 230 | */ 231 | public function serialize() { 232 | return serialize( array_values( $this->references ) ); 233 | } 234 | 235 | /** 236 | * @see https://wiki.php.net/rfc/custom_object_serialization 237 | * 238 | * @return array 239 | */ 240 | public function __serialize() { 241 | return [ 242 | 'references' => array_values( $this->references ), 243 | ]; 244 | } 245 | 246 | /** 247 | * @see https://wiki.php.net/rfc/custom_object_serialization 248 | * 249 | * @param array $data 250 | */ 251 | public function __unserialize( array $data ): void { 252 | $this->references = $data['references']; 253 | } 254 | 255 | /** 256 | * @see Serializable::unserialize 257 | * 258 | * @since 2.1 259 | * 260 | * @param string $serialized 261 | */ 262 | public function unserialize( $serialized ) { 263 | $this->__construct( unserialize( $serialized ) ); 264 | } 265 | 266 | /** 267 | * @since 4.4 268 | * 269 | * @return bool 270 | */ 271 | public function isEmpty() { 272 | return $this->references === []; 273 | } 274 | 275 | /** 276 | * The hash is purely valuer based. Order of the elements in the array is not held into account. 277 | * 278 | * @since 0.3 279 | * 280 | * @return string 281 | */ 282 | public function getValueHash() { 283 | $hasher = new MapValueHasher(); 284 | return $hasher->hash( $this->references ); 285 | } 286 | 287 | /** 288 | * The comparison is done purely value based, ignoring the order of the elements in the array. 289 | * 290 | * @since 0.3 291 | * 292 | * @param mixed $target 293 | * 294 | * @return bool 295 | */ 296 | public function equals( $target ) { 297 | if ( $this === $target ) { 298 | return true; 299 | } 300 | 301 | return $target instanceof self 302 | && $this->getValueHash() === $target->getValueHash(); 303 | } 304 | 305 | /** 306 | * @see Countable::count 307 | * 308 | * @return int 309 | */ 310 | public function count(): int { 311 | return count( $this->references ); 312 | } 313 | 314 | /** 315 | * @see IteratorAggregate::getIterator 316 | * 317 | * @since 5.0 318 | * 319 | * @return Traversable 320 | */ 321 | public function getIterator(): Traversable { 322 | return new ArrayIterator( array_values( $this->references ) ); 323 | } 324 | 325 | } 326 | -------------------------------------------------------------------------------- /src/SiteLink.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Michał Łazowik 20 | * @author Thiemo Kreuz 21 | */ 22 | class SiteLink { 23 | 24 | /** 25 | * @var string 26 | */ 27 | private $siteId; 28 | 29 | /** 30 | * @var string 31 | */ 32 | private $pageName; 33 | 34 | /** 35 | * @var ItemIdSet 36 | */ 37 | private $badges; 38 | 39 | /** 40 | * @param string $siteId 41 | * @param string $pageName 42 | * @param ItemIdSet|ItemId[]|null $badges 43 | * 44 | * @throws InvalidArgumentException 45 | */ 46 | public function __construct( $siteId, $pageName, $badges = null ) { 47 | if ( !is_string( $siteId ) || $siteId === '' ) { 48 | throw new InvalidArgumentException( '$siteId must be a non-empty string' ); 49 | } 50 | 51 | if ( !is_string( $pageName ) || $pageName === '' ) { 52 | throw new InvalidArgumentException( '$pageName must be a non-empty string' ); 53 | } 54 | 55 | $this->siteId = $siteId; 56 | $this->pageName = $pageName; 57 | $this->setBadges( $badges ); 58 | } 59 | 60 | /** 61 | * @param ItemIdSet|ItemId[]|null $badges 62 | * 63 | * @throws InvalidArgumentException 64 | */ 65 | private function setBadges( $badges ) { 66 | if ( $badges === null ) { 67 | $badges = new ItemIdSet(); 68 | } elseif ( is_array( $badges ) ) { 69 | $badges = new ItemIdSet( $badges ); 70 | } elseif ( !( $badges instanceof ItemIdSet ) ) { 71 | throw new InvalidArgumentException( 72 | '$badges must be an instance of ItemIdSet, an array of instances of ItemId, or null' 73 | ); 74 | } 75 | 76 | $this->badges = $badges; 77 | } 78 | 79 | /** 80 | * @since 0.4 81 | * 82 | * @return string 83 | */ 84 | public function getSiteId() { 85 | return $this->siteId; 86 | } 87 | 88 | /** 89 | * @since 0.4 90 | * 91 | * @return string 92 | */ 93 | public function getPageName() { 94 | return $this->pageName; 95 | } 96 | 97 | /** 98 | * Badges are not order dependent. 99 | * 100 | * @since 0.5 101 | * 102 | * @return ItemId[] 103 | */ 104 | public function getBadges() { 105 | return array_values( iterator_to_array( $this->badges ) ); 106 | } 107 | 108 | /** 109 | * 110 | * @since 0.7.4 111 | * 112 | * @param mixed $target 113 | * 114 | * @return bool 115 | */ 116 | public function equals( $target ) { 117 | if ( $this === $target ) { 118 | return true; 119 | } 120 | 121 | return $target instanceof self 122 | && $this->siteId === $target->siteId 123 | && $this->pageName === $target->pageName 124 | && $this->badges->equals( $target->badges ); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/SiteLinkList.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class SiteLinkList implements IteratorAggregate, Countable { 25 | 26 | /** 27 | * @var SiteLink[] 28 | */ 29 | private $siteLinks = []; 30 | 31 | /** 32 | * @param iterable|SiteLink[] $siteLinks Can be a non-array iterable since 8.1 33 | * 34 | * @throws InvalidArgumentException 35 | */ 36 | public function __construct( /* iterable */ $siteLinks = [] ) { 37 | if ( !is_iterable( $siteLinks ) ) { 38 | throw new InvalidArgumentException( '$siteLinks must be iterable' ); 39 | } 40 | 41 | foreach ( $siteLinks as $siteLink ) { 42 | if ( !( $siteLink instanceof SiteLink ) ) { 43 | throw new InvalidArgumentException( 'Every element of $siteLinks must be an instance of SiteLink' ); 44 | } 45 | 46 | $this->addSiteLink( $siteLink ); 47 | } 48 | } 49 | 50 | /** 51 | * @since 0.8 52 | * 53 | * @param SiteLink $link 54 | * 55 | * @throws InvalidArgumentException 56 | */ 57 | public function addSiteLink( SiteLink $link ) { 58 | if ( array_key_exists( $link->getSiteId(), $this->siteLinks ) ) { 59 | throw new InvalidArgumentException( 'Duplicate site id: ' . $link->getSiteId() ); 60 | } 61 | 62 | $this->siteLinks[$link->getSiteId()] = $link; 63 | } 64 | 65 | /** 66 | * @see SiteLink::__construct 67 | * 68 | * @since 0.8 69 | * 70 | * @param string $siteId 71 | * @param string $pageName 72 | * @param ItemIdSet|ItemId[]|null $badges 73 | * 74 | * @throws InvalidArgumentException 75 | */ 76 | public function addNewSiteLink( $siteId, $pageName, $badges = null ) { 77 | $this->addSiteLink( new SiteLink( $siteId, $pageName, $badges ) ); 78 | } 79 | 80 | /** 81 | * @since 2.5 82 | * 83 | * @param SiteLink $link 84 | */ 85 | public function setSiteLink( SiteLink $link ) { 86 | $this->siteLinks[$link->getSiteId()] = $link; 87 | } 88 | 89 | /** 90 | * @since 2.5 91 | * 92 | * @param string $siteId 93 | * @param string $pageName 94 | * @param ItemIdSet|ItemId[]|null $badges 95 | */ 96 | public function setNewSiteLink( $siteId, $pageName, $badges = null ) { 97 | $this->setSiteLink( new SiteLink( $siteId, $pageName, $badges ) ); 98 | } 99 | 100 | /** 101 | * @see IteratorAggregate::getIterator 102 | * 103 | * Returns an Iterator of SiteLink in which the keys are the site ids. 104 | * 105 | * @return Traversable 106 | */ 107 | public function getIterator(): Traversable { 108 | return new ArrayIterator( $this->siteLinks ); 109 | } 110 | 111 | /** 112 | * @see Countable::count 113 | * 114 | * @return int 115 | */ 116 | public function count(): int { 117 | return count( $this->siteLinks ); 118 | } 119 | 120 | /** 121 | * @param string $siteId 122 | * 123 | * @return SiteLink 124 | * @throws OutOfBoundsException 125 | * @throws InvalidArgumentException 126 | */ 127 | public function getBySiteId( $siteId ) { 128 | if ( !$this->hasLinkWithSiteId( $siteId ) ) { 129 | throw new OutOfBoundsException( 'SiteLink with siteId "' . $siteId . '" not found' ); 130 | } 131 | 132 | return $this->siteLinks[$siteId]; 133 | } 134 | 135 | /** 136 | * @since 0.8 137 | * 138 | * @param string $siteId 139 | * 140 | * @return bool 141 | * @throws InvalidArgumentException 142 | */ 143 | public function hasLinkWithSiteId( $siteId ) { 144 | if ( !is_string( $siteId ) ) { 145 | throw new InvalidArgumentException( '$siteId must be a string; got ' . get_debug_type( $siteId ) ); 146 | } 147 | 148 | return array_key_exists( $siteId, $this->siteLinks ); 149 | } 150 | 151 | /** 152 | * 153 | * @since 0.7.4 154 | * 155 | * @param mixed $target 156 | * 157 | * @return bool 158 | */ 159 | public function equals( $target ) { 160 | if ( $this === $target ) { 161 | return true; 162 | } 163 | 164 | return $target instanceof self 165 | && $this->siteLinks == $target->siteLinks; 166 | } 167 | 168 | /** 169 | * @since 1.0 170 | * 171 | * @return bool 172 | */ 173 | public function isEmpty() { 174 | return $this->siteLinks === []; 175 | } 176 | 177 | /** 178 | * @since 2.5 179 | * 180 | * @return SiteLink[] Array indexed by site id. 181 | */ 182 | public function toArray() { 183 | return $this->siteLinks; 184 | } 185 | 186 | /** 187 | * @since 0.8 188 | * 189 | * @param string $siteId 190 | * 191 | * @throws InvalidArgumentException 192 | */ 193 | public function removeLinkWithSiteId( $siteId ) { 194 | if ( !is_string( $siteId ) ) { 195 | throw new InvalidArgumentException( '$siteId must be a string; got ' . get_debug_type( $siteId ) ); 196 | } 197 | 198 | unset( $this->siteLinks[$siteId] ); 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/Snak/DerivedPropertyValueSnak.php: -------------------------------------------------------------------------------- 1 | equal other DerivedPropertyValueSnaks with the same base PropertyValueSnak. 22 | * This object will NOT ->equal any PropertyValueSnaks. 23 | * A newPropertyValueSnak method is provided for comparison convenience. 24 | * 25 | * @since 3.1 26 | * 27 | * @license GPL-2.0-or-later 28 | * @author Addshore 29 | */ 30 | class DerivedPropertyValueSnak extends PropertyValueSnak { 31 | 32 | /** 33 | * @var DataValue[] 34 | */ 35 | private $derivedDataValues = []; 36 | 37 | /** 38 | * @param PropertyId|EntityId|int $propertyId 39 | * @param DataValue $dataValue 40 | * @param DataValue[] $derivedDataValues 41 | * 42 | * @throws InvalidArgumentException 43 | */ 44 | public function __construct( 45 | $propertyId, 46 | DataValue $dataValue, 47 | array $derivedDataValues 48 | ) { 49 | parent::__construct( $propertyId, $dataValue ); 50 | 51 | foreach ( $derivedDataValues as $key => $extensionDataValue ) { 52 | if ( !( $extensionDataValue instanceof DataValue ) || !is_string( $key ) ) { 53 | throw new InvalidArgumentException( 54 | '$derivedDataValues must be an array of DataValue objects with string keys' 55 | ); 56 | } 57 | } 58 | 59 | $this->derivedDataValues = $derivedDataValues; 60 | } 61 | 62 | /** 63 | * @return DataValue[] with string keys 64 | */ 65 | public function getDerivedDataValues() { 66 | return $this->derivedDataValues; 67 | } 68 | 69 | /** 70 | * @param string $key 71 | * 72 | * @return DataValue|null 73 | */ 74 | public function getDerivedDataValue( $key ) { 75 | return $this->derivedDataValues[$key] ?? null; 76 | } 77 | 78 | /** 79 | * @return PropertyValueSnak 80 | */ 81 | public function newPropertyValueSnak() { 82 | return new PropertyValueSnak( $this->propertyId, $this->dataValue ); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/Snak/PropertyNoValueSnak.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class PropertyNoValueSnak extends SnakObject { 15 | 16 | /** 17 | * @see Snak::getType 18 | * 19 | * @since 0.2 20 | * 21 | * @return string 22 | */ 23 | public function getType() { 24 | return 'novalue'; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Snak/PropertySomeValueSnak.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class PropertySomeValueSnak extends SnakObject { 15 | 16 | /** 17 | * @see Snak::getType 18 | * 19 | * @since 0.2 20 | * 21 | * @return string 22 | */ 23 | public function getType() { 24 | return 'somevalue'; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Snak/PropertyValueSnak.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Daniel Kinzler 19 | */ 20 | class PropertyValueSnak extends SnakObject { 21 | 22 | /** @var DataValue */ 23 | protected $dataValue; 24 | 25 | /** 26 | * Support for passing in an EntityId instance that is not a PropertyId instance has 27 | * been deprecated since 0.5. 28 | * 29 | * @since 0.1 30 | * 31 | * @param PropertyId|EntityId|int $propertyId 32 | * @param DataValue $dataValue 33 | */ 34 | public function __construct( $propertyId, DataValue $dataValue ) { 35 | parent::__construct( $propertyId ); 36 | $this->dataValue = $dataValue; 37 | } 38 | 39 | /** 40 | * Returns the value of the property value snak. 41 | * 42 | * @since 0.1 43 | * 44 | * @return DataValue 45 | */ 46 | public function getDataValue() { 47 | return $this->dataValue; 48 | } 49 | 50 | /** 51 | * The serialization to use for hashing, for compatibility reasons this is 52 | * equivalent to the old (pre 7.4) PHP serialization. 53 | */ 54 | public function getSerializationForHash(): string { 55 | $propertyIdSerialization = $this->propertyId->getSerialization(); 56 | $innerSerialization = 'a:2:{i:0;s:' . strlen( $propertyIdSerialization ) . ':"' . 57 | $propertyIdSerialization . '";i:1;' . $this->getDataValueSerializationForHash() . '}'; 58 | 59 | return 'C:' . strlen( static::class ) . ':"' . static::class . 60 | '":' . strlen( $innerSerialization ) . ':{' . $innerSerialization . '}'; 61 | } 62 | 63 | /** 64 | * The serialization to use for hashing, for compatibility reasons this is 65 | * equivalent to the old (pre 7.4) PHP serialization. 66 | */ 67 | private function getDataValueSerializationForHash(): string { 68 | if ( method_exists( $this->dataValue, 'getSerializationForHash' ) ) { 69 | // If our DataValue provides/ needs a special serialization for 70 | // hashing, use it (currently only EntityIdValue). 71 | // @phan-suppress-next-line PhanUndeclaredMethod 72 | return $this->dataValue->getSerializationForHash(); 73 | } 74 | $innerSerialization = $this->dataValue->serialize(); 75 | $className = get_class( $this->dataValue ); 76 | 77 | return 'C:' . strlen( $className ) . ':"' . $className . 78 | '":' . strlen( $innerSerialization ?? '' ) . ':{' . $innerSerialization . '}'; 79 | } 80 | 81 | /** 82 | * @see Serializable::serialize 83 | * 84 | * @since 7.0 serialization format changed in an incompatible way 85 | * 86 | * @return string 87 | */ 88 | public function serialize() { 89 | return serialize( $this->__serialize() ); 90 | } 91 | 92 | /** 93 | * @see Serializable::unserialize 94 | * 95 | * @since 0.1 96 | * 97 | * @param string $serialized 98 | */ 99 | public function unserialize( $serialized ) { 100 | $this->__unserialize( unserialize( $serialized ) ); 101 | } 102 | 103 | public function __serialize(): array { 104 | return [ $this->propertyId->getSerialization(), $this->dataValue ]; 105 | } 106 | 107 | public function __unserialize( array $serialized ): void { 108 | [ $propertyId, $this->dataValue ] = $serialized; 109 | 110 | if ( is_string( $propertyId ) ) { 111 | $this->propertyId = new NumericPropertyId( $propertyId ); 112 | } else { 113 | // Backwards compatibility with the previous serialization format 114 | $this->propertyId = NumericPropertyId::newFromNumber( $propertyId ); 115 | } 116 | } 117 | 118 | /** 119 | * @see Snak::getType 120 | * 121 | * @since 0.2 122 | * 123 | * @return string 124 | */ 125 | public function getType() { 126 | return 'value'; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/Snak/Snak.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface Snak extends Serializable, PropertyIdProvider { 18 | 19 | /** 20 | * Returns a string that can be used to identify the type of snak. 21 | * 22 | * @since 0.2 23 | * 24 | * @return string 25 | */ 26 | public function getType(); 27 | 28 | /** 29 | * 30 | * @return string 31 | */ 32 | public function getHash(); 33 | 34 | /** 35 | * @param mixed $value 36 | * @return bool 37 | */ 38 | public function equals( $value ); 39 | 40 | public function __serialize(): array; 41 | 42 | public function __unserialize( array $serialized ): void; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Snak/SnakList.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Addshore 19 | */ 20 | class SnakList extends ArrayObject { 21 | 22 | /** 23 | * Maps snak hashes to their offsets. 24 | * 25 | * @var array [ snak hash (string) => snak offset (string|int) ] 26 | */ 27 | private $offsetHashes = []; 28 | 29 | /** 30 | * @var int 31 | */ 32 | private $indexOffset = 0; 33 | 34 | /** 35 | * @param Snak[]|Traversable $snaks 36 | * 37 | * @throws InvalidArgumentException 38 | */ 39 | public function __construct( $snaks = [] ) { 40 | if ( !is_iterable( $snaks ) ) { 41 | throw new InvalidArgumentException( '$snaks must be an array or an instance of Traversable' ); 42 | } 43 | 44 | foreach ( $snaks as $index => $snak ) { 45 | $this->setElement( $index, $snak ); 46 | } 47 | } 48 | 49 | /** 50 | * @since 0.1 51 | * 52 | * @param string $snakHash 53 | * 54 | * @return bool 55 | */ 56 | public function hasSnakHash( $snakHash ) { 57 | return array_key_exists( $snakHash, $this->offsetHashes ); 58 | } 59 | 60 | /** 61 | * @since 0.1 62 | * 63 | * @param string $snakHash 64 | */ 65 | public function removeSnakHash( $snakHash ) { 66 | if ( $this->hasSnakHash( $snakHash ) ) { 67 | $offset = $this->offsetHashes[$snakHash]; 68 | $this->offsetUnset( $offset ); 69 | } 70 | } 71 | 72 | /** 73 | * @since 0.1 74 | * 75 | * @param Snak $snak 76 | * 77 | * @return bool Indicates if the snak was added or not. 78 | */ 79 | public function addSnak( Snak $snak ) { 80 | if ( $this->hasSnak( $snak ) ) { 81 | return false; 82 | } 83 | 84 | $this->append( $snak ); 85 | return true; 86 | } 87 | 88 | /** 89 | * @since 0.1 90 | * 91 | * @param Snak $snak 92 | * 93 | * @return bool 94 | */ 95 | public function hasSnak( Snak $snak ) { 96 | return $this->hasSnakHash( $snak->getHash() ); 97 | } 98 | 99 | /** 100 | * @since 0.1 101 | * 102 | * @param Snak $snak 103 | */ 104 | public function removeSnak( Snak $snak ) { 105 | $this->removeSnakHash( $snak->getHash() ); 106 | } 107 | 108 | /** 109 | * @since 0.1 110 | * 111 | * @param string $snakHash 112 | * 113 | * @return Snak|bool 114 | */ 115 | public function getSnak( $snakHash ) { 116 | if ( !$this->hasSnakHash( $snakHash ) ) { 117 | return false; 118 | } 119 | 120 | $offset = $this->offsetHashes[$snakHash]; 121 | return $this->offsetGet( $offset ); 122 | } 123 | 124 | /** 125 | * 126 | * The comparison is done purely value based, ignoring the order of the elements in the array. 127 | * 128 | * @since 0.3 129 | * 130 | * @param mixed $target 131 | * 132 | * @return bool 133 | */ 134 | public function equals( $target ) { 135 | if ( $this === $target ) { 136 | return true; 137 | } 138 | 139 | return $target instanceof self 140 | && $this->getHash() === $target->getHash(); 141 | } 142 | 143 | /** 144 | * The hash is purely value based. Order of the elements in the array is not held into account. 145 | * 146 | * @since 0.1 147 | * 148 | * @return string 149 | */ 150 | public function getHash() { 151 | $hasher = new MapValueHasher(); 152 | return $hasher->hash( $this ); 153 | } 154 | 155 | /** 156 | * Groups snaks by property, and optionally orders them. 157 | * 158 | * @param string[] $order List of property ID strings to order by. Snaks with other properties 159 | * will also be grouped, but put at the end, in the order each property appeared first in the 160 | * original list. 161 | * 162 | * @since 0.5 163 | */ 164 | public function orderByProperty( array $order = [] ) { 165 | $byProperty = array_fill_keys( $order, [] ); 166 | 167 | /** @var Snak $snak */ 168 | foreach ( $this as $snak ) { 169 | $byProperty[$snak->getPropertyId()->getSerialization()][] = $snak; 170 | } 171 | 172 | $ordered = []; 173 | foreach ( $byProperty as $snaks ) { 174 | $ordered = array_merge( $ordered, $snaks ); 175 | } 176 | 177 | $this->exchangeArray( $ordered ); 178 | 179 | $index = 0; 180 | foreach ( $ordered as $snak ) { 181 | $this->offsetHashes[$snak->getHash()] = $index++; 182 | } 183 | } 184 | 185 | /** 186 | * Finds a new offset for when appending an element. 187 | * The base class does this, so it would be better to integrate, 188 | * but there does not appear to be any way to do this... 189 | * 190 | * @return int 191 | */ 192 | private function getNewOffset() { 193 | while ( $this->offsetExists( $this->indexOffset ) ) { 194 | $this->indexOffset++; 195 | } 196 | 197 | return $this->indexOffset; 198 | } 199 | 200 | /** 201 | * @see ArrayObject::offsetUnset 202 | * 203 | * @since 0.1 204 | * 205 | * @param int|string $index 206 | */ 207 | public function offsetUnset( $index ): void { 208 | if ( $this->offsetExists( $index ) ) { 209 | /** 210 | * @var Snak $element 211 | */ 212 | $element = $this->offsetGet( $index ); 213 | $hash = $element->getHash(); 214 | unset( $this->offsetHashes[$hash] ); 215 | 216 | parent::offsetUnset( $index ); 217 | } 218 | } 219 | 220 | /** 221 | * @see ArrayObject::append 222 | * 223 | * @param Snak $value 224 | */ 225 | public function append( $value ): void { 226 | $this->setElement( null, $value ); 227 | } 228 | 229 | /** 230 | * @see ArrayObject::offsetSet() 231 | * 232 | * @param int|string $index 233 | * @param Snak $value 234 | */ 235 | public function offsetSet( $index, $value ): void { 236 | $this->setElement( $index, $value ); 237 | } 238 | 239 | /** 240 | * Method that actually sets the element and holds 241 | * all common code needed for set operations, including 242 | * type checking and offset resolving. 243 | * 244 | * If null is supplied as an index, the next new offset 245 | * will be assigned. 246 | * 247 | * @param int|string|null $index 248 | * @param Snak $value 249 | * 250 | * @throws InvalidArgumentException 251 | */ 252 | private function setElement( $index, $value ) { 253 | if ( !( $value instanceof Snak ) ) { 254 | throw new InvalidArgumentException( '$value must be a Snak' ); 255 | } 256 | 257 | if ( $this->hasSnak( $value ) ) { 258 | return; 259 | } 260 | 261 | $index ??= $this->getNewOffset(); 262 | 263 | $hash = $value->getHash(); 264 | $this->offsetHashes[$hash] = $index; 265 | parent::offsetSet( $index, $value ); 266 | } 267 | 268 | /** 269 | * @see Serializable::serialize 270 | * 271 | * @return string 272 | */ 273 | public function serialize(): string { 274 | return serialize( $this->__serialize() ); 275 | } 276 | 277 | /** 278 | * @see Serializable::unserialize 279 | * 280 | * @param string $serialized 281 | */ 282 | public function unserialize( $serialized ): void { 283 | $serializationData = unserialize( $serialized ); 284 | $this->__unserialize( $serializationData ); 285 | } 286 | 287 | /** 288 | * @see https://wiki.php.net/rfc/custom_object_serialization 289 | * 290 | * @return array 291 | */ 292 | public function __serialize(): array { 293 | return [ 294 | 'data' => $this->getArrayCopy(), 295 | 'index' => $this->indexOffset, 296 | ]; 297 | } 298 | 299 | /** 300 | * @see https://wiki.php.net/rfc/custom_object_serialization 301 | * 302 | * @param array $data 303 | */ 304 | public function __unserialize( $data ): void { 305 | foreach ( $data['data'] as $offset => $value ) { 306 | // Just set the element, bypassing checks and offset resolving, 307 | // as these elements have already gone through this. 308 | parent::offsetSet( $offset, $value ); 309 | } 310 | 311 | $this->indexOffset = $data['index']; 312 | } 313 | 314 | /** 315 | * Returns if the ArrayObject has no elements. 316 | * 317 | * @return bool 318 | */ 319 | public function isEmpty() { 320 | return !$this->getIterator()->valid(); 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /src/Snak/SnakObject.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | abstract class SnakObject implements Snak { 21 | 22 | /** 23 | * @since 0.1 24 | * 25 | * @var PropertyId 26 | */ 27 | protected $propertyId; 28 | 29 | /** 30 | * Support for passing in an EntityId instance that is not a PropertyId instance has 31 | * been deprecated since 0.5. 32 | * 33 | * @since 0.1 34 | * 35 | * @param PropertyId|EntityId|int $propertyId 36 | * 37 | * @throws InvalidArgumentException 38 | */ 39 | public function __construct( $propertyId ) { 40 | if ( is_int( $propertyId ) ) { 41 | $propertyId = NumericPropertyId::newFromNumber( $propertyId ); 42 | } 43 | 44 | if ( !( $propertyId instanceof EntityId ) ) { 45 | throw new InvalidArgumentException( '$propertyId must be an instance of EntityId' ); 46 | } 47 | 48 | if ( $propertyId->getEntityType() !== Property::ENTITY_TYPE ) { 49 | throw new InvalidArgumentException( '$propertyId must have an entityType of ' . Property::ENTITY_TYPE ); 50 | } 51 | 52 | if ( !( $propertyId instanceof PropertyId ) ) { 53 | $propertyId = new NumericPropertyId( $propertyId->getSerialization() ); 54 | } 55 | 56 | $this->propertyId = $propertyId; 57 | } 58 | 59 | /** 60 | * @see PropertyIdProvider::getPropertyId 61 | * 62 | * @since 0.1 63 | * 64 | * @return PropertyId 65 | */ 66 | public function getPropertyId() { 67 | return $this->propertyId; 68 | } 69 | 70 | public function getHash(): string { 71 | return sha1( $this->getSerializationForHash() ); 72 | } 73 | 74 | /** 75 | * The serialization to use for hashing, for compatibility reasons this is 76 | * equivalent to the old (pre 7.4) PHP serialization. 77 | */ 78 | public function getSerializationForHash(): string { 79 | $data = $this->serialize(); 80 | return 'C:' . strlen( static::class ) . ':"' . static::class . 81 | '":' . strlen( $data ) . ':{' . $data . '}'; 82 | } 83 | 84 | /** 85 | * 86 | * @since 0.3 87 | * 88 | * @param mixed $target 89 | * 90 | * @return bool 91 | */ 92 | public function equals( $target ) { 93 | if ( $this === $target ) { 94 | return true; 95 | } 96 | 97 | return is_object( $target ) 98 | && get_called_class() === get_class( $target ) 99 | && $this->getHash() === $target->getHash(); 100 | } 101 | 102 | /** 103 | * @see Serializable::serialize 104 | * 105 | * @since 7.0 serialization format changed in an incompatible way 106 | * 107 | * @return string 108 | */ 109 | public function serialize() { 110 | return $this->propertyId->getSerialization(); 111 | } 112 | 113 | /** 114 | * @see Serializable::unserialize 115 | * 116 | * @since 0.1 117 | * 118 | * @param string $serialized 119 | */ 120 | public function unserialize( $serialized ) { 121 | try { 122 | $this->propertyId = new NumericPropertyId( $serialized ); 123 | } catch ( InvalidArgumentException $ex ) { 124 | // Backwards compatibility with the previous serialization format 125 | $this->propertyId = NumericPropertyId::newFromNumber( unserialize( $serialized ) ); 126 | } 127 | } 128 | 129 | public function __serialize(): array { 130 | return [ $this->propertyId->getSerialization() ]; 131 | } 132 | 133 | public function __unserialize( array $serialized ): void { 134 | $this->propertyId = new NumericPropertyId( $serialized[0] ); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/Snak/SnakRole.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class SnakRole { 14 | 15 | public const MAIN_SNAK = 0; 16 | public const QUALIFIER = 1; 17 | 18 | private function __construct() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Snak/TypedSnak.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class TypedSnak { 12 | 13 | /** 14 | * @var Snak 15 | */ 16 | private $snak; 17 | 18 | /** 19 | * @var string 20 | */ 21 | private $dataTypeId; 22 | 23 | /** 24 | * @param Snak $snak 25 | * @param string $dataTypeId 26 | */ 27 | public function __construct( Snak $snak, $dataTypeId ) { 28 | $this->snak = $snak; 29 | $this->dataTypeId = $dataTypeId; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getDataTypeId() { 36 | return $this->dataTypeId; 37 | } 38 | 39 | /** 40 | * @return Snak 41 | */ 42 | public function getSnak() { 43 | return $this->snak; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Statement/ReferencedStatementFilter.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ReferencedStatementFilter implements StatementFilter { 15 | 16 | /** 17 | * @since 4.4 18 | */ 19 | public const FILTER_TYPE = 'referenced'; 20 | 21 | /** 22 | * @param Statement $statement 23 | * 24 | * @return bool 25 | */ 26 | public function statementMatches( Statement $statement ) { 27 | return !$statement->getReferences()->isEmpty(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Statement/Statement.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Bene* < benestar.wikimedia@gmail.com > 21 | */ 22 | class Statement implements PropertyIdProvider { 23 | 24 | /** 25 | * Rank enum. Higher values are more preferred. 26 | * 27 | * @since 2.0 28 | */ 29 | public const RANK_PREFERRED = 2; 30 | public const RANK_NORMAL = 1; 31 | public const RANK_DEPRECATED = 0; 32 | 33 | /** 34 | * @var string|null 35 | */ 36 | private $guid = null; 37 | 38 | /** 39 | * @var Snak 40 | */ 41 | private $mainSnak; 42 | 43 | /** 44 | * The property value snaks making up the qualifiers for this statement. 45 | * 46 | * @var SnakList 47 | */ 48 | private $qualifiers; 49 | 50 | /** 51 | * @var ReferenceList 52 | */ 53 | private $references; 54 | 55 | /** 56 | * @var int element of the Statement::RANK_ enum 57 | */ 58 | private $rank = self::RANK_NORMAL; 59 | 60 | /** 61 | * @since 2.0 62 | */ 63 | public function __construct( 64 | Snak $mainSnak, 65 | ?SnakList $qualifiers = null, 66 | ?ReferenceList $references = null, 67 | ?string $guid = null 68 | ) { 69 | $this->mainSnak = $mainSnak; 70 | $this->qualifiers = $qualifiers ?: new SnakList(); 71 | $this->references = $references ?: new ReferenceList(); 72 | $this->setGuid( $guid ); 73 | } 74 | 75 | /** 76 | * @since 0.2 77 | * 78 | * @return string|null 79 | */ 80 | public function getGuid(): ?string { 81 | return $this->guid; 82 | } 83 | 84 | /** 85 | * Sets the GUID of this statement. 86 | * 87 | * @since 0.2 88 | * 89 | * @param string|null $guid 90 | */ 91 | public function setGuid( ?string $guid ): void { 92 | $this->guid = $guid; 93 | } 94 | 95 | /** 96 | * Returns the main value snak of this statement. 97 | * 98 | * @since 0.1 99 | * 100 | * @return Snak 101 | */ 102 | public function getMainSnak(): Snak { 103 | return $this->mainSnak; 104 | } 105 | 106 | /** 107 | * Sets the main value snak of this statement. 108 | * 109 | * @since 0.1 110 | * 111 | * @param Snak $mainSnak 112 | */ 113 | public function setMainSnak( Snak $mainSnak ): void { 114 | $this->mainSnak = $mainSnak; 115 | } 116 | 117 | /** 118 | * Returns the property value snaks making up the qualifiers for this statement. 119 | * 120 | * @since 0.1 121 | * 122 | * @return SnakList 123 | */ 124 | public function getQualifiers(): SnakList { 125 | return $this->qualifiers; 126 | } 127 | 128 | /** 129 | * Sets the property value snaks making up the qualifiers for this statement. 130 | * 131 | * @since 0.1 132 | * 133 | * @param SnakList $propertySnaks 134 | */ 135 | public function setQualifiers( SnakList $propertySnaks ): void { 136 | $this->qualifiers = $propertySnaks; 137 | } 138 | 139 | /** 140 | * Returns the references attached to this statement. 141 | * 142 | * @since 0.1 143 | * 144 | * @return ReferenceList 145 | */ 146 | public function getReferences(): ReferenceList { 147 | return $this->references; 148 | } 149 | 150 | /** 151 | * Sets the references attached to this statement. 152 | * 153 | * @since 0.1 154 | * 155 | * @param ReferenceList $references 156 | */ 157 | public function setReferences( ReferenceList $references ): void { 158 | $this->references = $references; 159 | } 160 | 161 | /** 162 | * @since 2.0 163 | * 164 | * @param Snak ...$snaks 165 | */ 166 | public function addNewReference( Snak ...$snaks ) { 167 | $this->references->addNewReference( ...$snaks ); 168 | } 169 | 170 | /** 171 | * Sets the rank of the statement. 172 | * The rank is an element of the Statement::RANK_ enum. 173 | * 174 | * @since 0.1 175 | * 176 | * @param int $rank 177 | * 178 | * @throws InvalidArgumentException 179 | */ 180 | public function setRank( int $rank ): void { 181 | $ranks = [ self::RANK_DEPRECATED, self::RANK_NORMAL, self::RANK_PREFERRED ]; 182 | 183 | if ( !in_array( $rank, $ranks, true ) ) { 184 | throw new InvalidArgumentException( 'Invalid rank specified for statement: ' . var_export( $rank, true ) ); 185 | } 186 | 187 | $this->rank = $rank; 188 | } 189 | 190 | /** 191 | * @since 0.1 192 | * 193 | * @return int 194 | */ 195 | public function getRank(): int { 196 | return $this->rank; 197 | } 198 | 199 | /** 200 | * @since 0.1 201 | * 202 | * @return string 203 | */ 204 | public function getHash(): string { 205 | $hashParts = [ 206 | sha1( $this->mainSnak->getHash() . $this->qualifiers->getHash() ), 207 | $this->rank, 208 | $this->references->getValueHash(), 209 | ]; 210 | return sha1( implode( '|', $hashParts ) ); 211 | } 212 | 213 | /** 214 | * Returns the id of the property of the main snak. 215 | * Short for ->getMainSnak()->getPropertyId() 216 | * 217 | * @see PropertyIdProvider::getPropertyId 218 | * 219 | * @since 0.2 220 | * 221 | * @return PropertyId 222 | */ 223 | public function getPropertyId(): PropertyId { 224 | return $this->getMainSnak()->getPropertyId(); 225 | } 226 | 227 | /** 228 | * Returns a list of all Snaks on this statement. This includes the main snak and all snaks 229 | * from qualifiers and references. 230 | * 231 | * This is a convenience method for use in code that needs to operate on all snaks, e.g. 232 | * to find all referenced Entities. 233 | * 234 | * @return Snak[] 235 | */ 236 | public function getAllSnaks(): array { 237 | $snaks = [ $this->mainSnak ]; 238 | 239 | foreach ( $this->qualifiers as $qualifier ) { 240 | $snaks[] = $qualifier; 241 | } 242 | 243 | /* @var Reference $reference */ 244 | foreach ( $this->getReferences() as $reference ) { 245 | foreach ( $reference->getSnaks() as $referenceSnak ) { 246 | $snaks[] = $referenceSnak; 247 | } 248 | } 249 | 250 | return $snaks; 251 | } 252 | 253 | /** 254 | * 255 | * @since 0.7.4 256 | * 257 | * @param mixed $target 258 | * 259 | * @return bool 260 | */ 261 | public function equals( $target ): bool { 262 | if ( $this === $target ) { 263 | return true; 264 | } 265 | 266 | return $target instanceof self 267 | && $this->guid === $target->guid 268 | && $this->rank === $target->rank 269 | && $this->mainSnak->equals( $target->mainSnak ) 270 | && $this->qualifiers->equals( $target->qualifiers ) 271 | && $this->references->equals( $target->references ); 272 | } 273 | 274 | /** 275 | * @see http://php.net/manual/en/language.oop5.cloning.php 276 | * 277 | * @since 5.1 278 | */ 279 | public function __clone() { 280 | $this->qualifiers = clone $this->qualifiers; 281 | $this->references = clone $this->references; 282 | } 283 | 284 | } 285 | -------------------------------------------------------------------------------- /src/Statement/StatementByGuidMap.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Kai Nissen < kai.nissen@wikimedia.de > 23 | */ 24 | class StatementByGuidMap implements IteratorAggregate, Countable { 25 | 26 | /** 27 | * @var Statement[] 28 | */ 29 | private $statements = []; 30 | 31 | /** 32 | * @param Statement[]|Traversable $statements 33 | */ 34 | public function __construct( $statements = [] ) { 35 | foreach ( $statements as $statement ) { 36 | $this->addStatement( $statement ); 37 | } 38 | } 39 | 40 | /** 41 | * If the provided statement has a GUID not yet in the map, it will be appended to the map. 42 | * If the GUID is already in the map, the statement with this guid will be replaced. 43 | * 44 | * @throws InvalidArgumentException 45 | * @param Statement $statement 46 | */ 47 | public function addStatement( Statement $statement ) { 48 | $index = $statement->getGuid(); 49 | if ( $index === null ) { 50 | throw new InvalidArgumentException( 'Can only add statements that have a non-null GUID' ); 51 | } 52 | 53 | $this->statements[$index] = $statement; 54 | } 55 | 56 | /** 57 | * @param string $statementGuid 58 | * 59 | * @return bool 60 | * @throws InvalidArgumentException 61 | */ 62 | public function hasStatementWithGuid( $statementGuid ) { 63 | $this->assertIsStatementGuid( $statementGuid ); 64 | 65 | return array_key_exists( $statementGuid, $this->statements ); 66 | } 67 | 68 | /** 69 | * @param string $statementGuid 70 | */ 71 | private function assertIsStatementGuid( $statementGuid ) { 72 | if ( !is_string( $statementGuid ) ) { 73 | throw new InvalidArgumentException( '$statementGuid needs to be a string' ); 74 | } 75 | } 76 | 77 | /** 78 | * @param string $statementGuid 79 | * 80 | * @return Statement|null 81 | */ 82 | public function getStatementByGuid( $statementGuid ) { 83 | $this->assertIsStatementGuid( $statementGuid ); 84 | 85 | if ( array_key_exists( $statementGuid, $this->statements ) ) { 86 | return $this->statements[$statementGuid]; 87 | } 88 | 89 | return null; 90 | } 91 | 92 | /** 93 | * Removes the statement with the specified GUID if it exists. 94 | * 95 | * @param string $statementGuid 96 | */ 97 | public function removeStatementWithGuid( $statementGuid ) { 98 | $this->assertIsStatementGuid( $statementGuid ); 99 | unset( $this->statements[$statementGuid] ); 100 | } 101 | 102 | /** 103 | * @see Countable::count 104 | * @return int 105 | */ 106 | public function count(): int { 107 | return count( $this->statements ); 108 | } 109 | 110 | /** 111 | * The iterator has the GUIDs of the statements as keys. 112 | * 113 | * @see IteratorAggregate::getIterator 114 | * @return Traversable 115 | */ 116 | public function getIterator(): Traversable { 117 | return new ArrayIterator( $this->statements ); 118 | } 119 | 120 | /** 121 | * Returns the map in array form. The array keys are the GUIDs of their associated statement. 122 | * 123 | * @return Statement[] 124 | */ 125 | public function toArray() { 126 | return $this->statements; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/Statement/StatementFilter.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface StatementFilter { 12 | 13 | /** 14 | * @param Statement $statement 15 | * 16 | * @return bool 17 | */ 18 | public function statementMatches( Statement $statement ); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Statement/StatementGuid.php: -------------------------------------------------------------------------------- 1 | getSerialization() . self::SEPARATOR . $guidPart; 37 | if ( $originalStatementId !== null 38 | && strtolower( $originalStatementId ) !== strtolower( $constructedStatementId ) ) { 39 | throw new InvalidArgumentException( '$originalStatementId does not match $entityId and/or $guidPart' ); 40 | } 41 | 42 | // use the original serialization when available to avoid normalizing the entity id prefix 43 | $this->serialization = $originalStatementId ?? $constructedStatementId; 44 | $this->entityId = $entityId; 45 | $this->guidPart = $guidPart; 46 | } 47 | 48 | public function getEntityId(): EntityId { 49 | return $this->entityId; 50 | } 51 | 52 | /** 53 | * @since 9.4 54 | */ 55 | public function getGuidPart(): string { 56 | return $this->guidPart; 57 | } 58 | 59 | /** 60 | * If the `$originalStatementId` parameter is not used when constructing the StatementGuid object, 61 | * then this method will return a statement id where the entity id prefix is normalized to upper case. 62 | * This could cause issues when comparing to other statement id serializations, 63 | * e.g. to look up a statement in a StatementList. 64 | */ 65 | public function getSerialization(): string { 66 | return $this->serialization; 67 | } 68 | 69 | /** 70 | * @param mixed $target 71 | */ 72 | public function equals( $target ): bool { 73 | if ( $this === $target ) { 74 | return true; 75 | } 76 | 77 | return $target instanceof self && $target->serialization === $this->serialization; 78 | } 79 | 80 | public function __toString(): string { 81 | return $this->serialization; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Statement/StatementList.php: -------------------------------------------------------------------------------- 1 | 30 | * @author Bene* < benestar.wikimedia@gmail.com > 31 | * @author Thiemo Kreuz 32 | */ 33 | class StatementList implements IteratorAggregate, Countable { 34 | 35 | /** 36 | * @var Statement[] 37 | */ 38 | private $statements; 39 | 40 | public function __construct( Statement ...$statements ) { 41 | $this->statements = $statements; 42 | } 43 | 44 | /** 45 | * Returns the property ids used by the statements. 46 | * The keys of the returned array hold the serializations of the property ids. 47 | * 48 | * @return PropertyId[] Array indexed by property id serialization. 49 | */ 50 | public function getPropertyIds(): array { 51 | $propertyIds = []; 52 | 53 | foreach ( $this->statements as $statement ) { 54 | $propertyIds[$statement->getPropertyId()->getSerialization()] = $statement->getPropertyId(); 55 | } 56 | 57 | return $propertyIds; 58 | } 59 | 60 | /** 61 | * @see ReferenceList::addReference 62 | * 63 | * @since 1.0, setting an index is supported since 6.1 64 | * 65 | * @param Statement $statement 66 | * @param int|null $index New position of the added statement, or null to append. 67 | * 68 | * @throws InvalidArgumentException 69 | */ 70 | public function addStatement( Statement $statement, ?int $index = null ): void { 71 | if ( $index === null ) { 72 | $this->statements[] = $statement; 73 | } elseif ( $index >= 0 ) { 74 | array_splice( $this->statements, $index, 0, [ $statement ] ); 75 | } else { 76 | throw new InvalidArgumentException( '$index must be a non-negative integer or null' ); 77 | } 78 | } 79 | 80 | /** 81 | * @param Snak $mainSnak 82 | * @param Snak[]|SnakList|null $qualifiers 83 | * @param Reference[]|ReferenceList|null $references 84 | * @param string|null $guid 85 | */ 86 | public function addNewStatement( 87 | Snak $mainSnak, $qualifiers = null, $references = null, ?string $guid = null 88 | ): void { 89 | $qualifiers = is_array( $qualifiers ) ? new SnakList( $qualifiers ) : $qualifiers; 90 | $references = is_array( $references ) ? new ReferenceList( $references ) : $references; 91 | 92 | $statement = new Statement( $mainSnak, $qualifiers, $references ); 93 | $statement->setGuid( $guid ); 94 | 95 | $this->statements[] = $statement; 96 | } 97 | 98 | /** 99 | * @since 3.0 100 | * 101 | * @param string|null $guid 102 | */ 103 | public function removeStatementsWithGuid( ?string $guid ): void { 104 | foreach ( $this->statements as $index => $statement ) { 105 | if ( $statement->getGuid() === $guid ) { 106 | unset( $this->statements[$index] ); 107 | } 108 | } 109 | 110 | $this->statements = array_values( $this->statements ); 111 | } 112 | 113 | /** 114 | * @param StatementGuid $statementGuid The GUID of the Statement to be replaced 115 | * @param Statement $newStatement The new Statement 116 | * 117 | * @throws StatementNotFoundException if the Statement with $statementGuid can't be found 118 | * @throws StatementGuidChangedException if the $newStatement has a different StatementGuid 119 | * @throws PropertyChangedException if the $newStatement has a different MainSnak Property 120 | */ 121 | public function replaceStatement( StatementGuid $statementGuid, Statement $newStatement ): void { 122 | $index = $this->getIndexOfFirstStatementWithGuid( (string)$statementGuid ); 123 | if ( $index === null ) { 124 | throw new StatementNotFoundException( "Statement with GUID '$statementGuid' not found" ); 125 | } elseif ( $newStatement->getGuid() && (string)$statementGuid !== $newStatement->getGuid() ) { 126 | throw new StatementGuidChangedException( 127 | 'The new Statement must not have a different Statement GUID than the original' 128 | ); 129 | } elseif ( !$this->statements[$index]->getMainSnak()->getPropertyId()->equals( $newStatement->getMainSnak()->getPropertyId() ) ) { 130 | throw new PropertyChangedException( 131 | 'The new Statement must not have a different Property than the original' 132 | ); 133 | } 134 | 135 | $newStatement->setGuid( (string)$statementGuid ); 136 | $this->statements[$index] = $newStatement; 137 | } 138 | 139 | /** 140 | * Statements that have a main snak already in the list are filtered out. 141 | * The last occurrences are retained. 142 | * 143 | * @since 1.0 144 | * 145 | * @return self 146 | */ 147 | public function getWithUniqueMainSnaks(): self { 148 | $statements = []; 149 | 150 | foreach ( $this->statements as $statement ) { 151 | $statements[$statement->getMainSnak()->getHash()] = $statement; 152 | } 153 | 154 | return new self( ...array_values( $statements ) ); 155 | } 156 | 157 | /** 158 | * @since 3.0 159 | * 160 | * @param PropertyId $id 161 | * 162 | * @return self 163 | */ 164 | public function getByPropertyId( PropertyId $id ): self { 165 | $statementList = new self(); 166 | 167 | foreach ( $this->statements as $statement ) { 168 | if ( $statement->getPropertyId()->equals( $id ) ) { 169 | $statementList->statements[] = $statement; 170 | } 171 | } 172 | 173 | return $statementList; 174 | } 175 | 176 | /** 177 | * @since 3.0 178 | * 179 | * @param int|int[] $acceptableRanks 180 | * 181 | * @return self 182 | */ 183 | public function getByRank( $acceptableRanks ): self { 184 | $acceptableRanks = array_flip( (array)$acceptableRanks ); 185 | $statementList = new self(); 186 | 187 | foreach ( $this->statements as $statement ) { 188 | if ( array_key_exists( $statement->getRank(), $acceptableRanks ) ) { 189 | $statementList->statements[] = $statement; 190 | } 191 | } 192 | 193 | return $statementList; 194 | } 195 | 196 | /** 197 | * Returns the so called "best statements". 198 | * If there are preferred statements, then this is all the preferred statements. 199 | * If there are no preferred statements, then this is all normal statements. 200 | * 201 | * @since 2.4 202 | * 203 | * @return self 204 | */ 205 | public function getBestStatements(): self { 206 | $statements = $this->getByRank( Statement::RANK_PREFERRED ); 207 | 208 | if ( !$statements->isEmpty() ) { 209 | return $statements; 210 | } 211 | 212 | return $this->getByRank( Statement::RANK_NORMAL ); 213 | } 214 | 215 | /** 216 | * Returns a list of all Snaks on this StatementList. This includes at least the main snaks of 217 | * all statements, the snaks from qualifiers, and the snaks from references. 218 | * 219 | * This is a convenience method for use in code that needs to operate on all snaks, e.g. 220 | * to find all referenced Entities. 221 | * 222 | * @since 1.1 223 | * 224 | * @return Snak[] Numerically indexed (non-sparse) array. 225 | */ 226 | public function getAllSnaks(): array { 227 | $snaks = []; 228 | 229 | foreach ( $this->statements as $statement ) { 230 | foreach ( $statement->getAllSnaks() as $snak ) { 231 | $snaks[] = $snak; 232 | } 233 | } 234 | 235 | return $snaks; 236 | } 237 | 238 | /** 239 | * @since 2.3 240 | * 241 | * @return Snak[] Numerically indexed (non-sparse) array. 242 | */ 243 | public function getMainSnaks(): array { 244 | $snaks = []; 245 | 246 | foreach ( $this->statements as $statement ) { 247 | $snaks[] = $statement->getMainSnak(); 248 | } 249 | 250 | return $snaks; 251 | } 252 | 253 | /** 254 | * @return Traversable 255 | */ 256 | public function getIterator(): Traversable { 257 | return new ArrayIterator( $this->statements ); 258 | } 259 | 260 | /** 261 | * @return Statement[] Numerically indexed (non-sparse) array. 262 | */ 263 | public function toArray(): array { 264 | return $this->statements; 265 | } 266 | 267 | /** 268 | * @see Countable::count 269 | * 270 | * @return int 271 | */ 272 | public function count(): int { 273 | return count( $this->statements ); 274 | } 275 | 276 | /** 277 | * 278 | * @param mixed $target 279 | * 280 | * @return bool 281 | */ 282 | public function equals( $target ): bool { 283 | if ( $this === $target ) { 284 | return true; 285 | } 286 | 287 | if ( !( $target instanceof self ) 288 | || $this->count() !== $target->count() 289 | ) { 290 | return false; 291 | } 292 | 293 | return $this->statementsEqual( $target->statements ); 294 | } 295 | 296 | private function statementsEqual( array $statements ): bool { 297 | reset( $statements ); 298 | 299 | foreach ( $this->statements as $statement ) { 300 | if ( !$statement->equals( current( $statements ) ) ) { 301 | return false; 302 | } 303 | 304 | next( $statements ); 305 | } 306 | 307 | return true; 308 | } 309 | 310 | public function isEmpty(): bool { 311 | return $this->statements === []; 312 | } 313 | 314 | /** 315 | * @see StatementByGuidMap 316 | * 317 | * @since 3.0 318 | * 319 | * @param string|null $statementGuid 320 | * 321 | * @return Statement|null The first statement with the given GUID or null if not found. 322 | */ 323 | public function getFirstStatementWithGuid( ?string $statementGuid ): ?Statement { 324 | $index = $this->getIndexOfFirstStatementWithGuid( $statementGuid ); 325 | if ( $index === null ) { 326 | return null; 327 | } 328 | return $this->statements[$index] ?? null; 329 | } 330 | 331 | /** 332 | * @param string|null $statementGuid 333 | * 334 | * @return int|null The index of the first statement with the given GUID or null if not found. 335 | */ 336 | private function getIndexOfFirstStatementWithGuid( ?string $statementGuid ): ?int { 337 | foreach ( $this->statements as $index => $statement ) { 338 | if ( $statement->getGuid() === $statementGuid ) { 339 | return $index; 340 | } 341 | } 342 | 343 | return null; 344 | } 345 | 346 | /** 347 | * @since 4.1 348 | * 349 | * @param StatementFilter $filter 350 | * 351 | * @return self 352 | */ 353 | public function filter( StatementFilter $filter ): self { 354 | $statementList = new self(); 355 | 356 | foreach ( $this->statements as $statement ) { 357 | if ( $filter->statementMatches( $statement ) ) { 358 | $statementList->statements[] = $statement; 359 | } 360 | } 361 | 362 | return $statementList; 363 | } 364 | 365 | /** 366 | * Removes all statements from this list. 367 | * 368 | * @since 7.0 369 | */ 370 | public function clear(): void { 371 | $this->statements = []; 372 | } 373 | 374 | /** 375 | * @see http://php.net/manual/en/language.oop5.cloning.php 376 | * 377 | * @since 5.1 378 | */ 379 | public function __clone() { 380 | foreach ( $this->statements as &$statement ) { 381 | $statement = clone $statement; 382 | } 383 | } 384 | 385 | } 386 | -------------------------------------------------------------------------------- /src/Statement/StatementListHolder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface StatementListProvider { 15 | 16 | /** 17 | * This is guaranteed to return the original, mutable object. 18 | * 19 | * @return StatementList 20 | */ 21 | public function getStatements(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Term/AliasGroup.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class AliasGroup implements Countable { 19 | 20 | /** 21 | * @var string Language code identifying the language of the aliases, but note that there is 22 | * nothing this class can do to enforce this convention. 23 | */ 24 | private $languageCode; 25 | 26 | /** 27 | * @var string[] 28 | */ 29 | private $aliases; 30 | 31 | /** 32 | * @param string $languageCode Language of the aliases. 33 | * @param string[] $aliases 34 | * 35 | * @throws InvalidArgumentException 36 | */ 37 | public function __construct( $languageCode, array $aliases = [] ) { 38 | if ( !is_string( $languageCode ) || $languageCode === '' ) { 39 | throw new InvalidArgumentException( '$languageCode must be a non-empty string' ); 40 | } 41 | 42 | $this->languageCode = $languageCode; 43 | $this->aliases = array_values( 44 | array_unique( 45 | array_map( 46 | 'trim', 47 | array_filter( 48 | $aliases, 49 | static function( $alias ) { 50 | if ( !is_string( $alias ) ) { 51 | throw new InvalidArgumentException( '$aliases must be an array of strings' ); 52 | } 53 | 54 | return trim( $alias ) !== ''; 55 | } 56 | ) 57 | ) 58 | ) 59 | ); 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getLanguageCode() { 66 | return $this->languageCode; 67 | } 68 | 69 | /** 70 | * @return string[] 71 | */ 72 | public function getAliases() { 73 | return $this->aliases; 74 | } 75 | 76 | /** 77 | * @return bool 78 | */ 79 | public function isEmpty() { 80 | return $this->aliases === []; 81 | } 82 | 83 | /** 84 | * 85 | * @param mixed $target 86 | * 87 | * @return bool 88 | */ 89 | public function equals( $target ) { 90 | if ( $this === $target ) { 91 | return true; 92 | } 93 | 94 | return is_object( $target ) 95 | && get_called_class() === get_class( $target ) 96 | && $this->languageCode === $target->languageCode 97 | && $this->aliases == $target->aliases; 98 | } 99 | 100 | /** 101 | * @see Countable::count 102 | * 103 | * @return int 104 | */ 105 | public function count(): int { 106 | return count( $this->aliases ); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/Term/AliasGroupFallback.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class AliasGroupFallback extends AliasGroup { 19 | 20 | /** 21 | * @var string Actual language of the aliases. 22 | */ 23 | private $actualLanguageCode; 24 | 25 | /** 26 | * @var string|null Source language if the aliases are transliterations. 27 | */ 28 | private $sourceLanguageCode; 29 | 30 | /** 31 | * @param string $requestedLanguageCode Requested language, not necessarily the language of the 32 | * aliases. 33 | * @param string[] $aliases 34 | * @param string $actualLanguageCode Actual language of the aliases. 35 | * @param string|null $sourceLanguageCode Source language if the aliases are transliterations. 36 | * 37 | * @throws InvalidArgumentException 38 | */ 39 | public function __construct( 40 | $requestedLanguageCode, 41 | array $aliases, 42 | $actualLanguageCode, 43 | $sourceLanguageCode 44 | ) { 45 | parent::__construct( $requestedLanguageCode, $aliases ); 46 | 47 | if ( !is_string( $actualLanguageCode ) || $actualLanguageCode === '' ) { 48 | throw new InvalidArgumentException( '$actualLanguageCode must be a non-empty string' ); 49 | } 50 | 51 | if ( !( $sourceLanguageCode === null 52 | || ( is_string( $sourceLanguageCode ) && $sourceLanguageCode !== '' ) 53 | ) ) { 54 | throw new InvalidArgumentException( '$sourceLanguageCode must be a non-empty string or null' ); 55 | } 56 | 57 | $this->actualLanguageCode = $actualLanguageCode; 58 | $this->sourceLanguageCode = $sourceLanguageCode; 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | public function getActualLanguageCode() { 65 | return $this->actualLanguageCode; 66 | } 67 | 68 | /** 69 | * @return string|null 70 | */ 71 | public function getSourceLanguageCode() { 72 | return $this->sourceLanguageCode; 73 | } 74 | 75 | /** 76 | * 77 | * @param mixed $target 78 | * 79 | * @return bool 80 | */ 81 | public function equals( $target ) { 82 | if ( $this === $target ) { 83 | return true; 84 | } 85 | 86 | return $target instanceof self 87 | && parent::equals( $target ) 88 | && $this->actualLanguageCode === $target->actualLanguageCode 89 | && $this->sourceLanguageCode === $target->sourceLanguageCode; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Term/AliasGroupList.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class AliasGroupList implements Countable, IteratorAggregate { 25 | 26 | /** 27 | * @var AliasGroup[] 28 | */ 29 | private $groups = []; 30 | 31 | /** 32 | * @param AliasGroup[] $aliasGroups 33 | * @throws InvalidArgumentException 34 | */ 35 | public function __construct( array $aliasGroups = [] ) { 36 | foreach ( $aliasGroups as $aliasGroup ) { 37 | if ( !( $aliasGroup instanceof AliasGroup ) ) { 38 | throw new InvalidArgumentException( 'Every element in $aliasGroups must be an instance of AliasGroup' ); 39 | } 40 | 41 | $this->setGroup( $aliasGroup ); 42 | } 43 | } 44 | 45 | /** 46 | * @see Countable::count 47 | * @return int 48 | */ 49 | public function count(): int { 50 | return count( $this->groups ); 51 | } 52 | 53 | /** 54 | * @see IteratorAggregate::getIterator 55 | * @return Traversable 56 | */ 57 | public function getIterator(): Traversable { 58 | return new ArrayIterator( $this->groups ); 59 | } 60 | 61 | /** 62 | * The array keys are the language codes of their associated AliasGroup. 63 | * 64 | * @since 2.3 65 | * 66 | * @return AliasGroup[] Array indexed by language code. 67 | */ 68 | public function toArray() { 69 | return $this->groups; 70 | } 71 | 72 | /** 73 | * @param string $languageCode 74 | * 75 | * @return AliasGroup 76 | * @throws OutOfBoundsException 77 | */ 78 | public function getByLanguage( $languageCode ) { 79 | if ( !array_key_exists( $languageCode, $this->groups ) ) { 80 | throw new OutOfBoundsException( 'AliasGroup with languageCode "' . $languageCode . '" not found' ); 81 | } 82 | 83 | return $this->groups[$languageCode]; 84 | } 85 | 86 | /** 87 | * @since 2.5 88 | * 89 | * @param string[] $languageCodes 90 | * 91 | * @return self 92 | */ 93 | public function getWithLanguages( array $languageCodes ) { 94 | return new self( array_intersect_key( $this->groups, array_flip( $languageCodes ) ) ); 95 | } 96 | 97 | /** 98 | * @param string $languageCode 99 | */ 100 | public function removeByLanguage( $languageCode ) { 101 | unset( $this->groups[$languageCode] ); 102 | } 103 | 104 | /** 105 | * If the group is empty, it will not be stored. 106 | * In case the language of that group had an associated group, that group will be removed. 107 | */ 108 | public function setGroup( AliasGroup $group ) { 109 | if ( $group->isEmpty() ) { 110 | unset( $this->groups[$group->getLanguageCode()] ); 111 | } else { 112 | $this->groups[$group->getLanguageCode()] = $group; 113 | } 114 | } 115 | 116 | /** 117 | * 118 | * @since 0.7.4 119 | * 120 | * @param mixed $target 121 | * 122 | * @return bool 123 | */ 124 | public function equals( $target ) { 125 | if ( $this === $target ) { 126 | return true; 127 | } 128 | 129 | if ( !( $target instanceof self ) 130 | || $this->count() !== $target->count() 131 | ) { 132 | return false; 133 | } 134 | 135 | foreach ( $this->groups as $group ) { 136 | if ( !$target->hasAliasGroup( $group ) ) { 137 | return false; 138 | } 139 | } 140 | 141 | return true; 142 | } 143 | 144 | /** 145 | * @since 2.4.0 146 | * 147 | * @return bool 148 | */ 149 | public function isEmpty() { 150 | return $this->groups === []; 151 | } 152 | 153 | /** 154 | * @since 0.7.4 155 | * 156 | * @param AliasGroup $group 157 | * 158 | * @return bool 159 | */ 160 | public function hasAliasGroup( AliasGroup $group ) { 161 | return array_key_exists( $group->getLanguageCode(), $this->groups ) 162 | && $this->groups[$group->getLanguageCode()]->equals( $group ); 163 | } 164 | 165 | /** 166 | * @since 0.8 167 | * 168 | * @param string $languageCode 169 | * 170 | * @return bool 171 | */ 172 | public function hasGroupForLanguage( $languageCode ) { 173 | return array_key_exists( $languageCode, $this->groups ); 174 | } 175 | 176 | /** 177 | * @since 0.8 178 | * 179 | * @param string $languageCode 180 | * @param string[] $aliases 181 | */ 182 | public function setAliasesForLanguage( $languageCode, array $aliases ) { 183 | $this->setGroup( new AliasGroup( $languageCode, $aliases ) ); 184 | } 185 | 186 | /** 187 | * Returns an array with language codes as keys the aliases as array values. 188 | * 189 | * @since 2.5 190 | * 191 | * @return array[] 192 | */ 193 | public function toTextArray() { 194 | $array = []; 195 | 196 | foreach ( $this->groups as $group ) { 197 | $array[$group->getLanguageCode()] = $group->getAliases(); 198 | } 199 | 200 | return $array; 201 | } 202 | 203 | /** 204 | * Removes all alias groups from this list. 205 | * 206 | * @since 7.0 207 | */ 208 | public function clear() { 209 | $this->groups = []; 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/Term/AliasesProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface AliasesProvider { 15 | 16 | /** 17 | * This is guaranteed to return the original, mutable object. 18 | * 19 | * @return AliasGroupList 20 | */ 21 | public function getAliasGroups(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Term/DescriptionsProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface DescriptionsProvider { 15 | 16 | /** 17 | * This is guaranteed to return the original, mutable object. 18 | * 19 | * @return TermList 20 | */ 21 | public function getDescriptions(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Term/Fingerprint.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Thiemo Kreuz 18 | */ 19 | class Fingerprint implements LabelsProvider, DescriptionsProvider, AliasesProvider { 20 | 21 | /** 22 | * @deprecated since 2.5, use new Fingerprint() instead. 23 | * 24 | * @return self 25 | */ 26 | public static function newEmpty() { 27 | return new self(); 28 | } 29 | 30 | /** 31 | * @var TermList 32 | */ 33 | private $labels; 34 | 35 | /** 36 | * @var TermList 37 | */ 38 | private $descriptions; 39 | 40 | /** 41 | * @var AliasGroupList 42 | */ 43 | private $aliasGroups; 44 | 45 | public function __construct( 46 | ?TermList $labels = null, 47 | ?TermList $descriptions = null, 48 | ?AliasGroupList $aliasGroups = null 49 | ) { 50 | $this->labels = $labels ?: new TermList(); 51 | $this->descriptions = $descriptions ?: new TermList(); 52 | $this->aliasGroups = $aliasGroups ?: new AliasGroupList(); 53 | } 54 | 55 | /** 56 | * @since 0.7.3 57 | * 58 | * @return TermList 59 | */ 60 | public function getLabels() { 61 | return $this->labels; 62 | } 63 | 64 | /** 65 | * @since 0.9 66 | * 67 | * @param string $languageCode 68 | * 69 | * @return bool 70 | */ 71 | public function hasLabel( $languageCode ) { 72 | return $this->labels->hasTermForLanguage( $languageCode ); 73 | } 74 | 75 | /** 76 | * @since 0.7.4 77 | * 78 | * @param string $languageCode 79 | * 80 | * @return Term 81 | * @throws OutOfBoundsException 82 | * @throws InvalidArgumentException 83 | */ 84 | public function getLabel( $languageCode ) { 85 | return $this->labels->getByLanguage( $languageCode ); 86 | } 87 | 88 | /** 89 | * @since 1.0 90 | * 91 | * @param string $languageCode 92 | * @param string $labelText 93 | * 94 | * @throws InvalidArgumentException 95 | */ 96 | public function setLabel( $languageCode, $labelText ) { 97 | $this->labels->setTerm( new Term( $languageCode, $labelText ) ); 98 | } 99 | 100 | /** 101 | * @since 0.7.4 102 | * 103 | * @param string $languageCode 104 | */ 105 | public function removeLabel( $languageCode ) { 106 | $this->labels->removeByLanguage( $languageCode ); 107 | } 108 | 109 | /** 110 | * @since 0.7.3 111 | * 112 | * @return TermList 113 | */ 114 | public function getDescriptions() { 115 | return $this->descriptions; 116 | } 117 | 118 | /** 119 | * @since 0.9 120 | * 121 | * @param string $languageCode 122 | * 123 | * @return bool 124 | */ 125 | public function hasDescription( $languageCode ) { 126 | return $this->descriptions->hasTermForLanguage( $languageCode ); 127 | } 128 | 129 | /** 130 | * @since 0.7.4 131 | * 132 | * @param string $languageCode 133 | * 134 | * @return Term 135 | * @throws OutOfBoundsException 136 | * @throws InvalidArgumentException 137 | */ 138 | public function getDescription( $languageCode ) { 139 | return $this->descriptions->getByLanguage( $languageCode ); 140 | } 141 | 142 | /** 143 | * @since 1.0 144 | * 145 | * @param string $languageCode 146 | * @param string $descriptionText 147 | * 148 | * @throws InvalidArgumentException 149 | */ 150 | public function setDescription( $languageCode, $descriptionText ) { 151 | $this->descriptions->setTerm( new Term( $languageCode, $descriptionText ) ); 152 | } 153 | 154 | /** 155 | * @since 0.7.4 156 | * 157 | * @param string $languageCode 158 | */ 159 | public function removeDescription( $languageCode ) { 160 | $this->descriptions->removeByLanguage( $languageCode ); 161 | } 162 | 163 | /** 164 | * @since 0.7.4 165 | * 166 | * @return AliasGroupList 167 | */ 168 | public function getAliasGroups() { 169 | return $this->aliasGroups; 170 | } 171 | 172 | /** 173 | * @since 0.9 174 | * 175 | * @param string $languageCode 176 | * 177 | * @return bool 178 | */ 179 | public function hasAliasGroup( $languageCode ) { 180 | return $this->aliasGroups->hasGroupForLanguage( $languageCode ); 181 | } 182 | 183 | /** 184 | * @since 0.7.4 185 | * 186 | * @param string $languageCode 187 | * 188 | * @return AliasGroup 189 | * @throws OutOfBoundsException 190 | * @throws InvalidArgumentException 191 | */ 192 | public function getAliasGroup( $languageCode ) { 193 | return $this->aliasGroups->getByLanguage( $languageCode ); 194 | } 195 | 196 | /** 197 | * @since 1.0 198 | * 199 | * @param string $languageCode 200 | * @param string[] $aliases 201 | * 202 | * @throws InvalidArgumentException 203 | */ 204 | public function setAliasGroup( $languageCode, array $aliases ) { 205 | $this->aliasGroups->setGroup( new AliasGroup( $languageCode, $aliases ) ); 206 | } 207 | 208 | /** 209 | * @since 0.7.4 210 | * 211 | * @param string $languageCode 212 | */ 213 | public function removeAliasGroup( $languageCode ) { 214 | $this->aliasGroups->removeByLanguage( $languageCode ); 215 | } 216 | 217 | /** 218 | * 219 | * @since 0.7.4 220 | * 221 | * @param mixed $target 222 | * 223 | * @return bool 224 | */ 225 | public function equals( $target ) { 226 | if ( $this === $target ) { 227 | return true; 228 | } 229 | 230 | return $target instanceof self 231 | && $this->descriptions->equals( $target->getDescriptions() ) 232 | && $this->labels->equals( $target->getLabels() ) 233 | && $this->aliasGroups->equals( $target->getAliasGroups() ); 234 | } 235 | 236 | /** 237 | * @since 0.7.4 238 | * 239 | * @return bool 240 | */ 241 | public function isEmpty() { 242 | return $this->labels->isEmpty() 243 | && $this->descriptions->isEmpty() 244 | && $this->aliasGroups->isEmpty(); 245 | } 246 | 247 | /** 248 | * @since 0.7.4 249 | * 250 | * @param TermList $labels 251 | */ 252 | public function setLabels( TermList $labels ) { 253 | $this->labels = $labels; 254 | } 255 | 256 | /** 257 | * @since 0.7.4 258 | * 259 | * @param TermList $descriptions 260 | */ 261 | public function setDescriptions( TermList $descriptions ) { 262 | $this->descriptions = $descriptions; 263 | } 264 | 265 | /** 266 | * @since 0.7.4 267 | * 268 | * @param AliasGroupList $groups 269 | */ 270 | public function setAliasGroups( AliasGroupList $groups ) { 271 | $this->aliasGroups = $groups; 272 | } 273 | 274 | /** 275 | * @see http://php.net/manual/en/language.oop5.cloning.php 276 | * 277 | * @since 5.1 278 | */ 279 | public function __clone() { 280 | // TermList is mutable, but Term is not. No deeper cloning necessary. 281 | $this->labels = clone $this->labels; 282 | $this->descriptions = clone $this->descriptions; 283 | // AliasGroupList is mutable, but AliasGroup is not. No deeper cloning necessary. 284 | $this->aliasGroups = clone $this->aliasGroups; 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /src/Term/FingerprintProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface FingerprintProvider { 15 | 16 | /** 17 | * This is guaranteed to return the original, mutable object. 18 | * 19 | * @return Fingerprint 20 | */ 21 | public function getFingerprint(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Term/LabelsProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface LabelsProvider { 15 | 16 | /** 17 | * This is guaranteed to return the original, mutable object. 18 | * 19 | * @return TermList 20 | */ 21 | public function getLabels(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Term/Term.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Term { 19 | 20 | /** 21 | * @var string Language code identifying the language of the text, but note that there is 22 | * nothing this class can do to enforce this convention. 23 | */ 24 | private $languageCode; 25 | 26 | /** 27 | * @var string 28 | */ 29 | private $text; 30 | 31 | /** 32 | * @param string $languageCode Language of the text. 33 | * @param string $text 34 | * 35 | * @throws InvalidArgumentException 36 | */ 37 | public function __construct( $languageCode, $text ) { 38 | if ( !is_string( $languageCode ) || $languageCode === '' ) { 39 | throw new InvalidArgumentException( '$languageCode must be a non-empty string' ); 40 | } 41 | 42 | if ( !is_string( $text ) ) { 43 | throw new InvalidArgumentException( '$text must be a string' ); 44 | } 45 | 46 | $this->languageCode = $languageCode; 47 | $this->text = $text; 48 | } 49 | 50 | /** 51 | * Language code. 52 | * 53 | * Note that in {@link TermFallback}, this is the *requested* language code, 54 | * and there is a separate {@link TermFallback::getActualLanguageCode()} method. 55 | * 56 | * @return string 57 | */ 58 | public function getLanguageCode() { 59 | return $this->languageCode; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getText() { 66 | return $this->text; 67 | } 68 | 69 | /** 70 | * 71 | * @param mixed $target 72 | * 73 | * @return bool 74 | */ 75 | public function equals( $target ) { 76 | if ( $this === $target ) { 77 | return true; 78 | } 79 | 80 | return is_object( $target ) 81 | && get_called_class() === get_class( $target ) 82 | && $this->languageCode === $target->languageCode 83 | && $this->text === $target->text; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/Term/TermFallback.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class TermFallback extends Term { 16 | 17 | /** 18 | * @var string Actual language of the text. 19 | */ 20 | private $actualLanguageCode; 21 | 22 | /** 23 | * @var string|null Source language if the text is a transliteration. 24 | */ 25 | private $sourceLanguageCode; 26 | 27 | /** 28 | * @param string $requestedLanguageCode Requested language, not necessarily the language of the 29 | * text. 30 | * @param string $text 31 | * @param string $actualLanguageCode Actual language of the text. 32 | * @param string|null $sourceLanguageCode Source language if the text is a transliteration. 33 | * 34 | * @throws InvalidArgumentException 35 | */ 36 | public function __construct( $requestedLanguageCode, $text, $actualLanguageCode, $sourceLanguageCode ) { 37 | parent::__construct( $requestedLanguageCode, $text ); 38 | 39 | if ( !is_string( $actualLanguageCode ) || $actualLanguageCode === '' ) { 40 | throw new InvalidArgumentException( '$actualLanguageCode must be a non-empty string' ); 41 | } 42 | 43 | if ( !( $sourceLanguageCode === null 44 | || ( is_string( $sourceLanguageCode ) && $sourceLanguageCode !== '' ) 45 | ) ) { 46 | throw new InvalidArgumentException( '$sourceLanguageCode must be a non-empty string or null' ); 47 | } 48 | 49 | $this->actualLanguageCode = $actualLanguageCode; 50 | $this->sourceLanguageCode = $sourceLanguageCode; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getActualLanguageCode() { 57 | return $this->actualLanguageCode; 58 | } 59 | 60 | /** 61 | * @return string|null 62 | */ 63 | public function getSourceLanguageCode() { 64 | return $this->sourceLanguageCode; 65 | } 66 | 67 | /** 68 | * 69 | * @param mixed $target 70 | * 71 | * @return bool 72 | */ 73 | public function equals( $target ) { 74 | if ( $this === $target ) { 75 | return true; 76 | } 77 | 78 | return $target instanceof self 79 | && parent::equals( $target ) 80 | && $this->actualLanguageCode === $target->actualLanguageCode 81 | && $this->sourceLanguageCode === $target->sourceLanguageCode; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/Term/TermList.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class TermList implements Countable, IteratorAggregate { 22 | 23 | /** 24 | * @var Term[] 25 | */ 26 | private $terms = []; 27 | 28 | /** 29 | * @param iterable|Term[] $terms Can be a non-array since 8.1 30 | * @throws InvalidArgumentException 31 | */ 32 | public function __construct( /* iterable */ $terms = [] ) { 33 | $this->addAll( $terms ); 34 | } 35 | 36 | /** 37 | * @see Countable::count 38 | * @return int 39 | */ 40 | public function count(): int { 41 | return count( $this->terms ); 42 | } 43 | 44 | /** 45 | * Returns an array with language codes as keys and the term text as values. 46 | * 47 | * @return string[] 48 | */ 49 | public function toTextArray() { 50 | $array = []; 51 | 52 | foreach ( $this->terms as $term ) { 53 | $array[$term->getLanguageCode()] = $term->getText(); 54 | } 55 | 56 | return $array; 57 | } 58 | 59 | /** 60 | * @see IteratorAggregate::getIterator 61 | * @return ArrayIterator 62 | */ 63 | public function getIterator(): ArrayIterator { 64 | return new ArrayIterator( $this->terms ); 65 | } 66 | 67 | /** 68 | * @param string $languageCode 69 | * 70 | * @return Term 71 | * @throws OutOfBoundsException 72 | */ 73 | public function getByLanguage( $languageCode ) { 74 | if ( !array_key_exists( $languageCode, $this->terms ) ) { 75 | throw new OutOfBoundsException( 'Term with languageCode "' . $languageCode . '" not found' ); 76 | } 77 | 78 | return $this->terms[$languageCode]; 79 | } 80 | 81 | /** 82 | * @since 2.5 83 | * 84 | * @param string[] $languageCodes 85 | * 86 | * @return self 87 | */ 88 | public function getWithLanguages( array $languageCodes ) { 89 | return new self( array_intersect_key( $this->terms, array_flip( $languageCodes ) ) ); 90 | } 91 | 92 | /** 93 | * @param string $languageCode 94 | */ 95 | public function removeByLanguage( $languageCode ) { 96 | unset( $this->terms[$languageCode] ); 97 | } 98 | 99 | /** 100 | * @param string $languageCode 101 | * 102 | * @return bool 103 | */ 104 | public function hasTermForLanguage( $languageCode ) { 105 | return array_key_exists( $languageCode, $this->terms ); 106 | } 107 | 108 | /** 109 | * Replaces non-empty or removes empty terms. 110 | */ 111 | public function setTerm( Term $term ) { 112 | if ( $term->getText() === '' ) { 113 | unset( $this->terms[$term->getLanguageCode()] ); 114 | } else { 115 | $this->terms[$term->getLanguageCode()] = $term; 116 | } 117 | } 118 | 119 | /** 120 | * @since 0.8 121 | * 122 | * @param string $languageCode 123 | * @param string $termText 124 | */ 125 | public function setTextForLanguage( $languageCode, $termText ) { 126 | $this->setTerm( new Term( $languageCode, $termText ) ); 127 | } 128 | 129 | /** 130 | * 131 | * @since 0.7.4 132 | * 133 | * @param mixed $target 134 | * 135 | * @return bool 136 | */ 137 | public function equals( $target ) { 138 | if ( $this === $target ) { 139 | return true; 140 | } 141 | 142 | if ( !( $target instanceof self ) 143 | || $this->count() !== $target->count() 144 | ) { 145 | return false; 146 | } 147 | 148 | foreach ( $this->terms as $term ) { 149 | if ( !$target->hasTerm( $term ) ) { 150 | return false; 151 | } 152 | } 153 | 154 | return true; 155 | } 156 | 157 | /** 158 | * @since 2.4.0 159 | * 160 | * @return bool 161 | */ 162 | public function isEmpty() { 163 | return $this->terms === []; 164 | } 165 | 166 | /** 167 | * @since 0.7.4 168 | * 169 | * @param Term $term 170 | * 171 | * @return bool 172 | */ 173 | public function hasTerm( Term $term ) { 174 | return array_key_exists( $term->getLanguageCode(), $this->terms ) 175 | && $this->terms[$term->getLanguageCode()]->equals( $term ); 176 | } 177 | 178 | /** 179 | * Removes all terms from this list. 180 | * 181 | * @since 7.0 182 | */ 183 | public function clear() { 184 | $this->terms = []; 185 | } 186 | 187 | /** 188 | * @since 8.1 189 | * 190 | * @param iterable|Term[] $terms 191 | * @throws InvalidArgumentException 192 | */ 193 | public function addAll( /* iterable */ $terms ) { 194 | if ( !is_array( $terms ) && !( $terms instanceof \Traversable ) ) { 195 | throw new InvalidArgumentException( '$terms must be iterable' ); 196 | } 197 | 198 | foreach ( $terms as $term ) { 199 | if ( !( $term instanceof Term ) ) { 200 | throw new InvalidArgumentException( 'Every element in $terms must be an instance of Term' ); 201 | } 202 | 203 | $this->setTerm( $term ); 204 | } 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/Term/TermTypes.php: -------------------------------------------------------------------------------- 1 |