refactor search for using search by pertinence

This commit is contained in:
2021-11-22 08:28:22 +00:00
parent f06f9c10ad
commit 9fb29ec110
41 changed files with 1071 additions and 727 deletions

View File

@@ -3,6 +3,7 @@
namespace Chill\MainBundle;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Security\Authorization\ChillVoterInterface;
use Chill\MainBundle\Security\ProvideRoleInterface;
@@ -41,6 +42,8 @@ class ChillMainBundle extends Bundle
->addTag('chill_main.scope_resolver');
$container->registerForAutoconfiguration(ChillEntityRenderInterface::class)
->addTag('chill.render_entity');
$container->registerForAutoconfiguration(SearchApiInterface::class)
->addTag('chill.search_api_provider');
$container->addCompilerPass(new SearchableServicesCompilerPass());
$container->addCompilerPass(new ConfigConsistencyCompilerPass());

View File

@@ -5,7 +5,7 @@
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ installation.name }} - {% block title %}{% endblock %}</title>
<title>{{ installation.name }} - {% block title %}{{ 'Homepage'|trans }}{% endblock %}</title>
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
{{ encore_entry_link_tags('mod_bootstrap') }}
@@ -68,6 +68,9 @@
<button type="submit" class="btn btn-lg btn-warning mt-3">
<i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
</button>
<a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
<i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
</a>
</center>
</form>
</div>

View File

