Skip to content

Commit

Permalink
HierarchyLookup add option to find super categories (#4030)
Browse files Browse the repository at this point in the history
  • Loading branch information
mwjames committed May 19, 2019
1 parent df97265 commit d2d42d9
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 34 deletions.
123 changes: 91 additions & 32 deletions src/HierarchyLookup.php
Expand Up @@ -24,8 +24,14 @@ class HierarchyLookup {
/**
* Consecutive hierarchy types
*/
const TYPE_PROPERTY = 'type.property';
const TYPE_CATEGORY = 'type.category';
const TYPE_PROPERTY = 'type/property';
const TYPE_CATEGORY = 'type/category';

/**
* Consecutive hierarchy direction
*/
const TYPE_SUPER = 'type/super';
const TYPE_SUB = 'type/sub';

/**
* @var Store
Expand Down Expand Up @@ -91,15 +97,23 @@ public function addListenersTo( ChangePropListener $changePropListener ) {

$callback = function( $context ) {
$this->cache->delete(
smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_PROPERTY, $this->subpropertyDepth ] )
smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_PROPERTY, self::TYPE_SUB, $this->subpropertyDepth ] )
);

$this->cache->delete(
smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_PROPERTY, self::TYPE_SUPER, $this->subpropertyDepth ] )
);
};

$changePropListener->addListenerCallback( '_SUBP', $callback );

$callback = function( $context ) {
$this->cache->delete(
smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_CATEGORY, $this->subcategoryDepth ] )
smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_CATEGORY, self::TYPE_SUB, $this->subcategoryDepth ] )
);

$this->cache->delete(
smwfCacheKey( self::CACHE_NAMESPACE, [ self::TYPE_CATEGORY, self::TYPE_SUPER, $this->subpropertyDepth ] )
);
};

Expand Down Expand Up @@ -196,24 +210,40 @@ public function findSubcategoryList( DIWikiPage $category ) {
return $this->lookup( '_SUBC', $category->getDBKey(), $category, new RequestOptions() );
}

/**
* @since 3.1
*
* @param DIWikiPage $category
*
* @return DIWikiPage[]|[]
*/
public function findNearbySuperCategories( DIWikiPage $category ) {

if ( $this->subcategoryDepth < 1 ) {
return [];
}

return $this->lookup( new DIProperty( '_SUBC', true ), $category->getDBKey(), $category, new RequestOptions() );
}

/**
* @since 3.0
*
* @param DIProperty|DIWikiPage $id
*
* @return DIProperty[]|DIWikiPage[]|[]
*/
public function getConsecutiveHierarchyList( $id ) {
public function getConsecutiveHierarchyList( $id, $hierarchyType = self::TYPE_SUB ) {

$hierarchyType = null;
$objectType = null;

if ( $id instanceof DIProperty ) {
$hierarchyType = self::TYPE_PROPERTY;
$objectType = self::TYPE_PROPERTY;
} elseif ( $id instanceof DIWikiPage && $id->getNamespace() === NS_CATEGORY ) {
$hierarchyType = self::TYPE_CATEGORY;
$objectType = self::TYPE_CATEGORY;
}

if ( $hierarchyType === null ) {
if ( $objectType === null ) {
throw new InvalidArgumentException( 'No matchable hierarchy type, expected a property or category entity.' );
}

Expand All @@ -226,38 +256,44 @@ public function getConsecutiveHierarchyList( $id ) {
//
// Invalidation of the cache will occur on each _SUBP/_SUBC change event (see
// ChangePropListener).
$depth = $objectType === self::TYPE_PROPERTY ? $this->subpropertyDepth : $this->subcategoryDepth;

$cacheKey = smwfCacheKey(
self::CACHE_NAMESPACE,
[
$objectType,
$hierarchyType,
( $hierarchyType === self::TYPE_PROPERTY ? $this->subpropertyDepth : $this->subcategoryDepth )
$depth
]
);

$hierarchyCache = $this->cache->fetch( $cacheKey );
$reqCacheUpdate = false;
$hierarchyMembers = [];

if ( $hierarchyCache === false ) {
if ( ( $hierarchyCache = $this->cache->fetch( $cacheKey ) ) === false ) {
$hierarchyCache = [];
}

$hierarchyMembers = [];
$key = $hierarchyType === self::TYPE_PROPERTY ? $id->getKey() : $id->getDBKey();
$key = $objectType === self::TYPE_PROPERTY ? $id->getKey() : $id->getDBKey();

if ( !isset( $hierarchyCache[$key] ) ) {
$hierarchyCache[$key] = [];

if ( $hierarchyType === self::TYPE_PROPERTY ) {
if ( $objectType === self::TYPE_PROPERTY ) {
$this->findSubproperties( $hierarchyMembers, $id, 1 );
} else {
$this->findSubcategories( $hierarchyMembers, $id, 1 );
if ( $hierarchyType === self::TYPE_SUPER ) {
$this->findSuperCategoriesByDepth( $hierarchyMembers, $id, 1 );
} else {
$this->findSubcategories( $hierarchyMembers, $id, 1 );
}
}

$hierarchyList[$key] = $hierarchyMembers;

// Store only the key to keep the cache size low
foreach ( $hierarchyList[$key] as $k ) {
if ( $hierarchyType === self::TYPE_PROPERTY ) {
if ( $objectType === self::TYPE_PROPERTY ) {
$hierarchyCache[$key][] = $k->getKey();
} else {
$hierarchyCache[$key][] = $k->getDBKey();
Expand All @@ -269,7 +305,7 @@ public function getConsecutiveHierarchyList( $id ) {
$hierarchyList[$key] = [];

foreach ( $hierarchyCache[$key] as $k ) {
if ( $hierarchyType === self::TYPE_PROPERTY ) {
if ( $objectType === self::TYPE_PROPERTY ) {
$hierarchyList[$key][] = new DIProperty( $k );
} else {
$hierarchyList[$key][] = new DIWikiPage( $k, NS_CATEGORY );
Expand Down Expand Up @@ -324,32 +360,55 @@ private function findSubcategories( &$hierarchyMembers, DIWikiPage $category, $d
}
}

private function lookup( $id, $key, DIWikiPage $subject, $requestOptions ) {
private function findSuperCategoriesByDepth( &$hierarchyMembers, DIWikiPage $category, $depth ) {

if ( $depth++ > $this->subcategoryDepth ) {
return;
}

$key = $id . '#' . $key . '#' . md5( $requestOptions->getHash() );
$categoryList = $this->findNearbySuperCategories(
$category
);

foreach ( $categoryList as $category ) {
$hierarchyMembers[] = $category;
$this->findSuperCategoriesByDepth( $hierarchyMembers, $category, $depth );
}
}

private function lookup( $property, $key, DIWikiPage $subject, $requestOptions ) {

if ( is_string( $property ) ) {
$property = new DIProperty( $property );
}

$key = md5(
$property->getKey() . '#' .
$property->isInverse() . '#' .
$key . '#' .
$requestOptions->getHash()
);

if ( isset( $this->inMemoryCache[$key] ) ) {
return $this->inMemoryCache[$key];
}

$res = $this->store->getPropertySubjects(
new DIProperty( $id ),
$requestOptions->setCaller( __METHOD__ );

$subjects = $this->store->getPropertySubjects(
$property,
$subject,
$requestOptions
);

$this->inMemoryCache[$key] = $res;
$this->inMemoryCache[$key] = $subjects;

$context = [
'method' => __METHOD__,
'role' => 'user',
'id' => $id,
'origin' => $subject
];

$this->logger->info( "[HierarchyLookup] Lookup: {id}, {origin}", $context );
$this->logger->info(
[ 'HierarchyLookup', "Lookup for: {id}, {origin}" ],
[ 'method' => __METHOD__, 'role' => 'user', 'id' => $property->getKey(), 'origin' => $subject ]
);

return $res;
return $subjects;
}

}
23 changes: 23 additions & 0 deletions src/RequestOptions.php
Expand Up @@ -104,6 +104,29 @@ class RequestOptions {
*/
private $options = [];

/**
* @var String|null
*/
private $caller;

/**
* @since 3.1
*
* @param string $caller
*/
public function setCaller( $caller ) {
$this->caller = $caller;
}

/**
* @since 3.1
*
* @return string
*/
public function getCaller() {
return $this->caller;
}

/**
* @since 1.0
*
Expand Down
99 changes: 97 additions & 2 deletions tests/phpunit/Unit/HierarchyLookupTest.php
Expand Up @@ -181,7 +181,7 @@ public function testGetConsecutiveSubpropertyList() {
$this->cache->expects( $this->once() )
->method( 'save' )
->with(
$this->stringContains( ':smw:hierarchy:25840d7839e2fe0369c3fe16014d21d1' ),
$this->stringContains( ':smw:hierarchy:2d440b72499319439ab5f466701f13fa' ),
$this->anything() );

$instance = new HierarchyLookup(
Expand Down Expand Up @@ -270,7 +270,7 @@ public function testGetConsecutiveSubcategoryList() {
$this->cache->expects( $this->once() )
->method( 'save' )
->with(
$this->stringContains( ':smw:hierarchy:d27ae8a4539ee4c7ab76aedcbf1c08c0' ),
$this->stringContains( ':smw:hierarchy:78c9d3ed63c959981731afeef22cc8e9' ),
$this->anything() );

$instance = new HierarchyLookup(
Expand All @@ -290,6 +290,62 @@ public function testGetConsecutiveSubcategoryList() {
);
}

public function testGetConsecutiveSuperCategoryList() {

$category = new DIWikiPage( 'Foo', NS_CATEGORY );

$expected = [
new DIWikiPage( 'Bar', NS_CATEGORY ),
new DIWikiPage( 'Foobar', NS_CATEGORY )
];

$a = DIWikiPage::newFromText( 'Bar', NS_CATEGORY );

$this->store->expects( $this->at( 0 ) )
->method( 'getPropertySubjects' )
->with(
$this->equalTo( new DIProperty( '_SUBC', true ) ),
$this->equalTo( $category ),
$this->anything() )
->will( $this->returnValue( [ $a ] ) );

$b = DIWikiPage::newFromText( 'Foobar', NS_CATEGORY );

$this->store->expects( $this->at( 1 ) )
->method( 'getPropertySubjects' )
->with(
$this->equalTo( new DIProperty( '_SUBC', true ) ),
$this->equalTo( $a ),
$this->anything() )
->will( $this->returnValue( [ $b ] ) );

$this->cache->expects( $this->once() )
->method( 'fetch' )
->will( $this->returnValue( false ) );

$this->cache->expects( $this->once() )
->method( 'save' )
->with(
$this->stringContains( ':smw:hierarchy:c61e6ee84187efaafbb31878af471432' ),
$this->anything() );

$instance = new HierarchyLookup(
$this->store,
$this->cache
);

$instance->setLogger(
$this->spyLogger
);

$instance->setSubcategoryDepth( 2 );

$this->assertEquals(
$expected,
$instance->getConsecutiveHierarchyList( $category, HierarchyLookup::TYPE_SUPER )
);
}

public function testGetConsecutiveCachedSubcategoryList() {

$category = new DIWikiPage( 'Foo', NS_CATEGORY );
Expand Down Expand Up @@ -374,6 +430,45 @@ public function testFindSubcategoryList() {
);
}

public function testFindNearbySuperCategories() {

$category = DIWikiPage::newFromText( 'Foo', NS_CATEGORY );

$expected = [
DIWikiPage::newFromText( 'Bar', NS_CATEGORY )
];

$store = $this->getMockBuilder( '\SMW\Store' )
->disableOriginalConstructor()
->getMockForAbstractClass();

$store->expects( $this->once() )
->method( 'getPropertySubjects' )
->with(
$this->equalTo( new DIProperty( '_SUBC', true ) ),
$this->equalTo( $category ),
$this->anything() )
->will( $this->returnValue( $expected ) );

$cache = $this->getMockBuilder( '\Onoi\Cache\Cache' )
->disableOriginalConstructor()
->getMock();

$instance = new HierarchyLookup(
$store,
$cache
);

$instance->setLogger(
$this->spyLogger
);

$this->assertEquals(
$expected,
$instance->findNearbySuperCategories( $category )
);
}

public function testDisabledSubpropertyLookup() {

$store = $this->getMockBuilder( '\SMW\Store' )
Expand Down

0 comments on commit d2d42d9

Please sign in to comment.