diff --git a/_config/config.yml b/_config/blacklist.yml similarity index 73% rename from _config/config.yml rename to _config/blacklist.yml index 37834a5..e37a064 100644 --- a/_config/config.yml +++ b/_config/blacklist.yml @@ -1,10 +1,6 @@ --- -Name: keysforcache +Name: kfc-blacklist --- -SilverStripe\ORM\DataObject: - extensions: - CacheKeyExtension: Terraformers\KeysForCache\Extensions\CacheKeyExtension - Terraformers\KeysForCache\Models\CacheKey: blacklist: CacheKey: Terraformers\KeysForCache\Models\CacheKey diff --git a/_config/cache.yml b/_config/cache.yml new file mode 100644 index 0000000..a866ad4 --- /dev/null +++ b/_config/cache.yml @@ -0,0 +1,8 @@ +--- +Name: kfc-cache +--- +SilverStripe\Core\Injector\Injector: + Psr\SimpleCache\CacheInterface.KeysForCache: + factory: SilverStripe\Core\Cache\CacheFactory + constructor: + namespace: "KeysForCache" diff --git a/_config/extensions.yml b/_config/extensions.yml new file mode 100644 index 0000000..57bfe83 --- /dev/null +++ b/_config/extensions.yml @@ -0,0 +1,6 @@ +--- +Name: kfc-extensions +--- +SilverStripe\ORM\DataObject: + extensions: + CacheKeyExtension: Terraformers\KeysForCache\Extensions\CacheKeyExtension diff --git a/src/RelationshipGraph/Graph.php b/src/RelationshipGraph/Graph.php index 835b874..e595c35 100644 --- a/src/RelationshipGraph/Graph.php +++ b/src/RelationshipGraph/Graph.php @@ -3,46 +3,61 @@ namespace Terraformers\KeysForCache\RelationshipGraph; use Exception; +use Psr\SimpleCache\CacheInterface; use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Config\Config_ForClass; +use SilverStripe\Core\Flushable; use SilverStripe\Core\Injector\Injectable; +use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\DataObject; -class Graph +class Graph implements Flushable { use Injectable; + public const CACHE_KEY = CacheInterface::class . '.KeysForCache'; + public const CACHE_KEY_EDGES = 'Edges'; + public const CACHE_KEY_GLOBAL_CARES = 'GlobalCares'; + private array $nodes = []; private array $edges = []; private array $global_cares = []; public function __construct() { + // Make sure that our Edges and GlobalCares are built and available $this->buildEdges(); $this->buildGlobalCares(); } + public static function flush() + { + Injector::inst()->get(self::CACHE_KEY)->clear(); + + $inst = static::singleton(); + $inst->buildEdges(); + $inst->buildGlobalCares(); + } + public function getEdgesFrom(string $from): array { return array_filter( - $this->edges, + $this->getEdges(), function (Edge $e) use ($from) { return $e->getFromClassName() === $from; } ); } - public function getGlobalCares(): array + public function getEdges(): array { - return $this->global_cares; + return $this->edges; } - public function flush(): void + public function getGlobalCares(): array { - $this->nodes = []; - $this->edges = []; - $this->global_cares = []; + return $this->global_cares; } private function addNode(Node $node): self @@ -69,13 +84,6 @@ private function findOrCreateNode(string $className): Node return $node; } - private function addEdge(Edge $edge): self - { - $this->edges[] = $edge; - - return $this; - } - private function getClassAndRelation(string $input): array { $res = explode('.', $input); @@ -83,9 +91,9 @@ private function getClassAndRelation(string $input): array return [$res[0], $res[1] ?? null]; } - private function getRelationshipConfig(?array $keys, Config_ForClass $config): array + private function getRelationshipConfig(?array $relationships, Config_ForClass $config): array { - if (!$keys) { + if (!$relationships) { return []; } @@ -99,8 +107,8 @@ private function getRelationshipConfig(?array $keys, Config_ForClass $config): a return array_filter( $relationshipConfigs, - function ($relationship) use ($keys) { - return in_array($relationship, $keys); + function ($relationship) use ($relationships) { + return in_array($relationship, $relationships); }, ARRAY_FILTER_USE_KEY ); @@ -176,8 +184,18 @@ private function getRelationType(string $className, string $relation): ?string private function buildEdges(): void { + $cache = Injector::inst()->get(self::CACHE_KEY); + + if ($cache->has(self::CACHE_KEY_EDGES)) { + $this->edges = $cache->get(self::CACHE_KEY_EDGES); + + return; + } + // Relations only exist from data objects $classes = ClassInfo::getValidSubClasses(DataObject::class); + // The Edges that we're about to create + $edges = []; foreach ($classes as $className) { $config = Config::forClass($className); @@ -196,7 +214,7 @@ private function buildEdges(): void // There is a chance that there was no valid Edge (for a reason that we do understand, and did not // want to throw an error for) if ($edge) { - $this->addEdge($edge); + $edges[] = $edge; } continue; @@ -206,7 +224,7 @@ private function buildEdges(): void $touchNode = $this->findOrCreateNode($touchClassName); $edge = new Edge($node, $touchNode, $relation, $this->getRelationType($className, $relation)); - $this->addEdge($edge); + $edges[] = $edge; } // $cares Edges always need to go $from the class being cared about $to this class @@ -224,7 +242,7 @@ private function buildEdges(): void // There is a chance that there was no valid Edge (for a reason that we do understand, and did not // want to throw an error for) if ($edge) { - $this->addEdge($edge); + $edges[] = $edge; } continue; @@ -237,12 +255,12 @@ private function buildEdges(): void // A dot notation is available, so we can map this immediately and continue if ($caresRelation) { $careNode = $this->findOrCreateNode($careClassName); - $this->addEdge(new Edge( + $edges[] = new Edge( $careNode, $node, $caresRelation, $this->getRelationType($careClassName, $caresRelation) - )); + ); continue; } @@ -273,12 +291,12 @@ private function buildEdges(): void } $careNode = $this->findOrCreateNode($careClassName); - $this->addEdge(new Edge( + $edges[] = new Edge( $careNode, $node, $caresRelation, $this->getRelationType($careClassName, $caresRelation) - )); + ); continue; } @@ -294,12 +312,12 @@ private function buildEdges(): void // Yes, it was a has_many on the other end of the relationship. We can add this Edge and continue if ($caresRelation) { $careNode = $this->findOrCreateNode($careClassName); - $this->addEdge(new Edge( + $edges[] = new Edge( $careNode, $node, $caresRelation, $this->getRelationType($careClassName, $caresRelation) - )); + ); continue; } @@ -322,18 +340,29 @@ private function buildEdges(): void } $careNode = $this->findOrCreateNode($careClassName); - $this->addEdge(new Edge( + $edges[] = new Edge( $careNode, $node, $caresRelation, $this->getRelationType($careClassName, $caresRelation) - )); + ); } } + + $cache->set(self::CACHE_KEY_EDGES, $edges); + $this->edges = $cache->get(self::CACHE_KEY_EDGES); } private function buildGlobalCares(): void { + $cache = Injector::inst()->get(self::CACHE_KEY); + + if ($cache->has(self::CACHE_KEY_GLOBAL_CARES)) { + $this->global_cares = $cache->get(self::CACHE_KEY_GLOBAL_CARES); + + return; + } + $classes = ClassInfo::getValidSubClasses(DataObject::class); $classes = array_map( @@ -366,7 +395,8 @@ function($carry, $item) { [] ); - $this->global_cares = $classes; + $cache->set(self::CACHE_KEY_GLOBAL_CARES, $classes); + $this->global_cares = $cache->get(self::CACHE_KEY_GLOBAL_CARES); } private function getThroughClassAndToField(array $throughData): ?array diff --git a/tests/RelationshipGraph/GraphTest.php b/tests/RelationshipGraph/GraphTest.php index 2603685..78703c7 100644 --- a/tests/RelationshipGraph/GraphTest.php +++ b/tests/RelationshipGraph/GraphTest.php @@ -3,7 +3,7 @@ namespace Terraformers\KeysForCache\Tests\RelationshipGraph; use ReflectionClass; -use SilverStripe\CMS\Model\SiteTree; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; use SilverStripe\SiteConfig\SiteConfig; use Terraformers\KeysForCache\RelationshipGraph\Edge; @@ -13,7 +13,6 @@ use Terraformers\KeysForCache\Tests\Mocks\Models\CaredHasManyModel; use Terraformers\KeysForCache\Tests\Mocks\Models\CaredHasOneModel; use Terraformers\KeysForCache\Tests\Mocks\Models\CaredManyManyModel; -use Terraformers\KeysForCache\Tests\Mocks\Models\CaredThroughModel; use Terraformers\KeysForCache\Tests\Mocks\Models\TouchedBelongsToModel; use Terraformers\KeysForCache\Tests\Mocks\Models\TouchedHasManyModel; use Terraformers\KeysForCache\Tests\Mocks\Models\TouchedHasOneModel; @@ -33,9 +32,16 @@ class GraphTest extends SapphireTest public function testAddGetNode(): void { $graph = Graph::singleton(); - $graph->flush(); + // Need to use ReflectionClass as the properties and methods we want to test are private $reflectionClass = new ReflectionClass(Graph::class); + + // Need to flush these properties so that we can explicitly test these methods + $property = $reflectionClass->getProperty('nodes'); + $property->setAccessible(true); + $property->setValue($graph, []); + + // Set accessible for the methods we are testing $add = $reflectionClass->getMethod('addNode'); $add->setAccessible(true); $get = $reflectionClass->getMethod('getNode'); @@ -54,9 +60,16 @@ public function testAddGetNode(): void public function testAddGetNodeNull(): void { $graph = Graph::singleton(); - $graph->flush(); + // Need to use ReflectionClass as the properties and methods we want to test are private $reflectionClass = new ReflectionClass(Graph::class); + + // Need to flush these properties so that we can explicitly test these methods + $property = $reflectionClass->getProperty('nodes'); + $property->setAccessible(true); + $property->setValue($graph, []); + + // Set accessible for the methods we are testing $add = $reflectionClass->getMethod('addNode'); $add->setAccessible(true); $get = $reflectionClass->getMethod('getNode'); @@ -69,66 +82,19 @@ public function testAddGetNodeNull(): void $this->assertNull($get->invoke($graph, NoCachePage::class)); } - public function testAddGetEdge(): void + public function testFindOrCreateNode(): void { $graph = Graph::singleton(); - $graph->flush(); + // Need to use ReflectionClass as the properties and methods we want to test are private $reflectionClass = new ReflectionClass(Graph::class); - $add = $reflectionClass->getMethod('addEdge'); - $add->setAccessible(true); - $get = $reflectionClass->getMethod('getEdgesFrom'); - $get->setAccessible(true); - - $fromNode = new Node(SiteTree::class); - $to = new Node(CaresPage::class); - - $edge = new Edge($fromNode, $to, 'Parent', 'has_one'); - - $add->invoke($graph, $edge); - - /** @var Edge $edge */ - $edges = $get->invoke($graph, SiteTree::class); - $this->assertCount(1, $edges); - - $edge = array_pop($edges); - - $this->assertInstanceOf(Edge::class, $edge); - $this->assertEquals(SiteTree::class, $edge->getFromClassName()); - $this->assertEquals(SiteTree::class, $edge->getFromClassName()); - } - - public function testAddGetEdgeNull(): void - { - $graph = Graph::singleton(); - $graph->flush(); - - $reflectionClass = new ReflectionClass(Graph::class); - $add = $reflectionClass->getMethod('addEdge'); - $add->setAccessible(true); - $get = $reflectionClass->getMethod('getEdgesFrom'); - $get->setAccessible(true); + // Need to flush these properties so that we can explicitly test these methods + $property = $reflectionClass->getProperty('nodes'); + $property->setAccessible(true); + $property->setValue($graph, []); - $fromNode = new Node(SiteTree::class); - $to = new Node(CaresPage::class); - - $edge = new Edge($fromNode, $to, 'Parent', 'has_one'); - - $add->invoke($graph, $edge); - - /** @var Edge $edge */ - $edges = $get->invoke($graph, CaresPage::class); - - $this->assertCount(0, $edges); - } - - public function testFindOrCreateNode(): void - { - $graph = Graph::singleton(); - $graph->flush(); - - $reflectionClass = new ReflectionClass(Graph::class); + // Set accessible for the methods we are testing $find = $reflectionClass->getMethod('findOrCreateNode'); $find->setAccessible(true); $get = $reflectionClass->getMethod('getNode'); @@ -262,4 +228,45 @@ public function testGetGlobalCares(): void $this->assertEquals(GlobalCaresPage::class, array_pop($siteConfigClears)); $this->assertEquals(GlobalCaresPage::class, array_pop($cachePageClears)); } + + public function testCacheSetOnFlush(): void + { + $graph = Graph::singleton(); + // Using ReflectionClass so that we can reset and test that these properties are populated as we expect + $reflectionClass = new ReflectionClass(Graph::class); + $edges = $reflectionClass->getProperty('edges'); + $edges->setAccessible(true); + $cares = $reflectionClass->getProperty('global_cares'); + $cares->setAccessible(true); + + // Reset property values + $edges->setValue($graph, []); + $cares->setValue($graph, []); + // Clear any cache that would have been created during initial instantiation + $cache = Injector::inst()->get(Graph::CACHE_KEY); + $cache->clear(); + + // Check that we're set up with no cache and no property values + $this->assertEmpty($edges->getValue($graph)); + $this->assertEmpty($cares->getValue($graph)); + $this->assertFalse($cache->has(Graph::CACHE_KEY_EDGES)); + $this->assertFalse($cache->has(Graph::CACHE_KEY_GLOBAL_CARES)); + + // Trigger a flush, which should rebuild our cache and set the property values again + Graph::singleton()::flush(); + + // Check that our edges and global_cares properties have been filled during the flush + $this->assertNotEmpty($edges->getValue($graph)); + $this->assertNotEmpty($cares->getValue($graph)); + // There should now also be cache values + $this->assertTrue($cache->has(Graph::CACHE_KEY_EDGES)); + $this->assertTrue($cache->has(Graph::CACHE_KEY_GLOBAL_CARES)); + } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } } diff --git a/tests/Scenarios/CaresTest.php b/tests/Scenarios/CaresTest.php index 076a3a0..09a85f5 100644 --- a/tests/Scenarios/CaresTest.php +++ b/tests/Scenarios/CaresTest.php @@ -2,8 +2,9 @@ namespace Terraformers\KeysForCache\Tests\Scenarios; -use SilverStripe\Dev\Debug; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; +use Terraformers\KeysForCache\RelationshipGraph\Graph; use Terraformers\KeysForCache\Services\ProcessedUpdatesService; use Terraformers\KeysForCache\Tests\Mocks\Models\CaredBelongsToModel; use Terraformers\KeysForCache\Tests\Mocks\Models\CaredHasManyModel; @@ -187,4 +188,11 @@ public function testManyManyThrough(): void $this->assertNotEmpty($newKey); $this->assertNotEquals($originalKey, $newKey); } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } } diff --git a/tests/Scenarios/DotNotationCaresTest.php b/tests/Scenarios/DotNotationCaresTest.php index 42cbdcb..e56d6b9 100644 --- a/tests/Scenarios/DotNotationCaresTest.php +++ b/tests/Scenarios/DotNotationCaresTest.php @@ -2,7 +2,9 @@ namespace Terraformers\KeysForCache\Tests\Scenarios; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; +use Terraformers\KeysForCache\RelationshipGraph\Graph; use Terraformers\KeysForCache\Services\ProcessedUpdatesService; use Terraformers\KeysForCache\Tests\Mocks\Models\DotNotationCaredBelongsToModel; use Terraformers\KeysForCache\Tests\Mocks\Models\DotNotationCaredHasManyModel; @@ -196,4 +198,11 @@ public function testCaresHasMany(): void $this->assertNotEmpty($originalKey); $this->assertNotEquals($originalKey, $newKey); } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } } diff --git a/tests/Scenarios/DotNotationTouchesTest.php b/tests/Scenarios/DotNotationTouchesTest.php index 7826e23..040dd03 100644 --- a/tests/Scenarios/DotNotationTouchesTest.php +++ b/tests/Scenarios/DotNotationTouchesTest.php @@ -2,7 +2,9 @@ namespace Terraformers\KeysForCache\Tests\Scenarios; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; +use Terraformers\KeysForCache\RelationshipGraph\Graph; use Terraformers\KeysForCache\Services\ProcessedUpdatesService; use Terraformers\KeysForCache\Tests\Mocks\Models\DotNotationTouchedBelongsToModel; use Terraformers\KeysForCache\Tests\Mocks\Models\DotNotationTouchedHasManyModel; @@ -186,4 +188,11 @@ public function testTouchesBelongsTo(): void $this->assertNotEmpty($newKey); $this->assertNotEquals($originalKey, $newKey); } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } } diff --git a/tests/Scenarios/ExtendedCaresTest.php b/tests/Scenarios/ExtendedCaresTest.php index 147e930..f54cae9 100644 --- a/tests/Scenarios/ExtendedCaresTest.php +++ b/tests/Scenarios/ExtendedCaresTest.php @@ -2,7 +2,9 @@ namespace Terraformers\KeysForCache\Tests\Scenarios; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; +use Terraformers\KeysForCache\RelationshipGraph\Graph; use Terraformers\KeysForCache\Services\ProcessedUpdatesService; use Terraformers\KeysForCache\Tests\Mocks\Models\CaredBelongsToModel; use Terraformers\KeysForCache\Tests\Mocks\Models\CaredHasManyModel; @@ -137,4 +139,11 @@ public function testCaresHasMany(): void $this->assertNotEmpty($originalKey); $this->assertNotEquals($originalKey, $newKey); } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } } diff --git a/tests/Scenarios/ExtendedTouchesTest.php b/tests/Scenarios/ExtendedTouchesTest.php index 60b1cf3..3266307 100644 --- a/tests/Scenarios/ExtendedTouchesTest.php +++ b/tests/Scenarios/ExtendedTouchesTest.php @@ -2,7 +2,9 @@ namespace Terraformers\KeysForCache\Tests\Scenarios; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; +use Terraformers\KeysForCache\RelationshipGraph\Graph; use Terraformers\KeysForCache\Services\ProcessedUpdatesService; use Terraformers\KeysForCache\Tests\Mocks\Models\TouchedBelongsToModel; use Terraformers\KeysForCache\Tests\Mocks\Models\TouchedHasManyModel; @@ -135,4 +137,11 @@ public function testTouchesBelongsTo(): void $this->assertNotEmpty($originalKey); $this->assertNotEquals($originalKey, $newKey); } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } } diff --git a/tests/Scenarios/GlobalCaresTest.php b/tests/Scenarios/GlobalCaresTest.php index 70368c5..7095acd 100644 --- a/tests/Scenarios/GlobalCaresTest.php +++ b/tests/Scenarios/GlobalCaresTest.php @@ -2,8 +2,10 @@ namespace Terraformers\KeysForCache\Tests\Scenarios; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; use SilverStripe\SiteConfig\SiteConfig; +use Terraformers\KeysForCache\RelationshipGraph\Graph; use Terraformers\KeysForCache\Services\ProcessedUpdatesService; use Terraformers\KeysForCache\Tests\Mocks\Pages\GlobalCaresPage; @@ -39,4 +41,11 @@ public function testGlobalCares() { $this->assertNotEmpty($originalKey); $this->assertNotEquals($originalKey, $newKey); } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } } diff --git a/tests/Scenarios/TouchesTest.php b/tests/Scenarios/TouchesTest.php index 10299ac..bea5e20 100644 --- a/tests/Scenarios/TouchesTest.php +++ b/tests/Scenarios/TouchesTest.php @@ -2,7 +2,9 @@ namespace Terraformers\KeysForCache\Tests\Scenarios; +use SilverStripe\Core\Injector\Injector; use SilverStripe\Dev\SapphireTest; +use Terraformers\KeysForCache\RelationshipGraph\Graph; use Terraformers\KeysForCache\Services\ProcessedUpdatesService; use Terraformers\KeysForCache\Tests\Mocks\Models\TouchedBelongsToModel; use Terraformers\KeysForCache\Tests\Mocks\Models\TouchedHasManyModel; @@ -187,4 +189,11 @@ public function testTouchesBelongsTo(): void $this->assertNotEmpty($originalKey); $this->assertNotEquals($originalKey, $newKey); } + + protected function tearDown(): void + { + Injector::inst()->get(Graph::CACHE_KEY)->clear(); + + parent::tearDown(); + } }