diff --git a/TODO b/TODO index c217c8d6..e37e1f7d 100644 --- a/TODO +++ b/TODO @@ -28,16 +28,17 @@ everything related to tags: - tag editing - category (from config.ini) - description - - implications - when entering child tag, add parent tag - - take care of recursion - - listing - - sort alphabetically - - sort by time of addition - - adding - - removing - - editing - - source tag - - target tags (allow multiple) + - relations + - handle tag relations editing in frontend + - privileges + - template + - ajax + - handle tag relations editing in backend + - tagservice::update + - tageditformdata + - tagcontroller + - privileges + - handle relations in autocomplete refactors: - add enum validation in IValidatables (needs refactors of enums and diff --git a/src/Dao/TagDao.php b/src/Dao/TagDao.php index 1127f30b..0793a79d 100644 --- a/src/Dao/TagDao.php +++ b/src/Dao/TagDao.php @@ -3,9 +3,14 @@ namespace Szurubooru\Dao; use Szurubooru\Dao\EntityConverters\PostEntityConverter; use Szurubooru\Dao\EntityConverters\TagEntityConverter; use Szurubooru\DatabaseConnection; +use Szurubooru\Entities\Entity; +use Szurubooru\Entities\Tag; class TagDao extends AbstractDao implements ICrudDao { + const TAG_RELATION_IMPLICATION = 1; + const TAG_RELATION_SUGGESTION = 2; + public function __construct(DatabaseConnection $databaseConnection) { parent::__construct( @@ -49,7 +54,7 @@ class TagDao extends AbstractDao implements ICrudDao ->disableSmartJoin() ->innerJoin('postTags pt1 ON pt1.tagId = tags.id') ->innerJoin('postTags pt2 ON pt2.postId = pt1.postId') - ->where('pt2.tagId = ?', $tagId) + ->where('pt2.tagId', $tagId) ->groupBy('tags.id') ->orderBy('tags.usages DESC, name ASC'); $arrayEntities = iterator_to_array($query); @@ -60,4 +65,87 @@ class TagDao extends AbstractDao implements ICrudDao { $this->deleteBy('usages', 0); } + + protected function afterLoad(Entity $tag) + { + $tag->setLazyLoader( + Tag::LAZY_LOADER_IMPLIED_TAGS, + function (Tag $tag) + { + return $this->findImpliedTags($tag); + }); + + $tag->setLazyLoader( + Tag::LAZY_LOADER_SUGGESTED_TAGS, + function (Tag $tag) + { + return $this->findSuggested($tag); + }); + } + + protected function afterSave(Entity $tag) + { + $this->syncImpliedTags($tag); + $this->syncSuggestedTags($tag); + } + + private function findImpliedTags(Tag $tag) + { + return $this->findRelatedTagsByType($tag, self::TAG_RELATION_IMPLICATION); + } + + private function findSuggested(Tag $tag) + { + return $this->findRelatedTagsByType($tag, self::TAG_RELATION_SUGGESTION); + } + + private function syncImpliedTags($tag) + { + $this->syncRelatedTagsByType($tag, $tag->getImpliedTags(), self::TAG_RELATION_IMPLICATION); + } + + private function syncSuggestedTags($tag) + { + $this->syncRelatedTagsByType($tag, $tag->getSuggestedTags(), self::TAG_RELATION_SUGGESTION); + } + + private function syncRelatedTagsByType(Tag $tag, array $relatedTags, $type) + { + $this->fpdo->deleteFrom('tagRelations') + ->where('tag1id', $tag->getId()) + ->where('type', $type) + ->execute(); + + $relatedTagIds = array_filter(array_unique(array_map( + function ($tag) + { + if (!$tag->getId()) + throw new \RuntimeException('Unsaved entities found'); + return $tag->getId(); + }, + $relatedTags))); + + foreach ($relatedTagIds as $tagId) + { + $this->fpdo + ->insertInto('tagRelations') + ->values([ + 'tag1id' => $tag->getId(), + 'tag2id' => $tagId, + 'type' => $type]) + ->execute(); + } + } + + private function findRelatedTagsByType(Tag $tag, $type) + { + $tagId = $tag->getId(); + $query = $this->fpdo->from($this->tableName) + ->disableSmartJoin() + ->innerJoin('tagRelations tr ON tags.id = tr.tag2id') + ->where('tr.type', $type) + ->where('tr.tag1id', $tagId); + $arrayEntities = iterator_to_array($query); + return $this->arrayToEntities($arrayEntities); + } } diff --git a/src/Entities/Tag.php b/src/Entities/Tag.php index ecd8f596..80a7b7d4 100644 --- a/src/Entities/Tag.php +++ b/src/Entities/Tag.php @@ -7,6 +7,9 @@ final class Tag extends Entity private $creationTime; private $banned = false; + const LAZY_LOADER_IMPLIED_TAGS = 'implications'; + const LAZY_LOADER_SUGGESTED_TAGS = 'suggestions'; + const META_USAGES = 'usages'; public function getName() @@ -44,4 +47,23 @@ final class Tag extends Entity return $this->getMeta(self::META_USAGES); } + public function getImpliedTags() + { + return $this->lazyLoad(self::LAZY_LOADER_IMPLIED_TAGS, []); + } + + public function setImpliedTags(array $impliedTags) + { + $this->lazySave(self::LAZY_LOADER_IMPLIED_TAGS, $impliedTags); + } + + public function getSuggestedTags() + { + return $this->lazyLoad(self::LAZY_LOADER_SUGGESTED_TAGS, []); + } + + public function setSuggestedTags(array $suggestedTags) + { + $this->lazySave(self::LAZY_LOADER_SUGGESTED_TAGS, $suggestedTags); + } } diff --git a/src/Upgrades/Upgrade24.php b/src/Upgrades/Upgrade24.php new file mode 100644 index 00000000..e3d72c02 --- /dev/null +++ b/src/Upgrades/Upgrade24.php @@ -0,0 +1,22 @@ +getPDO(); + $driver = $databaseConnection->getDriver(); + + $pdo->exec( + 'CREATE TABLE tagRelations ( + id INTEGER PRIMARY KEY ' . ($driver === 'mysql' ? 'AUTO_INCREMENT' : 'AUTOINCREMENT') . ', + tag1id INTEGER NOT NULL, + tag2id INTEGER NOT NULL, + type INTEGER(2) NOT NULL, + UNIQUE (tag1id, tag2id, type) + )'); + } +} diff --git a/src/di.php b/src/di.php index 2d040a95..baa74639 100644 --- a/src/di.php +++ b/src/di.php @@ -39,6 +39,7 @@ return [ $container->get(\Szurubooru\Upgrades\Upgrade21::class), $container->get(\Szurubooru\Upgrades\Upgrade22::class), $container->get(\Szurubooru\Upgrades\Upgrade23::class), + $container->get(\Szurubooru\Upgrades\Upgrade24::class), ]; }), diff --git a/tests/Dao/TagDaoTest.php b/tests/Dao/TagDaoTest.php index a054da4b..b82e68c6 100644 --- a/tests/Dao/TagDaoTest.php +++ b/tests/Dao/TagDaoTest.php @@ -25,6 +25,45 @@ final class TagDaoTest extends AbstractDatabaseTestCase $this->assertEntitiesEqual($tag, $actualTag); } + public function testSavingRelations() + { + $tag1 = new Tag(); + $tag1->setName('test 1'); + $tag1->setCreationTime(date('c')); + $tag2 = new Tag(); + $tag2->setName('test 2'); + $tag2->setCreationTime(date('c')); + $tag3 = new Tag(); + $tag3->setName('test 3'); + $tag3->setCreationTime(date('c')); + $tag4 = new Tag(); + $tag4->setName('test 4'); + $tag4->setCreationTime(date('c')); + $tagDao = $this->getTagDao(); + $tagDao->save($tag1); + $tagDao->save($tag2); + $tagDao->save($tag3); + $tagDao->save($tag4); + + $tag = new Tag(); + $tag->setName('test1'); + $tag->setCreationTime(date('c')); + $tag->setImpliedTags([$tag1, $tag3]); + $tag->setSuggestedTags([$tag2, $tag4]); + + $this->assertGreaterThan(0, count($tag->getImpliedTags())); + $this->assertGreaterThan(0, count($tag->getSuggestedTags())); + + $tagDao->save($tag); + $actualTag = $tagDao->findById($tag->getId()); + + $this->assertEntitiesEqual($tag, $actualTag); + $this->assertEntitiesEqual(array_values($tag->getImpliedTags()), array_values($actualTag->getImpliedTags())); + $this->assertEntitiesEqual(array_values($tag->getSuggestedTags()), array_values($actualTag->getSuggestedTags())); + $this->assertGreaterThan(0, count($actualTag->getImpliedTags())); + $this->assertGreaterThan(0, count($actualTag->getSuggestedTags())); + } + public function testFindByPostIds() { $pdo = $this->databaseConnection->getPDO();