r103893 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r103892‎ | r103893 | r103894 >
Date:13:34, 22 November 2011
Author:dantman
Status:ok (Comments)
Tags:
Comment:
Implement a number of namespace related equals functions:
* MWNamespace::equals to test equivalence of two namespaces (forward compatible with any changes we may make like introducing namespace keys like 'USER')
* MWNamespace::subjectEquals to test equivalence of the subject of two namespaces e.g.: MWNamespace::subjectEquals( NS_USER, $ns ); instead of testing for equivalence to both NS_USER and NS_USER_TALK
* Title::inNamespace to use instead of $title->getNamespace() == NS_???
* Title::inNamespaces for use like $title->inNamespaces( NS_USER, NS_PROJECT ) when you only care if it's in one of a number of namespaces (also accepts an array)
* Title::hasSubjectNamespace for use instead of testing for equivalence to both the subject and talk such as NS_USER and NS_USER_TALK.

Include phpunit tests for all this new code, and also add some tests for some existing code.
Modified paths:
  • /trunk/phase3/includes/Namespace.php (modified) (history)
  • /trunk/phase3/includes/Title.php (modified) (history)
  • /trunk/phase3/tests/phpunit/includes/MWNamespaceTest.php (modified) (history)
  • /trunk/phase3/tests/phpunit/includes/TitleMethodsTest.php (added) (history)

Diff [purge]

Index: trunk/phase3/tests/phpunit/includes/TitleMethodsTest.php
@@ -0,0 +1,78 @@
 2+<?php
 3+
 4+class TitleMethodsTest extends MediaWikiTestCase {
 5+
 6+ public function dataEquals() {
 7+ return array(
 8+ array( 'Main Page', 'Main Page', true ),
 9+ array( 'Main Page', 'Not The Main Page', false ),
 10+ array( 'Main Page', 'Project:Main Page', false ),
 11+ array( 'File:Example.png', 'Image:Example.png', true ),
 12+ array( 'Special:Version', 'Special:Version', true ),
 13+ array( 'Special:Version', 'Special:Recentchanges', false ),
 14+ array( 'Special:Version', 'Main Page', false ),
 15+ );
 16+ }
 17+
 18+ /**
 19+ * @dataProvider dataEquals
 20+ */
 21+ public function testEquals( $titleA, $titleB, $expectedBool ) {
 22+ $titleA = Title::newFromText( $titleA );
 23+ $titleB = Title::newFromText( $titleB );
 24+
 25+ $this->assertEquals( $titleA->equals( $titleB ), $expectedBool );
 26+ $this->assertEquals( $titleB->equals( $titleA ), $expectedBool );
 27+ }
 28+
 29+ public function dataInNamespace() {
 30+ return array(
 31+ array( 'Main Page', NS_MAIN, true ),
 32+ array( 'Main Page', NS_TALK, false ),
 33+ array( 'Main Page', NS_USER, false ),
 34+ array( 'User:Foo', NS_USER, true ),
 35+ array( 'User:Foo', NS_USER_TALK, false ),
 36+ array( 'User:Foo', NS_TEMPLATE, false ),
 37+ array( 'User_talk:Foo', NS_USER_TALK, true ),
 38+ array( 'User_talk:Foo', NS_USER, false ),
 39+ );
 40+ }
 41+
 42+ /**
 43+ * @dataProvider dataInNamespace
 44+ */
 45+ public function testInNamespace( $title, $ns, $expectedBool ) {
 46+ $title = Title::newFromText( $title );
 47+ $this->assertEquals( $title->inNamespace( $ns ), $expectedBool );
 48+ }
 49+
 50+ public function testInNamespaces() {
 51+ $mainpage = Title::newFromText( 'Main Page' );
 52+ $this->assertTrue( $mainpage->inNamespaces( NS_MAIN, NS_USER ) );
 53+ $this->assertTrue( $mainpage->inNamespaces( array( NS_MAIN, NS_USER ) ) );
 54+ $this->assertTrue( $mainpage->inNamespaces( array( NS_USER, NS_MAIN ) ) );
 55+ $this->assertFalse( $mainpage->inNamespaces( array( NS_PROJECT, NS_TEMPLATE ) ) );
 56+ }
 57+
 58+ public function dataHasSubjectNamespace() {
 59+ return array(
 60+ array( 'Main Page', NS_MAIN, true ),
 61+ array( 'Main Page', NS_TALK, true ),
 62+ array( 'Main Page', NS_USER, false ),
 63+ array( 'User:Foo', NS_USER, true ),
 64+ array( 'User:Foo', NS_USER_TALK, true ),
 65+ array( 'User:Foo', NS_TEMPLATE, false ),
 66+ array( 'User_talk:Foo', NS_USER_TALK, true ),
 67+ array( 'User_talk:Foo', NS_USER, true ),
 68+ );
 69+ }
 70+
 71+ /**
 72+ * @dataProvider dataHasSubjectNamespace
 73+ */
 74+ public function testHasSubjectNamespace( $title, $ns, $expectedBool ) {
 75+ $title = Title::newFromText( $title );
 76+ $this->assertEquals( $title->hasSubjectNamespace( $ns ), $expectedBool );
 77+ }
 78+
 79+}
