diff --git a/README.md b/README.md index cd14cf1..4ec10b9 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Search [![Build Status](https://travis-ci.org/trendwerk/search.svg?branch=master)](https://travis-ci.org/trendwerk/search) [![codecov](https://codecov.io/gh/trendwerk/search/branch/master/graph/badge.svg)](https://codecov.io/gh/trendwerk/search) -Basic extensions for searching in WordPress. Currently only supports searching in `postmeta`. +Basic extensions for searching in WordPress. Quick links: [Install](#install) | [Usage](#usage) | [Dimensions](#dimensions) | [Example](#example) -_Note: This basic extension is not very scalable and meant for smaller databases. This package could get slow for complex meta searches. In that case, [Elasticsearch](https://www.elastic.co/) would be a better solution._ +_Note: This basic extension is not very scalable and meant for smaller databases. This package could get slow for complex searches. In that case, [Elasticsearch](https://www.elastic.co/) would be a better solution._ ## Install ```sh @@ -27,7 +27,7 @@ $search->init(); This code should be run when bootstrapping your theme. ### Dimensions -Currently this package only supports metadata as a search dimension. Dimensions can be added by using `addDimension`: +Currently this package supports metadata and terms as search dimensions. Dimensions can be added by using `addDimension`: ```php $search->addDimension($dimension); @@ -39,7 +39,7 @@ $search->addDimension($dimension); ### Meta ```php -$metaDimension = new \Trendwerk\Search\Dimension\Meta([ +$metaDimension = new \Trendwerk\Search\Dimension\Meta($wpdb, [ 'key' => 'firstName', ]); @@ -53,13 +53,27 @@ Available options for constructing an instance of `Meta`: | `key` | `null` | Yes | The `meta_key` to search for | `compare` | `=` | No | The database comparison that should be made for the meta key. Currently supports `LIKE` and `=`. When using `LIKE`, make sure to include a percent symbol (`%`) in your `key` parameter as a wildcard. See [Example](#example) +### Terms +```php +$search->addDimension(new \Trendwerk\Search\Dimension\Term($wpdb, [ + 'taxonomy' => 'taxonomyName', +])); +``` + +Available options for constructing an instance of `Term`: + +| Parameter | Default | Required | Description | +| :--- | :--- | :--- | :--- | +| `taxonomy` | `null` | Yes | The `taxonomy` which terms should be included in search + ## Example ```php use Trendwerk\Search\Dimension\Meta; +use Trendwerk\Search\Dimension\Term; use Trendwerk\Search\Search; -$search = Search(); +$search = new Search(); $search->init(); $search->addDimension(new Meta($wpdb, [ @@ -70,4 +84,8 @@ $search->addDimension(new Meta($wpdb, [ $search->addDimension(new Meta($wpdb, [ 'key' => 'firstName', ])); + +$search->addDimension(new Term($wpdb, [ + 'taxonomy' => 'category', +])); ``` diff --git a/src/Dimension/Term.php b/src/Dimension/Term.php new file mode 100644 index 0000000..7ebe987 --- /dev/null +++ b/src/Dimension/Term.php @@ -0,0 +1,58 @@ +options = $options; + $this->wpdb = $wpdb; + } + + public function join($aliasCount = 0) + { + $tableAlias = $this->tableAlias . $aliasCount; + + $sql = "INNER JOIN {$this->wpdb->term_relationships} AS {$tableAlias} "; + $sql .= "ON ({$this->wpdb->posts}.ID = {$tableAlias}.object_id)"; + + return $sql; + } + + public function search($searchWord, $aliasCount = 0) + { + $tableAlias = $this->tableAlias . $aliasCount; + $searchWord = $this->wpdb->esc_like($searchWord); + + $termIds = array_map('absint', $this->termsFor($searchWord)); + + if (count($termIds) == 0) { + return; + } + + $termIds = implode(',', $termIds); + + return "{$tableAlias}.term_taxonomy_id IN ({$termIds})"; + } + + private function termsFor($searchWord) + { + return $this->wpdb->get_col($this->wpdb->prepare("SELECT {$this->wpdb->term_taxonomy}.term_id + FROM {$this->wpdb->term_taxonomy} + INNER JOIN {$this->wpdb->terms} USING(term_id) + WHERE taxonomy = %s + AND {$this->wpdb->terms}.name LIKE %s + ", $this->options['taxonomy'], "%{$searchWord}%")); + } +} diff --git a/src/Hook/Posts.php b/src/Hook/Posts.php index 45c3a81..68ccc6a 100644 --- a/src/Hook/Posts.php +++ b/src/Hook/Posts.php @@ -73,6 +73,8 @@ public function search($sql, WP_Query $query) $searches[] = $dimension->search($searchWord, $index); } + $searches = array_filter($searches); + $search = '(' . implode($or, $searches) . ')' . $or; $clause = preg_replace('/' . $or . '/', $or . $search, $clause, 1); diff --git a/tests/Search/Dimension/TermTest.php b/tests/Search/Dimension/TermTest.php new file mode 100644 index 0000000..ad87829 --- /dev/null +++ b/tests/Search/Dimension/TermTest.php @@ -0,0 +1,104 @@ +wpdb = Mockery::mock('wpdb'); + $this->wpdb->posts = 'wp_posts'; + $this->wpdb->term_relationships = 'wp_term_relationships'; + $this->wpdb->term_taxonomy = 'wp_term_taxonomy'; + $this->wpdb->terms = 'wp_terms'; + } + + public function testKeyRequired() + { + $this->expectException(BadMethodCallException::class); + $meta = new Term($this->wpdb, []); + } + + public function testJoin() + { + $tableAliasCount = 2; + $tableAlias = $this->tableAlias . $tableAliasCount; + + $expectation = "INNER JOIN {$this->wpdb->term_relationships} AS {$tableAlias} "; + $expectation .= "ON ({$this->wpdb->posts}.ID = {$tableAlias}.object_id)"; + + $term = $this->create('taxonomyName'); + + $result = $term->join($tableAliasCount); + + $this->assertEquals($expectation, $result); + } + + public function testSearch() + { + $this->search('Testterm', 'testTaxonomy'); + } + + public function testSearchAliasCount() + { + $this->search('Term', 'taxonomy', 2); + } + + public function testNoHit() + { + $this->search('Testterm', 'testTaxonomy', 2, []); + } + + private function search($searchWord, $taxonomy, $tableAliasCount = 0, $foundTermIds = [18, 12]) + { + $tableAlias = $this->tableAlias . $tableAliasCount; + + if (count($foundTermIds) == 0) { + $expectation = ''; + } else { + $termIds = implode(',', $foundTermIds); + $expectation = "{$tableAlias}.term_taxonomy_id IN ({$termIds})"; + } + + $term = $this->create($taxonomy); + + WP_Mock::wpPassthruFunction('absint', ['times' => count($foundTermIds)]); + + $this->wpdb->shouldReceive('esc_like') + ->once() + ->with($searchWord) + ->andReturn($searchWord); + + $this->wpdb->shouldReceive('prepare') + ->once() + ->andReturnUsing(function ($sql, $taxonomy, $search) { + return sprintf($sql, $taxonomy, $search); + }); + + $this->wpdb->shouldReceive('get_col') + ->once() + ->andReturn($foundTermIds); + + $result = $term->search($searchWord, $tableAliasCount); + + $this->assertEquals($expectation, $result); + } + + private function create($taxonomy) + { + $term = new Term($this->wpdb, [ + 'taxonomy' => $taxonomy, + ]); + + return $term; + } +}