@@ -26,10 +26,10 @@ use Chill\MainBundle\Search\ParsingException;
/**
* This class implements abstract search with most common responses.
*
*
* you should use this abstract class instead of SearchInterface : if the signature of
* search interface change, the generic method will be implemented here.
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*
*/
@@ -37,7 +37,7 @@ abstract class AbstractSearch implements SearchInterface
{
/**
* parse string expected to be a date and transform to a DateTime object
*
*
* @param type $string
* @return \DateTime
* @throws ParsingException if the date is not parseable
@@ -51,14 +51,14 @@ abstract class AbstractSearch implements SearchInterface
. 'not parsable', 0, $ex);
throw $exception;
}
}
/**
* recompose a pattern, retaining only supported terms
*
*
* the outputted string should be used to show users their search
*
*
* @param array $terms
* @param array $supportedTerms
* @param string $domain if your domain is NULL, you should set NULL. You should set used domain instead
@@ -67,35 +67,35 @@ abstract class AbstractSearch implements SearchInterface
protected function recomposePattern(array $terms, array $supportedTerms, $domain = NULL)
{
$recomposed = '';
if ($domain !== NULL)
{
$recomposed .= '@'.$domain.' ';
}
foreach ($supportedTerms as $term) {
if (array_key_exists($term, $terms) && $term !== '_default') {
$recomposed .= ' '.$term.':';
$containsSpace = \strpos($terms[$term], " ") !== false;
if ($containsSpace) {
$recomposed .= "(";
$recomposed .= '"';
}
$recomposed .= (mb_stristr(' ', $terms[$term]) === FALSE) ? $terms[$term] : '('.$terms[$term].')';
if ($containsSpace) {
$recomposed .= ")";
$recomposed .= '"';
}
}
}
if ($terms['_default'] !== '') {
$recomposed .= ' '.$terms['_default'];
}
//strip first character if empty
if (mb_strcut($recomposed, 0, 1) === ' '){
$recomposed = mb_strcut($recomposed, 1);
}
return $recomposed;
}
}
}

View File

@@ -20,19 +20,15 @@ class SearchApi
private EntityManagerInterface $em;
private PaginatorFactory $paginator;
private array $providers = [];
private iterable $providers = [];
public function __construct(
EntityManagerInterface $em,
SearchPersonApiProvider $searchPerson,
ThirdPartyApiSearch $thirdPartyApiSearch,
SearchUserApiProvider $searchUser,
iterable $providers,
PaginatorFactory $paginator
) {
$this->em = $em;
$this->providers[] = $searchPerson;
$this->providers[] = $thirdPartyApiSearch;
$this->providers[] = $searchUser;
$this->providers = $providers;
$this->paginator = $paginator;
}
@@ -68,10 +64,15 @@ class SearchApi
private function findProviders(string $pattern, array $types, array $parameters): array
{
return \array_filter(
$this->providers,
fn($p) => $p->supportsTypes($pattern, $types, $parameters)
);
$providers = [];
foreach ($this->providers as $provider) {
if ($provider->supportsTypes($pattern, $types, $parameters)) {
$providers[] = $provider;
}
}
return $providers;
}
private function countItems($providers, $types, $parameters): int
@@ -82,12 +83,12 @@ class SearchApi
$countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
$countNq->setParameters($parameters);
return $countNq->getSingleScalarResult();
return (int) $countNq->getSingleScalarResult();
}
private function buildCountQuery(array $queries, $types, $parameters)
{
$query = "SELECT COUNT(*) AS count FROM ({union_unordered}) AS sq";
$query = "SELECT SUM(c) AS count FROM ({union_unordered}) AS sq";
$unions = [];
$parameters = [];
@@ -141,17 +142,20 @@ class SearchApi
private function prepareProviders(array $rawResults)
{
$metadatas = [];
$providers = [];
foreach ($rawResults as $r) {
foreach ($this->providers as $k => $p) {
if ($p->supportsResult($r['key'], $r['metadata'])) {
$metadatas[$k][] = $r['metadata'];
$providers[$k] = $p;
break;
}
}
}
foreach ($metadatas as $k => $m) {
$this->providers[$k]->prepare($m);
$providers[$k]->prepare($m);
}
}

View File

@@ -4,6 +4,8 @@ namespace Chill\MainBundle\Search;
class SearchApiQuery
{
private array $select = [];
private array $selectParams = [];
private ?string $selectKey = null;
private array $selectKeyParams = [];
private ?string $jsonbMetadata = null;
@@ -15,6 +17,38 @@ class SearchApiQuery
private array $whereClauses = [];
private array $whereClausesParams = [];
public function addSelectClause(string $select, array $params = []): self
{
$this->select[] = $select;
$this->selectParams = [...$this->selectParams, ...$params];
return $this;
}
public function resetSelectClause(): self
{
$this->select = [];
$this->selectParams = [];
$this->selectKey = null;
$this->selectKeyParams = [];
$this->jsonbMetadata = null;
$this->jsonbMetadataParams = [];
$this->pertinence = null;
$this->pertinenceParams = [];
return $this;
}
public function getSelectClauses(): array
{
return $this->select;
}
public function getSelectParams(): array
{
return $this->selectParams;
}
public function setSelectKey(string $selectKey, array $params = []): self
{
$this->selectKey = $selectKey;
@@ -47,6 +81,16 @@ class SearchApiQuery
return $this;
}
public function getFromClause(): string
{
return $this->fromClause;
}
public function getFromParams(): array
{
return $this->fromClauseParams;
}
/**
* Set the where clause and replace all existing ones.
*
@@ -54,7 +98,7 @@ class SearchApiQuery
public function setWhereClauses(string $whereClause, array $params = []): self
{
$this->whereClauses = [$whereClause];
$this->whereClausesParams = [$params];
$this->whereClausesParams = $params;
return $this;
}
@@ -71,11 +115,53 @@ class SearchApiQuery
public function andWhereClause(string $whereClause, array $params = []): self
{
$this->whereClauses[] = $whereClause;
$this->whereClausesParams[] = $params;
\array_push($this->whereClausesParams, ...$params);
return $this;
}
private function buildSelectParams(bool $count = false): array
{
if ($count) {
return [];
}
$args = $this->getSelectParams();
if (null !== $this->selectKey) {
$args = [...$args, ...$this->selectKeyParams];
}
if (null !== $this->jsonbMetadata) {
$args = [...$args, ...$this->jsonbMetadataParams];
}
if (null !== $this->pertinence) {
$args = [...$args, ...$this->pertinenceParams];
}
return $args;
}
private function buildSelectClause(bool $countOnly = false): string
{
if ($countOnly) {
return 'count(*) AS c';
}
$selects = $this->getSelectClauses();
if (null !== $this->selectKey) {
$selects[] = \strtr("'{key}' AS key", [ '{key}' => $this->selectKey ]);
}
if (null !== $this->jsonbMetadata) {
$selects[] = \strtr('{metadata} AS metadata', [ '{metadata}' => $this->jsonbMetadata]);
}
if (null !== $this->pertinence) {
$selects[] = \strtr('{pertinence} AS pertinence', [ '{pertinence}' => $this->pertinence]);
}
return \implode(', ', $selects);
}
public function buildQuery(bool $countOnly = false): string
{
$isMultiple = count($this->whereClauses);
@@ -87,19 +173,8 @@ class SearchApiQuery
($isMultiple ? ')' : '')
;
if (!$countOnly) {
$select = \strtr("
'{key}' AS key,
{metadata} AS metadata,
{pertinence} AS pertinence
", [
'{key}' => $this->selectKey,
'{metadata}' => $this->jsonbMetadata,
'{pertinence}' => $this->pertinence,
]);
} else {
$select = "1 AS c";
}
$select = $this->buildSelectClause($countOnly);
return \strtr("SELECT
{select}
@@ -116,18 +191,16 @@ class SearchApiQuery
public function buildParameters(bool $countOnly = false): array
{
if (!$countOnly) {
return \array_merge(
$this->selectKeyParams,
$this->jsonbMetadataParams,
$this->pertinenceParams,
$this->fromClauseParams,
\array_merge([], ...$this->whereClausesParams),
);
return [
...$this->buildSelectParams($countOnly),
...$this->fromClauseParams,
...$this->whereClausesParams,
];
} else {
return \array_merge(
$this->fromClauseParams,
\array_merge([], ...$this->whereClausesParams),
);
return [
...$this->fromClauseParams,
...$this->whereClausesParams,
];
}
}
}

View File

@@ -10,10 +10,10 @@ use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
* installed into the app.
* the service is callable from the container with
* $container->get('chill_main.search_provider')
*
* the syntax for search string is :
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
* entities. It may exists multiple search provider for the same domain (example:
*
* the syntax for search string is :
* - domain, which begin with `@`. Example: `@person`. Restrict the search to some
* entities. It may exists multiple search provider for the same domain (example:
* a search provider for people which performs regular search, and suggestion search
* with phonetical algorithms
* - terms, which are the terms of the search. There are terms with argument (example :
@@ -25,17 +25,17 @@ class SearchProvider
{
/**
*
*
* @var SearchInterface[]
*/
private $searchServices = array();
/**
*
* @var HasAdvancedSearchForm[]
*/
private $hasAdvancedFormSearchServices = array();
/*
* return search services in an array, ordered by
* the order key (defined in service definition)
@@ -59,7 +59,7 @@ class SearchProvider
return $this->searchServices;
}
public function getHasAdvancedFormSearchServices()
{
//sort the array
@@ -75,7 +75,7 @@ class SearchProvider
/**
* parse the search string to extract domain and terms
*
*
* @param string $pattern
* @return string[] an array where the keys are _domain, _default (residual terms) or term
*/
@@ -95,9 +95,9 @@ class SearchProvider
/**
* Extract the domain of the subject
*
*
* The domain begins with `@`. Example: `@person`, `@report`, ....
*
*
* @param type $subject
* @return string
* @throws ParsingException
@@ -121,14 +121,15 @@ class SearchProvider
private function extractTerms(&$subject)
{
$terms = array();
preg_match_all('/([a-z\-]+):([\w\-]+|\([^\(\r\n]+\))/', $subject, $matches);
$matches = [];
preg_match_all('/([a-z\-]+):(([^"][\S\-]+)|"[^"]*")/', $subject, $matches);
foreach ($matches[2] as $key => $match) {
//remove from search pattern
$this->mustBeExtracted[] = $matches[0][$key];
//strip parenthesis
if (mb_substr($match, 0, 1) === '(' &&
mb_substr($match, mb_strlen($match) - 1) === ')') {
if (mb_substr($match, 0, 1) === '"' &&
mb_substr($match, mb_strlen($match) - 1) === '"') {
$match = trim(mb_substr($match, 1, mb_strlen($match) - 2));
}
$terms[$matches[1][$key]] = $match;
@@ -139,14 +140,14 @@ class SearchProvider
/**
* store string which must be extracted to find default arguments
*
*
* @var string[]
*/
private $mustBeExtracted = array();
/**
* extract default (residual) arguments
*
*
* @param string $subject
* @return string
*/
@@ -158,7 +159,7 @@ class SearchProvider
/**
* search through services which supports domain and give
* results as an array of resultsfrom different SearchInterface
*
*
* @param string $pattern
* @param number $start
* @param number $limit
@@ -167,25 +168,25 @@ class SearchProvider
* @return array of results from different SearchInterface
* @throws UnknowSearchDomainException if the domain is unknow
*/
public function getSearchResults($pattern, $start = 0, $limit = 50,
public function getSearchResults($pattern, $start = 0, $limit = 50,
array $options = array(), $format = 'html')
{
$terms = $this->parse($pattern);
$results = array();
//sort searchServices by order
$sortedSearchServices = array();
foreach($this->searchServices as $service) {
$sortedSearchServices[$service->getOrder()] = $service;
}
if ($terms['_domain'] !== NULL) {
foreach ($sortedSearchServices as $service) {
if ($service->supports($terms['_domain'], $format)) {
$results[] = $service->renderResult($terms, $start, $limit, $options);
}
}
if (count($results) === 0) {
throw new UnknowSearchDomainException($terms['_domain']);
}
@@ -196,24 +197,24 @@ class SearchProvider
}
}
}
//sort array
ksort($results);
return $results;
}
public function getResultByName($pattern, $name, $start = 0, $limit = 50,
array $options = array(), $format = 'html')
array $options = array(), $format = 'html')
{
$terms = $this->parse($pattern);
$search = $this->getByName($name);
if ($terms['_domain'] !== NULL && !$search->supports($terms['_domain'], $format))
{
throw new ParsingException("The domain is not supported for the search name");
}
return $search->renderResult($terms, $start, $limit, $options, $format);
}
@@ -232,16 +233,16 @@ class SearchProvider
throw new UnknowSearchNameException($name);
}
}
/**
* return searchservice with an advanced form, defined in service
* return searchservice with an advanced form, defined in service
* definition.
*
*
* @param string $name
* @return HasAdvancedSearchForm
* @throws UnknowSearchNameException
*/
public function getHasAdvancedFormByName($name)
public function getHasAdvancedFormByName($name)
{
if (\array_key_exists($name, $this->hasAdvancedFormSearchServices)) {
return $this->hasAdvancedFormSearchServices[$name];
@@ -253,7 +254,7 @@ class SearchProvider
public function addSearchService(SearchInterface $service, $name)
{
$this->searchServices[$name] = $service;
if ($service instanceof HasAdvancedSearchFormInterface) {
$this->hasAdvancedFormSearchServices[$name] = $service;
}
@@ -477,7 +478,7 @@ class SearchProvider
$string = strtr($string, $chars);
} /* remove from wordpress: we use only utf 8
* else {
// Assume ISO-8859-1 if not UTF-8
$chars['in'] = chr(128) . chr(131) . chr(138) . chr(142) . chr(154) . chr(158)
. chr(159) . chr(162) . chr(165) . chr(181) . chr(192) . chr(193) . chr(194)

View File

@@ -0,0 +1,36 @@
<?php
namespace Chill\MainBundle\Search\Utils;
use \DateTimeImmutable;
class ExtractDateFromPattern
{
private const DATE_PATTERN = [
["([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))", 'Y-m-d'], // 1981-05-12
["((0[1-9]|[12]\d|3[01])\/(0[1-9]|1[0-2])\/([12]\d{3}))", 'd/m/Y'], // 15/12/1980
["((0[1-9]|[12]\d|3[01])-(0[1-9]|1[0-2])-([12]\d{3}))", 'd-m-Y'], // 15/12/1980
];
public function extractDates(string $subject): SearchExtractionResult
{
$dates = [];
$filteredSubject = $subject;
foreach (self::DATE_PATTERN as [$pattern, $format]) {
$matches = [];
\preg_match_all($pattern, $filteredSubject, $matches);
foreach ($matches[0] as $match) {
$date = DateTimeImmutable::createFromFormat($format, $match);
if (false !== $date) {
$dates[] = $date;
// filter string: remove what is found
$filteredSubject = \trim(\strtr($filteredSubject, [$match => ""]));
}
}
}
return new SearchExtractionResult($filteredSubject, $dates);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Chill\MainBundle\Search\Utils;
class ExtractPhonenumberFromPattern
{
private const PATTERN = "([\+]{0,1}[0-9\ ]{5,})";
public function extractPhonenumber(string $subject): SearchExtractionResult
{
$matches = [];
\preg_match(self::PATTERN, $subject,$matches);
if (0 < count($matches)) {
$phonenumber = [];
$length = 0;
foreach (\str_split(\trim($matches[0])) as $key => $char) {
switch ($char) {
case '0':
$length++;
if ($key === 0) { $phonenumber[] = '+32'; }
else { $phonenumber[] = $char; }
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
$length++;
$phonenumber[] = $char;
break;
case ' ':
break;
default:
throw new \LogicException("should not match not alnum character");
}
}
if ($length > 5) {
$filtered = \trim(\strtr($subject, [$matches[0] => '']));
return new SearchExtractionResult($filtered, [\implode('', $phonenumber)] );
}
}
return new SearchExtractionResult($subject, []);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Chill\MainBundle\Search\Utils;
class SearchExtractionResult
{
private string $filteredSubject;
private array $found;
public function __construct(string $filteredSubject, array $found)
{
$this->filteredSubject = $filteredSubject;
$this->found = $found;
}
public function getFound(): array
{
return $this->found;
}
public function hasResult(): bool
{
return [] !== $this->found;
}
public function getFilteredSubject(): string
{
return $this->filteredSubject;
}
}

View File

@@ -12,7 +12,7 @@ class SearchApiQueryTest extends TestCase
$q = new SearchApiQuery();
$q->setSelectJsonbMetadata('boum')
->setSelectKey('bim')
->setSelectPertinence('1')
->setSelectPertinence('?', ['gamma'])
->setFromClause('badaboum')
->andWhereClause('foo', [ 'alpha' ])
->andWhereClause('bar', [ 'beta' ])
@@ -21,12 +21,12 @@ class SearchApiQueryTest extends TestCase
$query = $q->buildQuery();
$this->assertStringContainsString('(foo) AND (bar)', $query);
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
$query = $q->buildQuery(true);
$this->assertStringContainsString('(foo) AND (bar)', $query);
$this->assertEquals(['alpha', 'beta'], $q->buildParameters());
$this->assertEquals(['gamma', 'alpha', 'beta'], $q->buildParameters());
}
public function testWithoutWhereClause()
@@ -42,4 +42,20 @@ class SearchApiQueryTest extends TestCase
$this->assertEquals([], $q->buildParameters());
}
public function testBuildParams()
{
$q = new SearchApiQuery();
$q
->addSelectClause('bada', [ 'one', 'two' ])
->addSelectClause('boum', ['three', 'four'])
->setWhereClauses('mywhere', [ 'six', 'seven'])
;
$params = $q->buildParameters();
$this->assertEquals(['six', 'seven'], $q->buildParameters(true));
$this->assertEquals(['one', 'two', 'three', 'four', 'six', 'seven'], $q->buildParameters());
}
}

View File

@@ -26,17 +26,17 @@ use PHPUnit\Framework\TestCase;
class SearchProviderTest extends TestCase
{
/**
*
* @var SearchProvider
* @var SearchProvider
*/
private $search;
public function setUp()
{
$this->search = new SearchProvider();
//add a default service
$this->addSearchService(
$this->createDefaultSearchService('I am default', 10), 'default'
@@ -46,7 +46,7 @@ class SearchProviderTest extends TestCase
$this->createNonDefaultDomainSearchService('I am domain bar', 20, FALSE), 'bar'
);
}
/**
* @expectedException \Chill\MainBundle\Search\UnknowSearchNameException
*/
@@ -54,11 +54,11 @@ class SearchProviderTest extends TestCase
{
$this->search->getByName("invalid name");
}
public function testSimplePattern()
{
$terms = $this->p("@person birthdate:2014-01-02 name:(my name) is not my name");
$terms = $this->p("@person birthdate:2014-01-02 name:\"my name\" is not my name");
$this->assertEquals(array(
'_domain' => 'person',
'birthdate' => '2014-01-02',
@@ -66,40 +66,40 @@ class SearchProviderTest extends TestCase
'name' => 'my name'
), $terms);
}
public function testWithoutDomain()
{
$terms = $this->p('foo:bar residual');
$this->assertEquals(array(
'_domain' => null,
'foo' => 'bar',
'_default' => 'residual'
), $terms);
}
public function testWithoutDefault()
{
$terms = $this->p('@person foo:bar');
$this->assertEquals(array(
'_domain' => 'person',
'foo' => 'bar',
'_default' => ''
), $terms);
}
public function testCapitalLetters()
{
$terms = $this->p('Foo:Bar LOL marCi @PERSON');
$this->assertEquals(array(
'_domain' => 'person',
'_default' => 'lol marci',
'foo' => 'bar'
), $terms);
}
/**
* @expectedException Chill\MainBundle\Search\ParsingException
*/
@@ -107,12 +107,11 @@ class SearchProviderTest extends TestCase
{
$term = $this->p("@person @report");
}
public function testDoubleParenthesis()
{
$terms = $this->p("@papamobile name:(my beautiful name) residual "
. "surname:(i love techno)");
$terms = $this->p('@papamobile name:"my beautiful name" residual surname:"i love techno"');
$this->assertEquals(array(
'_domain' => 'papamobile',
'name' => 'my beautiful name',
@@ -120,65 +119,65 @@ class SearchProviderTest extends TestCase
'surname' => 'i love techno'
), $terms);
}
public function testAccentued()
{
//$this->markTestSkipped('accentued characters must be implemented');
$terms = $this->p('manço bélier aztèque à saloù ê');
$this->assertEquals(array(
'_domain' => NULL,
'_default' => 'manco belier azteque a salou e'
), $terms);
}
public function testAccentuedCapitals()
{
//$this->markTestSkipped('accentued characters must be implemented');
$terms = $this->p('MANÉÀ oÛ lÎ À');
$this->assertEquals(array(
'_domain' => null,
'_default' => 'manea ou li a'
), $terms);
}
public function testTrimInParenthesis()
{
$terms = $this->p('foo:(bar )');
$terms = $this->p('foo:"bar "');
$this->assertEquals(array(
'_domain' => null,
'foo' => 'bar',
'_default' => ''
), $terms);
}
public function testTrimInDefault()
{
$terms = $this->p(' foo bar ');
$this->assertEquals(array(
'_domain' => null,
'_default' => 'foo bar'
), $terms);
}
public function testArgumentNameWithTrait()
{
$terms = $this->p('date-from:2016-05-04');
$this->assertEquals(array(
'_domain' => null,
'date-from' => '2016-05-04',
'_default' => ''
), $terms);
}
/**
* Test the behaviour when no domain is provided in the search pattern :
* Test the behaviour when no domain is provided in the search pattern :
* the default search should be enabled
*/
public function testSearchResultDefault()
@@ -186,12 +185,12 @@ class SearchProviderTest extends TestCase
$response = $this->search->getSearchResults('default search');
//$this->markTestSkipped();
$this->assertEquals(array(
"I am default"
), $response);
), $response);
}
/**
* @expectedException \Chill\MainBundle\Search\UnknowSearchDomainException
*/
@@ -200,49 +199,49 @@ class SearchProviderTest extends TestCase
$response = $this->search->getSearchResults('@unknow domain');
//$this->markTestSkipped();
}
public function testSearchResultDomainSearch()
{
//add a search service which will be supported
$this->addSearchService(
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
);
$response = $this->search->getSearchResults('@foo default search');
$this->assertEquals(array(
"I am domain foo"
), $response);
}
public function testSearchWithinSpecificSearchName()
{
//add a search service which will be supported
$this->addSearchService(
$this->createNonDefaultDomainSearchService("I am domain foo", 100, TRUE), 'foo'
);
$response = $this->search->getResultByName('@foo search', 'foo');
$this->assertEquals('I am domain foo', $response);
}
/**
* @expectedException \Chill\MainBundle\Search\ParsingException
*/
public function testSearchWithinSpecificSearchNameInConflictWithSupport()
{
$response = $this->search->getResultByName('@foo default search', 'bar');
}
/**
* shortcut for executing parse method
*
*
* @param unknown $pattern
* @return string[]
*/
@@ -250,12 +249,12 @@ class SearchProviderTest extends TestCase
{
return $this->search->parse($pattern);
}
/**
* Add a search service to the chill.main.search_provider
*
*
* Useful for mocking the SearchInterface
*
*
* @param SearchInterface $search
* @param string $name
*/
@@ -264,52 +263,52 @@ class SearchProviderTest extends TestCase
$this->search
->addSearchService($search, $name);
}
private function createDefaultSearchService($result, $order)
{
$mock = $this
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
//set the mock as default
$mock->expects($this->any())
->method('isActiveByDefault')
->will($this->returnValue(TRUE));
$mock->expects($this->any())
->method('getOrder')
->will($this->returnValue($order));
//set the return value
$mock->expects($this->any())
->method('renderResult')
->will($this->returnValue($result));
return $mock;
}
private function createNonDefaultDomainSearchService($result, $order, $domain)
{
$mock = $this
->getMockForAbstractClass('Chill\MainBundle\Search\AbstractSearch');
//set the mock as default
$mock->expects($this->any())
->method('isActiveByDefault')
->will($this->returnValue(FALSE));
$mock->expects($this->any())
->method('getOrder')
->will($this->returnValue($order));
$mock->expects($this->any())
->method('supports')
->will($this->returnValue($domain));
//set the return value
$mock->expects($this->any())
->method('renderResult')
->will($this->returnValue($result));
return $mock;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Search\Utils;
use Chill\MainBundle\Search\Utils\ExtractDateFromPattern;
use PHPUnit\Framework\TestCase;
class ExtractDateFromPatternTest extends TestCase
{
/**
* @dataProvider provideSubjects
*/
public function testExtractDate(string $subject, string $filtered, int $count, ...$datesSearched)
{
$extractor = new ExtractDateFromPattern();
$result = $extractor->extractDates($subject);
$this->assertCount($count, $result->getFound());
$this->assertEquals($filtered, $result->getFilteredSubject());
$this->assertContainsOnlyInstancesOf(\DateTimeImmutable::class, $result->getFound());
$dates = \array_map(
function (\DateTimeImmutable $d) {
return $d->format('Y-m-d');
}, $result->getFound()
);
foreach ($datesSearched as $date) {
$this->assertContains($date, $dates);
}
}
public function provideSubjects()
{
yield ["15/06/1981", "", 1, '1981-06-15'];
yield ["15/06/1981 30/12/1987", "", 2, '1981-06-15', '1987-12-30'];
yield ["diallo 15/06/1981", "diallo", 1, '1981-06-15'];
yield ["diallo 31/03/1981", "diallo", 1, '1981-03-31'];
yield ["diallo 15-06-1981", "diallo", 1, '1981-06-15'];
yield ["diallo 1981-12-08", "diallo", 1, '1981-12-08'];
yield ["diallo", "diallo", 0];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Search\Utils;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class ExtractPhonenumberFromPatternTest extends KernelTestCase
{
/**
* @dataProvider provideData
*/
public function testExtract($subject, $expectedCount, $expected, $filteredSubject, $msg)
{
$extractor = new ExtractPhonenumberFromPattern();
$result = $extractor->extractPhonenumber($subject);
$this->assertCount($expectedCount, $result->getFound());
$this->assertEquals($filteredSubject, $result->getFilteredSubject());
$this->assertEquals($expected, $result->getFound());
}
public function provideData()
{
yield ['Diallo', 0, [], 'Diallo', "no phonenumber"];
yield ['Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', "no phonenumber and a date"];
yield ['Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', "a phonenumber and a name"];
yield ['Diallo 123 456', 1, ['123456'], 'Diallo', "a number and a name, without leadiing 0"];
yield ['123 456', 1, ['123456'], '', "only phonenumber"];
yield ['0123 456', 1, ['+32123456'], '', "only phonenumber with a leading 0"];
}
}

View File

@@ -8,7 +8,16 @@ services:
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
Chill\MainBundle\Search\SearchApi: ~
Chill\MainBundle\Search\SearchApi:
autowire: true
autoconfigure: true
arguments:
$providers: !tagged_iterator chill.search_api_provider
Chill\MainBundle\Search\Entity\:
resource: '../../Search/Entity'
Chill\MainBundle\Search\Utils\:
autowire: true
autoconfigure: true
resource: './../Search/Utils/'

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20211119173554 extends AbstractMigration
{
public function getDescription(): string
{
return 'remove comment on deprecated json_array type';
}
public function up(Schema $schema): void
{
$columns = [
'users.attributes'
];
foreach ($columns as $col) {
$this->addSql("COMMENT ON COLUMN $col IS NULL");
}
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException();
}
}

View File

@@ -86,6 +86,10 @@ address more:
Create a new address: Créer une nouvelle adresse
Create an address: Créer une adresse
Update address: Modifier l'adresse
City or postal code: Ville ou code postal
# contact
Part of the phonenumber: Partie du numéro de téléphone
#serach
Your search is empty. Please provide search terms.: La recherche est vide. Merci de fournir des termes de recherche.
@@ -190,7 +194,7 @@ Location: Localisation
Location type list: Liste des types de localisation
Create a new location type: Créer un nouveau type de localisation
Available for users: Disponible aux utilisateurs
Address required: Adresse requise?
Address required: Adresse requise?
Contact data: Données de contact?
optional: optionnel
required: requis