Index: trunk/phase3/tests/phpunit/includes/MWNamespaceTest.php
@@ -39,25 +39,29 @@
4040 /**
4141 * Please make sure to change testIsTalk() if you change the assertions below
4242 */
43 - public function testIsMain() {
 43+ public function testIsSubject() {
4444 // Special namespaces
45 - $this->assertTrue( MWNamespace::isMain( NS_MEDIA ) );
46 - $this->assertTrue( MWNamespace::isMain( NS_SPECIAL ) );
 45+ $this->assertTrue( MWNamespace::isSubject( NS_MEDIA ) );
 46+ $this->assertTrue( MWNamespace::isSubject( NS_SPECIAL ) );
4747
4848 // Subject pages
49 - $this->assertTrue( MWNamespace::isMain( NS_MAIN ) );
50 - $this->assertTrue( MWNamespace::isMain( NS_USER ) );
51 - $this->assertTrue( MWNamespace::isMain( 100 ) ); # user defined
 49+ $this->assertTrue( MWNamespace::isSubject( NS_MAIN ) );
 50+ $this->assertTrue( MWNamespace::isSubject( NS_USER ) );
 51+ $this->assertTrue( MWNamespace::isSubject( 100 ) ); # user defined
5252
5353 // Talk pages
54 - $this->assertFalse( MWNamespace::isMain( NS_TALK ) );
55 - $this->assertFalse( MWNamespace::isMain( NS_USER_TALK ) );
56 - $this->assertFalse( MWNamespace::isMain( 101 ) ); # user defined
 54+ $this->assertFalse( MWNamespace::isSubject( NS_TALK ) );
 55+ $this->assertFalse( MWNamespace::isSubject( NS_USER_TALK ) );
 56+ $this->assertFalse( MWNamespace::isSubject( 101 ) ); # user defined
 57+
 58+ // Back compat
 59+ $this->assertTrue( MWNamespace::isMain( NS_MAIN ) == MWNamespace::isSubject( NS_MAIN ) );
 60+ $this->assertTrue( MWNamespace::isMain( NS_USER_TALK ) == MWNamespace::isSubject( NS_USER_TALK ) );
5761 }
5862
5963 /**
60 - * Reverse of testIsMain().
61 - * Please update testIsMain() if you change assertions below
 64+ * Reverse of testIsSubject().
 65+ * Please update testIsSubject() if you change assertions below
6266 */
6367 public function testIsTalk() {
6468 // Special namespaces
@@ -76,12 +80,26 @@
7781 }
7882
7983 /**
 84+ */
 85+ public function testGetSubject() {
 86+ // Special namespaces are their own subjects
 87+ $this->assertEquals( NS_MEDIA, MWNamespace::getSubject( NS_MEDIA ) );
 88+ $this->assertEquals( NS_SPECIAL, MWNamespace::getSubject( NS_SPECIAL ) );
 89+
 90+ $this->assertEquals( NS_MAIN, MWNamespace::getSubject( NS_TALK ) );
 91+ $this->assertEquals( NS_USER, MWNamespace::getSubject( NS_USER_TALK ) );
 92+ }
 93+
 94+ /**
8095 * Regular getTalk() calls
8196 * Namespaces without a talk page (NS_MEDIA, NS_SPECIAL) are tested in
8297 * the function testGetTalkExceptions()
8398 */
8499 public function testGetTalk() {
85100 $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_MAIN ) );
 101+ $this->assertEquals( NS_TALK, MWNamespace::getTalk( NS_TALK ) );
 102+ $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER ) );
 103+ $this->assertEquals( NS_USER_TALK, MWNamespace::getTalk( NS_USER_TALK ) );
