Completed
Push — master ( ece4ce...71b299 )
by Fabien
04:42
created

MatcherObjectFactory::parseQuery()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 29
rs 6.7272
cc 7
eloc 16
nc 6
nop 3
1
<?php
2
namespace Fab\Vidi\Persistence;
3
4
/**
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
 * It is free software; you can redistribute it and/or modify it under
8
 * the terms of the GNU General Public License, either version 2
9
 * of the License, or any later version.
10
 *
11
 * For the full copyright and license information, please read the
12
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
 * The TYPO3 project - inspiring people to share!
15
 */
16
17
use Fab\Vidi\Module\ModuleName;
18
use Fab\Vidi\Resolver\FieldPathResolver;
19
use TYPO3\CMS\Core\SingletonInterface;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use TYPO3\CMS\Core\Utility\MathUtility;
22
use Fab\Vidi\Module\ModuleLoader;
23
use Fab\Vidi\Tca\Tca;
24
use TYPO3\CMS\Extbase\Object\ObjectManager;
25
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
26
27
/**
28
 * Factory class related to Matcher object.
29
 */
30
class MatcherObjectFactory implements SingletonInterface
31
{
32
33
    /**
34
     * Gets a singleton instance of this class.
35
     *
36
     * @return $this
37
     */
38
    static public function getInstance()
0 ignored issues
show
Coding Style introduced by
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
39
    {
40
        return GeneralUtility::makeInstance(self::class);
41
    }
42
43
    /**
44
     * Returns a matcher object.
45
     *
46
     * @param array $matches
47
     * @param string $dataType
48
     * @return Matcher
49
     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
50
     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
51
     * @throws \Fab\Vidi\Exception\InvalidKeyInArrayException
52
     * @throws \InvalidArgumentException
53
     */
54
    public function getMatcher(array $matches = array(), $dataType = '')
55
    {
56
57
        if (empty($dataType)) {
58
            $dataType = $this->getModuleLoader()->getDataType();
59
        }
60
61
        /** @var $matcher Matcher */
62
        $matcher = GeneralUtility::makeInstance(Matcher::class, array(), $dataType);
63
64
        $matcher = $this->applyCriteriaFromDataTables($matcher, $dataType);
65
        $matcher = $this->applyCriteriaFromMatchesArgument($matcher, $matches);
66
67
        if ($this->isBackendMode()) {
68
            $matcher = $this->applyCriteriaFromUrl($matcher);
69
            $matcher = $this->applyCriteriaFromTSConfig($matcher);
70
        }
71
72
        // Trigger signal for post processing Matcher Object.
73
        $this->emitPostProcessMatcherObjectSignal($matcher);
74
75
        return $matcher;
76
    }
77
78
    /**
79
     * Get a possible id from the URL and apply as filter criteria.
80
     * Except if the main module belongs to the File. The id would be a combined identifier
81
     * including the storage and a mount point.
82
     *
83
     * @param Matcher $matcher
84
     * @return Matcher $matcher
85
     * @throws \Fab\Vidi\Exception\InvalidKeyInArrayException
86
     */
87
    protected function applyCriteriaFromUrl(Matcher $matcher)
88
    {
89
90
        if (GeneralUtility::_GP('id') && $this->getModuleLoader()->getMainModule() !== ModuleName::FILE) {
91
            $matcher->equals('pid', GeneralUtility::_GP('id'));
92
        }
93
94
        return $matcher;
95
    }
96
97
    /**
98
     * @param Matcher $matcher
99
     * @return Matcher $matcher
100
     * @throws \InvalidArgumentException
101
     * @throws \Fab\Vidi\Exception\InvalidKeyInArrayException
102
     */
103
    protected function applyCriteriaFromTSConfig(Matcher $matcher)
104
    {
105
        $dataType = $this->getModuleLoader()->getDataType();
106
        $tsConfigPath = sprintf('tx_vidi.dataType.%s.constraints', $dataType);
107
        $tsConfig = $this->getBackendUser()->getTSConfig($tsConfigPath);
108
109
        if (is_array($tsConfig['properties']) && !empty($tsConfig['properties'])) {
110
111
            foreach ($tsConfig['properties'] as $constraint) {
112
113
                if (preg_match('/(.+) (>=|>|<|<=|=|like) (.+)/is', $constraint, $matches) && count($matches) === 4) {
114
115
                    $operator = $matcher->getSupportedOperators()[strtolower(trim($matches[2]))];
116
                    $operand = trim($matches[1]);
117
                    $value = trim($matches[3]);
118
119
                    $matcher->$operator($operand, $value);
120
                } elseif (preg_match('/(.+) (in) (.+)/is', $constraint, $matches) && count($matches) === 4) {
121
122
                    $operator = $matcher->getSupportedOperators()[trim($matches[2])];
123
                    $operand = trim($matches[1]);
124
                    $value = trim($matches[3]);
125
                    $matcher->$operator($operand, GeneralUtility::trimExplode(',', $value, true));
126
                }
127
            }
128
        }
129
130
        return $matcher;
131
    }
132
133
    /**
134
     * @param Matcher $matcher
135
     * @param array $matches
136
     * @return Matcher $matcher
137
     */
138
    protected function applyCriteriaFromMatchesArgument(Matcher $matcher, $matches)
139
    {
140
        foreach ($matches as $fieldNameAndPath => $value) {
141
            // CSV values should be considered as "in" operator in the query, otherwise "equals".
142
            $explodedValues = GeneralUtility::trimExplode(',', $value, true);
143
            if (count($explodedValues) > 1) {
144
                $matcher->in($fieldNameAndPath, $explodedValues);
145
            } else {
146
                $matcher->equals($fieldNameAndPath, $explodedValues[0]);
147
            }
148
        }
149
150
        return $matcher;
151
    }
152
153
    /**
154
     * Apply criteria specific to jQuery plugin DataTable.
155
     *
156
     * @param Matcher $matcher
157
     * @param string $dataType
158
     * @return Matcher $matcher
159
     * @throws \Exception
160
     */
161
    protected function applyCriteriaFromDataTables(Matcher $matcher, $dataType)
162
    {
163
164
        // Special case for Grid in the BE using jQuery DataTables plugin.
165
        // Retrieve a possible search term from GP.
166
        $query = GeneralUtility::_GP('search');
167
        if (is_array($query)) {
168
            if (!empty($query['value'])) {
169
                $query = $query['value'];
170
            } else {
171
                $query = '';
172
            }
173
        }
174
175
        if (strlen($query) > 0) {
176
177
            // Parse the json query coming from the Visual Search.
178
            $query = rawurldecode($query);
179
            $queryParts = json_decode($query, true);
180
181
            if (is_array($queryParts)) {
182
                $matcher = $this->parseQuery($queryParts, $matcher, $dataType);
183
            } else {
184
                $matcher->setSearchTerm($query);
185
            }
186
        }
187
        return $matcher;
188
    }
189
190
    /**
191
     * @param array $queryParts
192
     * @param Matcher $matcher
193
     * @param string $dataType
194
     * @return Matcher $matcher
195
     * @throws \InvalidArgumentException
196
     */
197
    protected function parseQuery(array $queryParts, Matcher $matcher, $dataType)
198
    {
199
        foreach ($queryParts as $term) {
200
            $fieldNameAndPath = key($term);
201
202
            $resolvedDataType = $this->getFieldPathResolver()->getDataType($fieldNameAndPath, $dataType);
203
            $fieldName = $this->getFieldPathResolver()->stripFieldPath($fieldNameAndPath, $dataType);
204
205
            // Retrieve the value.
206
            $value = current($term);
207
208
            if (Tca::grid($resolvedDataType)->hasFacet($fieldName) && Tca::grid($resolvedDataType)->facet($fieldName)->canModifyMatcher()) {
209
                $matcher = Tca::grid($resolvedDataType)->facet($fieldName)->modifyMatcher($matcher, $value);
210
            } elseif (Tca::table($resolvedDataType)->hasField($fieldName)) {
211
                // Check whether the field exists and set it as "equal" or "like".
212
                if ($this->isOperatorEquals($fieldNameAndPath, $dataType, $value)) {
213
                    $matcher->equals($fieldNameAndPath, $value);
214
                } else {
215
                    $matcher->like($fieldNameAndPath, $value);
216
                }
217
            } elseif ($fieldNameAndPath === 'text') {
218
                // Special case if field is "text" which is a pseudo field in this case.
219
                // Set the search term which means Vidi will
220
                // search in various fields with operator "like". The fields come from key "searchFields" in the TCA.
221
                $matcher->setSearchTerm($value);
222
            }
223
        }
224
        return $matcher;
225
    }
226
227
    /**
228
     * Tell whether the operator should be equals instead of like for a search, e.g. if the value is numerical.
229
     *
230
     * @param string $fieldName
231
     * @param string $dataType
232
     * @param string $value
233
     * @return bool
234
     * @throws \Exception
235
     */
236
    protected function isOperatorEquals($fieldName, $dataType, $value)
237
    {
238
        return (Tca::table($dataType)->field($fieldName)->hasRelation() && MathUtility::canBeInterpretedAsInteger($value))
239
        || Tca::table($dataType)->field($fieldName)->isNumerical();
240
    }
241
242
    /**
243
     * Signal that is called for post-processing a matcher object.
244
     *
245
     * @param Matcher $matcher
246
     * @signal
247
     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException
248
     * @throws \TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException
249
     * @throws \Fab\Vidi\Exception\InvalidKeyInArrayException
250
     */
251
    protected function emitPostProcessMatcherObjectSignal(Matcher $matcher)
252
    {
253
254
        if (strlen($matcher->getDataType()) <= 0) {
255
256
            /** @var ModuleLoader $moduleLoader */
257
            $moduleLoader = $this->getObjectManager()->get(ModuleLoader::class);
258
            $matcher->setDataType($moduleLoader->getDataType());
259
        }
260
261
        $this->getSignalSlotDispatcher()->dispatch('Fab\Vidi\Controller\Backend\ContentController', 'postProcessMatcherObject', array($matcher, $matcher->getDataType()));
262
    }
263
264
    /**
265
     * Get the SignalSlot dispatcher
266
     *
267
     * @return Dispatcher
268
     */
269
    protected function getSignalSlotDispatcher()
270
    {
271
        return $this->getObjectManager()->get(Dispatcher::class);
272
    }
273
274
    /**
275
     * @return ObjectManager
276
     * @throws \InvalidArgumentException
277
     */
278
    protected function getObjectManager()
279
    {
280
        return GeneralUtility::makeInstance(ObjectManager::class);
281
    }
282
283
    /**
284
     * Get the Vidi Module Loader.
285
     *
286
     * @return ModuleLoader
287
     * @throws \InvalidArgumentException
288
     */
289
    protected function getModuleLoader()
290
    {
291
        return GeneralUtility::makeInstance(ModuleLoader::class);
292
    }
293
294
    /**
295
     * @return FieldPathResolver
296
     * @throws \InvalidArgumentException
297
     */
298
    protected function getFieldPathResolver()
299
    {
300
        return GeneralUtility::makeInstance(FieldPathResolver::class);
301
    }
302
303
    /**
304
     * Returns an instance of the current Backend User.
305
     *
306
     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
307
     */
308
    protected function getBackendUser()
0 ignored issues
show
Coding Style introduced by
getBackendUser uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
309
    {
310
        return $GLOBALS['BE_USER'];
311
    }
312
313
    /**
314
     * Returns whether the current mode is Backend
315
     *
316
     * @return bool
317
     */
318
    protected function isBackendMode()
319
    {
320
        return TYPO3_MODE === 'BE';
321
    }
322
}
323