diff --git a/.travis.yml b/.travis.yml index 3450c10..61cee86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -66,6 +66,6 @@ script: - 'if [[ $PHPCS_TEST ]]; then vendor/bin/phpcs --standard=ruleset.xml --extensions=php --tab-width=4 -sp src tests ; fi' - 'if [[ $LINT_CHECK ]]; then vendor/bin/parallel-lint src/ tests/ ; fi' - - 'if [[ $PHPSTAN_TEST ]]; then vendor/bin/phpstan analyse --level=6 -c tests/phpstan.neon src/ ; fi' + - 'if [[ $PHPSTAN_TEST ]]; then vendor/bin/phpstan analyse --level=8 -c tests/phpstan.neon src/ ; fi' after_script: - 'if [[ $PHPUNIT_COVERAGE_TEST ]]; then bash <(curl -s https://codecov.io/bash) -f coverage.xml; fi' diff --git a/composer.json b/composer.json index b79cfec..1806787 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "checkcs": "vendor/bin/phpcs --standard=ruleset.xml --extensions=php --tab-width=4 -sp src tests", "fixcs": "vendor/bin/phpcbf --standard=ruleset.xml --extensions=php --tab-width=4 -sp src tests", "lint": "vendor/bin/parallel-lint src/ tests/", - "phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --level=6 -c tests/phpstan.neon src/", + "phpstan": "vendor/bin/phpstan analyse --memory-limit=2G --level=8 -c tests/phpstan.neon src/", "test": "vendor/bin/phpunit tests" } } diff --git a/src/Base/IndexCreator.php b/src/Base/IndexCreator.php index d276355..043cdfc 100644 --- a/src/Base/IndexCreator.php +++ b/src/Base/IndexCreator.php @@ -9,7 +9,6 @@ namespace Suilven\FreeTextSearch\Base; -use SilverStripe\ORM\DataObjectSchema; use Suilven\FreeTextSearch\Indexes; abstract class IndexCreator implements \Suilven\FreeTextSearch\Interfaces\IndexCreator @@ -22,53 +21,6 @@ abstract class IndexCreator implements \Suilven\FreeTextSearch\Interfaces\IndexC abstract public function createIndex(string $indexName): void; - /** - * Helper method to get get field specs for a DataObject relevant to it's index definition - * - * @param string $indexName the name of the index - * @return array - */ - protected function getFieldSpecs(string $indexName): array - { - $indexes = new Indexes(); - $index = $indexes->getIndex($indexName); - $singleton = \singleton($index->getClass()); - - $fields = $this->getFields($indexName); - - /** @var \SilverStripe\ORM\DataObjectSchema $schema */ - $schema = $singleton->getSchema(); - $specs = $schema->fieldSpecs($index->getClass(), DataObjectSchema::INCLUDE_CLASS); - - /** @var array $filteredSpecs the DB specs for fields related to the index */ - $filteredSpecs = []; - - foreach ($fields as $field) { - if ($field === 'Link') { - continue; - } - $fieldType = $specs[$field]; - - // fix likes of varchar(255) - $fieldType = \explode('(', $fieldType)[0]; - - // remove the class name - $fieldType = \explode('.', $fieldType)[1]; - - $filteredSpecs[$field] = $fieldType; - } - - // if Link undefined in the original index specs, add it if the method exists on the singleton dataobject - if (!isset($filteredSpecs['Link'])) { - if (\method_exists($singleton, 'Link')) { - $filteredSpecs['Link'] = 'Varchar'; - } - } - - return $filteredSpecs; - } - - /** @return array */ protected function getStoredFields(string $indexName): array { @@ -77,32 +29,4 @@ protected function getStoredFields(string $indexName): array return $index->getStoredFields(); } - - - /** @return array */ - protected function getFields(string $indexName): array - { - $indexes = new Indexes(); - $index = $indexes->getIndex($indexName); - - $fields = []; - - foreach ($index->getFields() as $field) { - $fields[] = $field; - } - - foreach ($index->getTokens() as $token) { - $fields[] = $token; - } - - foreach ($index->getStoredFields() as $storedField) { - $fields[] = $storedField; - } - - if (!\in_array('Link', $fields, true)) { - $fields[] = 'Link'; - } - - return $fields; - } } diff --git a/src/Base/Indexer.php b/src/Base/Indexer.php index 4c17d16..7969360 100644 --- a/src/Base/Indexer.php +++ b/src/Base/Indexer.php @@ -31,7 +31,11 @@ public function setIndexName(string $newIndexName): void } - /** @return array> */ + /** + * index name -> field name -> list of values + * + * @return array> + */ public function getIndexablePayload(\SilverStripe\ORM\DataObject $dataObject): array { $helper = new IndexingHelper(); @@ -44,11 +48,15 @@ public function getIndexablePayload(\SilverStripe\ORM\DataObject $dataObject): a // populate MVA columns $mvaColumns = $index->getHasManyFields(); + + /** @var string $mvaColumnName */ foreach (\array_keys($mvaColumns) as $mvaColumnName) { $relationship = $mvaColumns[$mvaColumnName]['relationship']; - /** @phpstan-ignore-next-line */ + // @phpstan-ignore-next-line $relationshipDOs = $dataObject->$relationship(); + + /** @var array $values */ $values = []; foreach ($relationshipDOs as $mvaDO) { $values[] = $mvaDO->ID; diff --git a/src/Base/Searcher.php b/src/Base/Searcher.php index 5f276f9..e0f2461 100644 --- a/src/Base/Searcher.php +++ b/src/Base/Searcher.php @@ -13,7 +13,7 @@ abstract class Searcher implements \Suilven\FreeTextSearch\Interfaces\Searcher { - /** @var array $filters */ + /** @var array $filters */ protected $filters; /** @var int */ @@ -35,7 +35,7 @@ abstract class Searcher implements \Suilven\FreeTextSearch\Interfaces\Searcher abstract public function search(?string $q): SearchResults; - /** @param array $filters */ + /** @param array $filters */ public function setFilters(array $filters): void { $this->filters = $filters; diff --git a/src/Extension/IndexingExtension.php b/src/Extension/IndexingExtension.php index 7b4d2d0..42731c0 100644 --- a/src/Extension/IndexingExtension.php +++ b/src/Extension/IndexingExtension.php @@ -44,6 +44,7 @@ public function onBeforeWrite(): void return; } + // @phpstan-ignore-next-line $this->getOwner()->IsDirtyFreeTextSearch = true; } @@ -61,6 +62,8 @@ public function onAfterWrite(): void // a dataobject could belong to multiple indexes. Update them all $helper = new IndexingHelper(); + + // @phpstan-ignore-next-line $indexNames = $helper->getIndexes($this->getOwner()); // @phpstan-ignore-next-line @@ -80,6 +83,8 @@ public function onAfterWrite(): void $indexer = $factory->getIndexer(); foreach ($indexNames as $indexName) { $indexer->setIndexName($indexName); + + // @phpstan-ignore-next-line $indexer->index($this->getOwner()); } } diff --git a/src/Helper/BulkIndexingHelper.php b/src/Helper/BulkIndexingHelper.php index 619380d..db7c598 100644 --- a/src/Helper/BulkIndexingHelper.php +++ b/src/Helper/BulkIndexingHelper.php @@ -113,6 +113,8 @@ public function bulkIndex(string $indexName, bool $dirty = false, ?CLImate $clim $climate->blue(' per second '); } + // assert this as a string, as it can initially be null + /** @var string $clazz */ $clazz = $index->getClass(); $table = Config::inst()->get($clazz, 'table_name'); diff --git a/src/Helper/IndexingHelper.php b/src/Helper/IndexingHelper.php index 59538cf..7a891a5 100644 --- a/src/Helper/IndexingHelper.php +++ b/src/Helper/IndexingHelper.php @@ -86,4 +86,32 @@ public function getFieldsToIndex(DataObject $dataObject): array return $payload; } + + + /** @return array */ + public function getFields(string $indexName): array + { + $indexes = new Indexes(); + $index = $indexes->getIndex($indexName); + + $fields = []; + + foreach ($index->getFields() as $field) { + $fields[] = $field; + } + + foreach ($index->getTokens() as $token) { + $fields[] = $token; + } + + foreach ($index->getStoredFields() as $storedField) { + $fields[] = $storedField; + } + + if (!\in_array('Link', $fields, true)) { + $fields[] = 'Link'; + } + + return $fields; + } } diff --git a/src/Helper/SearchHelper.php b/src/Helper/SearchHelper.php new file mode 100644 index 0000000..6f464da --- /dev/null +++ b/src/Helper/SearchHelper.php @@ -0,0 +1,54 @@ +> + */ + public function getTextFieldPayload(DataObject $dataObject): array + { + $helper = new IndexingHelper(); + $fullPayload = $helper->getFieldsToIndex($dataObject); + + $textPayload = []; + + $keys = \array_keys($fullPayload); + $specsHelper = new SpecsHelper(); + + foreach ($keys as $key) { + if ($fullPayload[$key] === []) { + continue; + } + + $textPayload[$key] = []; + $specs = $specsHelper->getFieldSpecs($key); + + foreach (\array_keys($specs) as $field) { + // skip link field + if ($field === 'Link') { + continue; + } + $type = $specs[$field]; + if (!\in_array($type, ['Varchar', 'HTMLText'], true)) { + continue; + } + + $textPayload[$key][$field] = (string) $fullPayload[$key][$field]; + } + } + + return $textPayload; + } +} diff --git a/src/Helper/SpecsHelper.php b/src/Helper/SpecsHelper.php new file mode 100644 index 0000000..410b9f7 --- /dev/null +++ b/src/Helper/SpecsHelper.php @@ -0,0 +1,63 @@ + + */ + public function getFieldSpecs(string $indexName): array + { + $indexes = new Indexes(); + $index = $indexes->getIndex($indexName); + $singleton = \singleton((string)($index->getClass())); + + $helper = new IndexingHelper(); + $fields = $helper->getFields($indexName); + + /** @var \SilverStripe\ORM\DataObjectSchema $schema */ + $schema = $singleton->getSchema(); + $specs = $schema->fieldSpecs((string) $index->getClass(), DataObjectSchema::INCLUDE_CLASS); + + /** @var array $filteredSpecs the DB specs for fields related to the index */ + $filteredSpecs = []; + + foreach ($fields as $field) { + if ($field === 'Link') { + continue; + } + $fieldType = $specs[$field]; + + // fix likes of varchar(255) + $fieldType = \explode('(', $fieldType)[0]; + + // remove the class name + $fieldType = \explode('.', $fieldType)[1]; + + $filteredSpecs[$field] = $fieldType; + } + + // if Link undefined in the original index specs, add it if the method exists on the singleton dataobject + if (!isset($filteredSpecs['Link'])) { + if (\method_exists($singleton, 'Link')) { + $filteredSpecs['Link'] = 'Varchar'; + } + } + + return $filteredSpecs; + } +} diff --git a/src/Indexes.php b/src/Indexes.php index 6dd4a86..949cbed 100644 --- a/src/Indexes.php +++ b/src/Indexes.php @@ -13,8 +13,8 @@ */ class Indexes { - /** @var array|null */ - private $indexesByName; + /** @var array */ + private $indexesByName = []; /** @@ -22,7 +22,7 @@ class Indexes */ public function getIndex(string $name): Index { - if (\is_null($this->indexesByName)) { + if ($this->indexesByName === []) { $this->getIndexes(); } @@ -107,7 +107,7 @@ public function getIndexes(): array $this->indexesByName[$index->getName()] = $index; } - + return $this->indexesByName; } diff --git a/src/Interfaces/Indexer.php b/src/Interfaces/Indexer.php index 6e28c41..54919bf 100644 --- a/src/Interfaces/Indexer.php +++ b/src/Interfaces/Indexer.php @@ -11,6 +11,7 @@ use SilverStripe\ORM\DataObject; +// @phpcs:disable SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification interface Indexer { @@ -24,6 +25,6 @@ public function index(DataObject $dataObject): void; public function setIndexName(string $newIndexName): void; // this is provided by the base indexer - /** @return array> */ + /* @return array> */ public function getIndexablePayload(\SilverStripe\ORM\DataObject $dataObject): array; } diff --git a/src/Interfaces/Searcher.php b/src/Interfaces/Searcher.php index aadcc25..32f151a 100644 --- a/src/Interfaces/Searcher.php +++ b/src/Interfaces/Searcher.php @@ -9,12 +9,13 @@ namespace Suilven\FreeTextSearch\Interfaces; +use SilverStripe\ORM\DataObject; use Suilven\FreeTextSearch\Container\SearchResults; //@phpcs:disable SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification interface Searcher { - /** @param array $filters */ + /** @param array $filters */ public function setFilters(array $filters): void; @@ -40,4 +41,8 @@ public function setPage(int $pageNumber): void; /** @param string $q the search query */ public function search(?string $q): SearchResults; + + + /** @param \SilverStripe\ORM\DataObject $dataObject a dataObject relevant to the index */ + public function searchForSimilar(DataObject $dataObject): SearchResults; } diff --git a/src/Page/SearchPageController.php b/src/Page/SearchPageController.php index 75150de..893e27b 100644 --- a/src/Page/SearchPageController.php +++ b/src/Page/SearchPageController.php @@ -130,6 +130,8 @@ public function index(): \SilverStripe\View\ViewableData_Customised $indexes = new Indexes(); $index = $indexes->getIndex($model->IndexToSearch); + + /** @var string $clazz */ $clazz = $index->getClass(); $templateName = 'Suilven/FreeTextSearch/' . \str_replace('\\', '/', $clazz); diff --git a/src/Task/CreateIndexTask.php b/src/Task/CreateIndexTask.php index 97b5dc9..9cc73d1 100644 --- a/src/Task/CreateIndexTask.php +++ b/src/Task/CreateIndexTask.php @@ -35,6 +35,7 @@ public function run($request) $climate = new CLImate(); // check this script is being run by admin + // @phpstan-ignore-next-line $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN")); // for testing purposes diff --git a/src/Task/ReindexTask.php b/src/Task/ReindexTask.php index 8a0c774..bed4fc1 100644 --- a/src/Task/ReindexTask.php +++ b/src/Task/ReindexTask.php @@ -45,6 +45,7 @@ public function run($request) $climate = new CLImate(); // check this script is being run by admin + // @phpstan-ignore-next-line $canAccess = (Director::isDev() || Director::is_cli() || Permission::check("ADMIN")); // for testing purposes diff --git a/tests/Factory/IndexCreatorFactoryTest.php b/tests/Factory/IndexCreatorFactoryTest.php index 1558b29..89b81f6 100644 --- a/tests/Factory/IndexCreatorFactoryTest.php +++ b/tests/Factory/IndexCreatorFactoryTest.php @@ -4,6 +4,7 @@ use SilverStripe\Dev\SapphireTest; use Suilven\FreeTextSearch\Factory\IndexCreatorFactory; +use Suilven\FreeTextSearch\Helper\SpecsHelper; use Suilven\FreeTextSearch\Tests\Mock\IndexCreator; class IndexCreatorFactoryTest extends SapphireTest @@ -25,11 +26,7 @@ public function testFactory(): void $this->assertEquals(['Link'], $indexCreator->getIndexStoredFields()); - $reflection = new \ReflectionClass(\get_class($indexCreator)); - $method = $reflection->getMethod('getFieldSpecs'); - $method->setAccessible(true); - - $specs = $method->invokeArgs($indexCreator, ['sitetree']); + $helper = new SpecsHelper(); $this->assertEquals([ 'Title' => 'Varchar', 'Content' => 'HTMLText', @@ -39,16 +36,14 @@ public function testFactory(): void 'Created' => 'DBDatetime', 'LastEdited' => 'DBDatetime', 'Link' => 'Varchar', - ], $specs); + ], $helper->getFieldSpecs('sitetree')); - $specs = $method->invokeArgs($indexCreator, ['members']); $this->assertEquals([ 'FirstName' => 'Varchar', 'Surname' => 'Varchar', 'Email' => 'Varchar', - ], $specs); + ], $helper->getFieldSpecs('members')); - $specs = $method->invokeArgs($indexCreator, ['flickrphotos']); $this->assertEquals([ 'Title' => 'Varchar', 'Description' => 'HTMLText', @@ -56,6 +51,6 @@ public function testFactory(): void 'ShutterSpeed' => 'Varchar', 'ISO' => 'Int', // @todo test fails here with missing link - ], $specs); + ], $helper->getFieldSpecs('flickrphotos')); } } diff --git a/tests/Helper/SearchHelperTest.php b/tests/Helper/SearchHelperTest.php new file mode 100644 index 0000000..1143f08 --- /dev/null +++ b/tests/Helper/SearchHelperTest.php @@ -0,0 +1,26 @@ +first(); + \error_log($page->Title); + + $helper = new SearchHelper(); + $payload = $helper->getTextFieldPayload($page); + $this->assertEquals(['sitetree' => [ + 'Title' => 'The Break In San Marino Is Bright', + 'Content' => 'The wind in Kenya is waste.', + 'MenuTitle' => 'The Break In San Marino Is Bright', + ]], $payload); + } +} diff --git a/tests/Mock/Searcher.php b/tests/Mock/Searcher.php index 027d123..a56ade9 100644 --- a/tests/Mock/Searcher.php +++ b/tests/Mock/Searcher.php @@ -9,7 +9,10 @@ namespace Suilven\FreeTextSearch\Tests\Mock; +// @phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter + use SilverStripe\ORM\ArrayList; +use SilverStripe\ORM\DataObject; use Suilven\FreeTextSearch\Container\SearchResults; class Searcher extends \Suilven\FreeTextSearch\Base\Searcher @@ -48,4 +51,11 @@ public function search(?string $q): SearchResults return $result; } + + + /** @param \SilverStripe\ORM\DataObject $dataObject a dataObject relevant to the index */ + public function searchForSimilar(DataObject $dataObject): SearchResults + { + return $this->search('Fish'); + } }