86104 }
87105
88106 /**
@@ -108,7 +126,7 @@
109127 * the function testGetAssociatedExceptions()
110128 */
111129 public function testGetAssociated() {
112 - $this->assertEquals( NS_TALK, MWNamespace::getAssociated( NS_MAIN ) );
 130+ $this->assertEquals( NS_TALK, MWNamespace::getAssociated( NS_MAIN ) );
113131 $this->assertEquals( NS_MAIN, MWNamespace::getAssociated( NS_TALK ) );
114132
115133 }
@@ -131,17 +149,6 @@
132150 }
133151
134152 /**
135 - */
136 - public function testGetSubject() {
137 - // Special namespaces are their own subjects
138 - $this->assertEquals( NS_MEDIA, MWNamespace::getSubject( NS_MEDIA ) );
139 - $this->assertEquals( NS_SPECIAL, MWNamespace::getSubject( NS_SPECIAL ) );
140 -
141 - $this->assertEquals( NS_MAIN, MWNamespace::getSubject( NS_TALK ) );
142 - $this->assertEquals( NS_USER, MWNamespace::getSubject( NS_USER_TALK ) );
143 - }
144 -
145 - /**
146153 * @todo Implement testExists().
147154 */
148155 /*
@@ -152,7 +159,41 @@
153160 );
154161 }
155162 */
 163+
