SynonymMatcher::supports()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 2
1
<?php
2
3
namespace TreeHouse\Model\Config\Matcher;
4
5
use TreeHouse\Model\Config\Matcher\Normalizer\NormalizerInterface;
6
use TreeHouse\Model\Config\Matcher\Stemmer\StemmerInterface;
7
8
class SynonymMatcher implements MatcherInterface
9
{
10
    /**
11
     * @var NormalizerInterface
12
     */
13
    protected $normalizer;
14
15
    /**
16
     * @var StemmerInterface
17
     */
18
    protected $stemmer;
19
20
    /**
21
     * @var array
22
     */
23
    protected $synonyms;
24
25
    /**
26
     * @var array
27
     */
28
    protected $stopwords;
29
30
    /**
31
     * @param NormalizerInterface $normalizer
32
     * @param StemmerInterface    $stemmer
33
     * @param array               $synonyms
34
     * @param array               $stopwords
35
     */
36
    public function __construct(NormalizerInterface $normalizer, StemmerInterface $stemmer, array $synonyms, array $stopwords)
37
    {
38
        $this->normalizer = $normalizer;
39
        $this->stemmer    = $stemmer;
40
        $this->synonyms   = $synonyms;
41
        $this->stopwords  = $stopwords;
42
    }
43
44
    /**
45
     * @param string $field
46
     * @param string $value
47
     *
48
     * @return integer|null
49
     */
50
    public function match($field, $value)
51
    {
52
        $matches = $this->getMatches($field, $value);
53
54
        return empty($matches) ? null : $matches[min(array_keys($matches))];
55
    }
56
57
    /**
58
     * @param string $field
59
     * @param string $value
60
     *
61
     * @throws \OutOfBoundsException
62
     * @throws \InvalidArgumentException
63
     * @throws \BadMethodCallException
64
     *
65
     * @return array
66
     */
67
    public function getMatches($field, $value)
68
    {
69
        if (!$this->supports($field)) {
70
            throw new \BadMethodCallException(sprintf('Config field "%s" is not supported', $field));
71
        }
72
73
        // don't try to match null value
74
        if (is_null($value)) {
75
            return null;
76
        }
77
78
        // we need something we can cast to string
79
        if (!is_scalar($value)) {
80
            throw new \InvalidArgumentException(
81
                sprintf(
82
                    'Expecting scalar value for matching "%s", got "%s" instead',
83
                    $field,
84
                    gettype($value)
85
                )
86
            );
87
        }
88
89
        $synonyms  = $this->getSynonyms($field);
90
        $stopwords = $this->getStopwords($field);
91
92
        $value = (string) $value;
93
94
        if ($value === '') {
95
            return null;
96
        }
97
98
        // compare in lowercase
99
        $value = $this->normalizer->normalize($value);
100
101
        // check for synonyms
102
        $matches = [];
103
        foreach ($synonyms as $key => $keySynonyms) {
104
            foreach ($keySynonyms as $synonym) {
105
                $distance = $this->getDistance($value, $synonym);
106
                if ($distance === 0) {
107
                    // exact match, use this
108
                    return [0 => $key];
109
                }
110
111
                // it has to be somewhat similar
112
                if ($distance <= strlen($value) / 3) {
113
                    $matches[$distance] = $key;
114
                }
115
116
                // try for all stopwords
117
                foreach ($stopwords as $stopword) {
118
                    if (($stopword === $value) || false === strpos($value, $stopword)) {
119
                        // exact stopword match or no match
120
                        continue;
121
                    }
122
123
                    $distance = $this->getDistance(str_replace($stopword, '', $value), $synonym);
124
                    if ($distance === 0) {
125
                        return [0 => $key];
126
                    }
127
128
                    // it has to be somewhat similar
129
                    if ($distance <= strlen($value) / 3) {
130
                        $matches[$distance] = $key;
131
                    }
132
                }
133
            }
134
        }
135
136
        return $matches;
137
    }
138
139
    /**
140
     * @param string $field
141
     *
142
     * @return boolean
143
     */
144
    public function supports($field)
145
    {
146
        return array_key_exists($field, $this->synonyms);
147
    }
148
149
    /**
150
     * @param string $field
151
     *
152
     * @return array
153
     */
154
    public function getSynonyms($field)
155
    {
156
        return array_key_exists($field, $this->synonyms) ? $this->synonyms[$field] : [];
157
    }
158
159
    /**
160
     * @param string $field
161
     *
162
     * @return array
163
     */
164
    public function getStopwords($field)
165
    {
166
        return array_key_exists($field, $this->stopwords) ? $this->stopwords[$field] : [];
167
    }
168
169
    /**
170
     * @param string $value
171
     * @param string $synonym
172
     *
173
     * @return integer
174
     */
175
    protected function getDistance($value, $synonym)
176
    {
177
        // first try an exact match
178
        if ($value === $synonym) {
179
            return 0;
180
        }
181
182
        // try the slugified versions and stem them
183
        $synonymSlug = $this->stemmer->stem($value);
184
        $valueSlug   = $this->stemmer->stem($synonym);
185
186
        return levenshtein($synonymSlug, $valueSlug);
187
    }
188
}
189