156164 /**
 165+ * Test MWNamespace::equals
 166+ * Note if we add a namespace registration system with keys like 'MAIN'
 167+ * we should add tests here for equivilance on things like 'MAIN' == 0
 168+ * and 'MAIN' == NS_MAIN.
 169+ */
 170+ public function testEquals() {
 171+ $this->assertTrue( MWNamespace::equals( NS_MAIN, NS_MAIN ) );
 172+ $this->assertTrue( MWNamespace::equals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN'
 173+ $this->assertTrue( MWNamespace::equals( NS_USER, NS_USER ) );
 174+ $this->assertTrue( MWNamespace::equals( NS_USER, 2 ) );
 175+ $this->assertTrue( MWNamespace::equals( NS_USER_TALK, NS_USER_TALK ) );
 176+ $this->assertTrue( MWNamespace::equals( NS_SPECIAL, NS_SPECIAL ) );
 177+ $this->assertFalse( MWNamespace::equals( NS_MAIN, NS_TALK ) );
 178+ $this->assertFalse( MWNamespace::equals( NS_USER, NS_USER_TALK ) );
 179+ $this->assertFalse( MWNamespace::equals( NS_PROJECT, NS_TEMPLATE ) );
 180+ }
 181+
 182+ /**
 183+ * Test MWNamespace::subjectEquals
 184+ */
 185+ public function testSubjectEquals() {
 186+ $this->assertTrue( MWNamespace::subjectEquals( NS_MAIN, NS_MAIN ) );
 187+ $this->assertTrue( MWNamespace::subjectEquals( NS_MAIN, 0 ) ); // In case we make NS_MAIN 'MAIN'
 188+ $this->assertTrue( MWNamespace::subjectEquals( NS_USER, NS_USER ) );
 189+ $this->assertTrue( MWNamespace::subjectEquals( NS_USER, 2 ) );
 190+ $this->assertTrue( MWNamespace::subjectEquals( NS_USER_TALK, NS_USER_TALK ) );
 191+ $this->assertTrue( MWNamespace::subjectEquals( NS_SPECIAL, NS_SPECIAL ) );
 192+ $this->assertTrue( MWNamespace::subjectEquals( NS_MAIN, NS_TALK ) );
 193+ $this->assertTrue( MWNamespace::subjectEquals( NS_USER, NS_USER_TALK ) );
 194+ $this->assertFalse( MWNamespace::subjectEquals( NS_PROJECT, NS_TEMPLATE ) );
 195+ }
 196+
 197+ /**
157198 * @todo Implement testGetCanonicalNamespaces().
158199 */
159200 /*
Index: trunk/phase3/includes/Title.php
@@ -1946,6 +1946,57 @@
19471947 }
19481948
19491949 /**
 1950+ * Returns true if the title is inside the specified namespace.
 1951+ *
 1952+ * Please make use of this instead of comparing to getNamespace()
 1953+ * This function is much more resistant to changes we may make
 1954+ * to namespaces than code that makes direct comparisons.
 1955+ * @param $ns The namespace
 1956+ * @return bool
 1957+ * @since 1.19
 1958+ */
 1959+ public function inNamespace( $ns ) {
 1960+ return MWNamespace::equals( $this->getNamespace(), $ns );
 1961+ }
 1962+
 1963+ /**
 1964+ * Returns true if the title is inside one of the specified namespaces.
 1965+ *
 1966+ * @param ...$namespaces The namespaces to check for
 1967+ * @return bool
 1968+ * @since 1.19
 1969+ */
 1970+ public function inNamespaces( /* ... */ ) {
 1971+ $namespaces = func_get_args();
 1972+ if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
 1973+ $namespaces = $namespaces[0];
 1974+ }
 1975+
 1976+ foreach ( $namespaces as $ns ) {
 1977+ if ( $this->inNamespace( $ns ) ) {
 1978+ return true;
 1979+ }
 1980+ }
 1981+
 1982+ return false;
 1983+ }
 1984+
 1985+ /**
 1986+ * Returns true if the title has the same subject namespace as the
 1987+ * namespace specified.
 1988+ * For example this method will take NS_USER and return true if namespace
 1989+ * is either NS_USER or NS_USER_TALK since both of them have NS_USER
 1990+ * as their subject namespace.
 1991+ *
 1992+ * This is MUCH simpler than individually testing for equivilance
 1993+ * against both NS_USER and NS_USER_TALK, and is also forward compatible.
 1994+ * @since 1.19
 1995+ */
 1996+ public function hasSubjectNamespace( $ns ) {
 1997+ return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
 1998+ }
 1999+
 2000+ /**
19502001 * Does this have subpages? (Warning, usually requires an extra DB query.)
19512002 *
19522003 * @return Bool
Index: trunk/phase3/includes/Namespace.php
@@ -58,12 +58,21 @@
5959 *
6060 * @param $index Int: namespace index
6161 * @return bool
 62+ * @since 1.19
6263 */
63 - public static function isMain( $index ) {
 64+ public static function isSubject( $index ) {
6465 return !self::isTalk( $index );
6566 }
6667
6768 /**
 69+ * @see self::isSubject
 70+ * @deprecated Please use the more consistently named isSubject (since 1.19)
 71+ */
 72+ public static function isMain( $index ) {
 73+ return self::isSubject( $index );
 74+ }
 75+
 76+ /**
6877 * Is the given namespace a talk namespace?
6978 *
7079 * @param $index Int: namespace index
@@ -131,6 +140,7 @@
132141 * @param $index
133142 *
134143 * @return bool
 144+ * @since 1.19
135145 */
136146 public static function exists( $index ) {
137147 $nslist = self::getCanonicalNamespaces();
@@ -138,6 +148,39 @@
139149 }
140150
141151 /**
 152+ * Returns whether the specified namespaces are the same namespace
 153+ *
 154+ * @note It's possible that in the future we may start using something
 155+ * other than just namespace indexes. Under that circumstance making use
 156+ * of this function rather than directly doing comparison will make
 157+ * sure that code will not potentially break.
 158+ *
 159+ * @param $ns1 The first namespace index
 160+ * @param $ns2 The second namespae index
 161+ *
 162+ * @return bool
 163+ * @since 1.19
 164+ */
 165+ public static function equals( $ns1, $ns2 ) {
 166+ return $ns1 == $ns2;
 167+ }
 168+
 169+ /**
 170+ * Returns whether the specified namespaces share the same subject.
 171+ * eg: NS_USER and NS_USER wil return true, as well
 172+ * NS_USER and NS_USER_TALK will return true.
 173+ *
 174+ * @param $ns1 The first namespace index
 175+ * @param $ns2 The second namespae index
 176+ *
 177+ * @return bool
 178+ * @since 1.19
 179+ */
 180+ public static function subjectEquals( $ns1, $ns2 ) {
 181+ return self::getSubject( $ns1 ) == self::getSubject( $ns2 );
 182+ }
 183+
 184+ /**
142185 * Returns array of all defined namespaces with their canonical
143186 * (English) names.
144187 *

Follow-up revisions

RevisionCommit summaryAuthorDate
r105891add some namespaces equality tests...hashar15:31, 12 December 2011

Comments

#Comment by Dantman (talk | contribs)   13:37, 22 November 2011

Oh right, I forgot. Also renamed MWNamespace::isMain to MWNamespace::isSubject:

  • Because it's more consistent (we're using getSubject so isSubject makes sense).
  • Because isMain is confusing, MWNamespace::isMain sounds almost like you're asking if the namespace you pass is NS_MAIN.

Of course I included a MWNamespace::isMain back compat method.

#Comment by Happy-melon (talk | contribs)   14:46, 22 November 2011

I see the benefit to abstracting away these comparisons (I finished your first section of your wikitech-l post thinking "whytf", but it actually makes some sense to create more flexibility in these comparisons), but this is not a very object-oriented or modular way of going about it. Surely the more elegant way of doing this is to rework the MWNamespace class into an actual lightweight object that can be instantiated, and return it from $title->getNamespace(). "is this namespace one of these namespaces" is logic that has nothing to do with Title objects, so it does not really belong in Title.php; we're trying to break up those huge ancient classes, not expand them. $title->getNamespace()->isOneOf( NS_FOO, NS_BAR ) is no harder to code, but rather cleaner, especially when we want to start expanding those comparison methods beyond stubs.

#Comment by Dantman (talk | contribs)   14:50, 22 November 2011

Sure... that's a possibility.

Though it'll have even more friction than this change. Every current getNamespace() == NS_USER may break and we'll have to have an entirely new class because MWNamespace has static functions and we'll need to switch to instance methods.

I'll think about that.

We'll probably need a new Title method though.

#Comment by Happy-melon (talk | contribs)   17:00, 22 November 2011

There are loads of ways to avoid breaking stuff:

  1. Convert the NS_ constants to objects and thereby convert to doing object comparisons 'under the hood'.
  2. Allow an implicit cast from a Namespace object to an int; would be nicer, but I don't think PHP allows it.
  3. Redefine the NS_ constants as strings, and define the __toString() magic method on the Namespace class to return the namespace number as a string.
  4. Add a $returnObj parameter to getNamespace() and gradually migrate calls to use it. Or add a separate getNamespaceObj() method to Title, and migrate similarly.
  5. Just migrate stuff (this can be done in conjunction with one of the above). You've basically already said that all getNamespace() == NS_FOO calls will need to be replaced at some point anyway to accommodate new features; it's only slightly more disruptive to do it all in the space of a single release.
#Comment by Brion VIBBER (talk | contribs)   23:56, 22 November 2011

Constants must be scalar values: http://us2.php.net/define

And of course, the database can't store namespace objects into integer fields. Ultimately the DB schema comes down to an integer ID number, and that's what's stored in the various _namespace fields.

:)

You could play with magic toString methods to have them return the number, which might work, but ick! It feels like it would be very fragile.

I would love some nice lightweight namespace objects in a next-gen interface, but probably distinct to avoid conflicts and confusion -- eg, don't override existing getNamespace() methods but have a new method with a different name that returns an object; have things that take namespaces change to take either.

#Comment by Tim Starling (talk | contribs)   00:42, 23 November 2011

My preference is for getNamespaceObj(). Using __toString() will break in lots of subtle ways.

> class Foo { function __toString() { return '1'; } }

> $f = new Foo;

> var_dump($f == '1')
bool(true)

> var_dump($f == 1)
PHP Notice:  Object of class Foo could not be converted to int in ... : eval()'d code on line 1
bool(true)

> var_dump($f === 1)
bool(false)

> var_dump($f === '1')
bool(false)
#Comment by Dantman (talk | contribs)   03:06, 23 November 2011

getNamespaceObj was the one name I could come up with too. Though, TBH that '-Obj' pattern feels like that kind that extension developers will consider a little ugly and potentially decide to just opt to keeping getNamespace() without ever bothering to even start using the new interface in their code.

I almost wish we would have started with a bad naming scheme like getNS. If we were using a name like that we could make use of that as an excuse to switch to a better name like getNamespace for the new api... but we're not. I can't quite think of any name nicer than getNamespace.

#Comment by Hashar (talk | contribs)   17:40, 28 November 2011
> class Foo { function __toString() { return '1'; } }
> $f = new Foo;
> var_dump($f === '1')
bool(false)

That last test is probably the best example to never use __toString() :p

#Comment by Tim Starling (talk | contribs)   23:07, 28 November 2011

In the "$f == 1" case the notice is masking what an epic failure it is. So maybe this is a better example:

> class Foo { function __toString() { return '2'; } }

> $f = new Foo;

> var_dump($f == 2)
PHP Notice:  Object of class Foo could not be converted to int in ... eval()'d code on line 1
bool(false)

> print intval($f)
PHP Notice:  Object of class Foo could not be converted to int in ... eval()'d code on line 1
1

Status & tagging log