Passed
Push — master ( 492d83...a726fe )
by Alex
01:10
created

SimpleQueryProvider::getResourceSet()   C

Complexity

Conditions 12
Paths 104

Size

Total Lines 43
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 43
rs 5.0933
c 0
b 0
f 0
cc 12
eloc 31
nc 104
nop 6

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace POData\Providers\Query;
4
5
use POData\Providers\Expression\MySQLExpressionProvider;
6
use POData\Providers\Metadata\ResourceProperty;
7
use POData\Providers\Metadata\ResourceSet;
8
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
9
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
10
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
11
12
abstract class SimpleQueryProvider implements IQueryProvider
13
{
14
    /**
15
     * @var Connection
16
     */
17
    protected $db;
18
19
    public function __construct($db)
20
    {
21
        $this->db = $db;
22
    }
23
24
    /**
25
     * Query all data from DB.
26
     *
27
     * @param string     $sql        SQL query
28
     * @param array|null $parameters Parameters for SQL query
29
     *
30
     * @return array[]|null Array of associated arrays (column name => column value)
31
     */
32
    abstract protected function queryAll($sql, $parameters = null);
33
34
    /**
35
     * Query one value from DB.
36
     *
37
     * @param string     $sql        SQL query
38
     * @param array|null $parameters Parameters for SQL query
39
     *
40
     * @return mixed Value
41
     */
42
    abstract protected function queryScalar($sql, $parameters = null);
43
44
    /* Stubbed Implementaiton Here */
45
    public function getQueryProvider()
46
    {
47
        return new QueryProvider();
48
    }
49
50
    public function handlesOrderedPaging()
51
    {
52
        return true;
53
    }
54
55
    public function getExpressionProvider()
56
    {
57
        return new MySQLExpressionProvider();
58
    }
59
60
    /**
61
     * Get entity name by class name.
62
     *
63
     * @param string $entityClassName Class name
64
     *
65
     * @return string Entity name
66
     */
67
    protected function getEntityName($entityClassName)
68
    {
69
        preg_match_all('/\\\([a-zA-Z]+)/', $entityClassName, $matches);
70
        if (!empty($matches[1])) {
71
            return $matches[1][count($matches[1]) - 1];
72
        }
73
74
        return $entityClassName;
75
    }
76
77
    /**
78
     * Get table name by entity name.
79
     *
80
     * @param string $entityName Entity name
81
     *
82
     * @return string Table name
83
     */
84
    protected function getTableName($entityName)
85
    {
86
        $tableName = $entityName;
87
        preg_match_all('/[A-Z][a-z]+/', $entityName, $matches);
88
        if (!empty($matches[0])) {
89
            $tableName = implode('_', $matches[0]);
90
        }
91
92
        return strtolower($tableName);
93
    }
94
95
    /**
96
     * Get part of SQL query with ORDER BY condition.
97
     *
98
     * @param InternalOrderByInfo $orderBy Order by condition
99
     *
100
     * @return string ORDER BY condition
101
     */
102
    protected function getOrderByExpressionAsString(InternalOrderByInfo $orderBy)
103
    {
104
        $result = '';
105
        foreach ($orderBy->getOrderByInfo()->getOrderByPathSegments() as $order) {
106
            foreach ($order->getSubPathSegments() as $subOrder) {
107
                $result .= $result ? ', ' : '';
108
                $result .= $subOrder->getName();
109
                $result .= $order->isAscending() ? ' ASC' : ' DESC';
110
            }
111
        }
112
113
        return $result;
114
    }
115
116
    /**
117
     * Common method for getResourceFromRelatedResourceSet() and getResourceFromResourceSet().
118
     *
119
     * @param KeyDescriptor|null $keyDescriptor
120
     */
121
    protected function getResource(
122
        ResourceSet $resourceSet,
123
        $keyDescriptor,
124
        array $whereCondition = []
125
    ) {
126
        $where = '';
127
        $parameters = [];
128
        $index = 0;
129
        if ($keyDescriptor) {
130
            foreach ($keyDescriptor->getValidatedNamedValues() as $key => $value) {
131
                ++$index;
132
                //Keys have already been validated, so this is not a SQL injection surface
133
                $where .= $where ? ' AND ' : '';
134
                $where .= $key . ' = :param' . $index;
135
                $parameters[':param' . $index] = $value[0];
136
            }
137
        }
138
        foreach ($whereCondition as $fieldName => $fieldValue) {
139
            ++$index;
140
            $where .= $where ? ' AND ' : '';
141
            $where .= $fieldName . ' = :param' . $index;
142
            $parameters[':param' . $index] = $fieldValue;
143
        }
144
        $where = $where ? ' WHERE ' . $where : '';
145
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
146
        $entityName = $this->getEntityName($entityClassName);
147
        $sql = 'SELECT * FROM ' . $this->getTableName($entityName) . $where . ' LIMIT 1';
148
        $result = $this->queryAll($sql, $parameters);
149
        if ($result) {
150
            $result = $result[0];
151
        }
152
153
        return $entityClassName::fromRecord($result);
154
    }
155
156
    /**
157
     * For queries like http://localhost/NorthWind.svc/Customers.
158
     */
159
    public function getResourceSet(
160
        QueryType $queryType,
161
        ResourceSet $resourceSet,
162
        $filterInfo = null,
163
        $orderBy = null,
164
        $top = null,
165
        $skip = null
166
    ) {
167
        $result = new QueryResult();
168
        $entityClassName = $resourceSet->getResourceType()->getInstanceType()->name;
169
        $entityName = $this->getEntityName($entityClassName);
170
        $tableName = $this->getTableName($entityName);
171
        $option = null;
172
        if ($queryType == QueryType::ENTITIES_WITH_COUNT()) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
173
            //tell mysql we want to know the count prior to the LIMIT
174
            //$option = 'SQL_CALC_FOUND_ROWS';
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
175
        }
176
        $where = $filterInfo ? ' WHERE ' . $filterInfo->getExpressionAsString() : '';
177
        $order = $orderBy ? ' ORDER BY ' . $this->getOrderByExpressionAsString($orderBy) : '';
178
        $sqlCount = 'SELECT COUNT(*) FROM ' . $tableName . $where;
179
        if ($queryType == QueryType::ENTITIES() || $queryType == QueryType::ENTITIES_WITH_COUNT()) {
180
            $sql = 'SELECT ' . $option . ' * FROM ' . $tableName . $where . $order
181
                    .($top ? ' LIMIT ' . $top : '') . ($skip ? ' OFFSET ' . $skip : '');
182
            $data = $this->queryAll($sql);
183
184
            if ($queryType == QueryType::ENTITIES_WITH_COUNT()) {
185
                //get those found rows
186
                //$result->count = $this->queryScalar('SELECT FOUND_ROWS()');
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
187
                $result->count = $this->queryScalar($sqlCount);
188
            }
189
            $result->results = array_map($entityClassName . '::fromRecord', $data);
190
        } elseif ($queryType == QueryType::COUNT()) {
191
            $top = null !== $top ? intval($top) : $top;
192
            $skip = null !== $skip ? intval($skip) : $skip;
193
            $result->count = QueryResult::adjustCountForPaging(
0 ignored issues
show
Documentation Bug introduced by
It seems like \POData\Providers\Query\...sqlCount), $top, $skip) can also be of type double. However, the property $count is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
194
                $this->queryScalar($sqlCount),
195
                $top,
196
                $skip
197
            );
198
        }
199
200
        return $result;
201
    }
202
203
    /**
204
     * For queries like http://localhost/NorthWind.svc/Customers(‘ALFKI’).
205
     */
206
    public function getResourceFromResourceSet(
207
        ResourceSet $resourceSet,
208
        KeyDescriptor $keyDescriptor
209
    ) {
210
        return $this->getResource($resourceSet, $keyDescriptor);
211
    }
212
213
    /**
214
     * For queries like http://localhost/NorthWind.svc/Customers(‘ALFKI’)/Orders.
215
     */
216
    public function getRelatedResourceSet(
217
        QueryType $queryType,
218
        ResourceSet $sourceResourceSet,
219
        $sourceEntityInstance,
220
        ResourceSet $targetResourceSet,
221
        ResourceProperty $targetProperty,
222
        $filterInfo = null,
223
        $orderBy = null,
224
        $top = null,
225
        $skip = null
226
    ) {
227
        // Correct filter
228
        $srcClass = get_class($sourceEntityInstance);
229
        $filterFieldName = $this->getTableName($this->getEntityName($srcClass)) . '_id';
230
        $navigationPropertiesUsedInTheFilterClause = null;
231
        $filterExpAsDataSourceExp = '';
232
        if ($filterInfo) {
233
            $navigationPropertiesUsedInTheFilterClause = $filterInfo->getNavigationPropertiesUsed();
234
            $filterExpAsDataSourceExp = $filterInfo->getExpressionAsString();
235
        }
236
        $filterExpAsDataSourceExp .= $filterExpAsDataSourceExp ? ' AND ' : '';
237
        $filterExpAsDataSourceExp .= $filterFieldName . ' = ' . $sourceEntityInstance->id;
238
        $completeFilterInfo = new FilterInfo($navigationPropertiesUsedInTheFilterClause, $filterExpAsDataSourceExp);
0 ignored issues
show
Bug introduced by
It seems like $navigationPropertiesUsedInTheFilterClause defined by null on line 230 can also be of type null; however, POData\UriProcessor\Quer...lterInfo::__construct() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
239
240
        return $this->getResourceSet($queryType, $targetResourceSet, $completeFilterInfo, $orderBy, $top, $skip);
241
    }
242
243
    /**
244
     * For queries like http://localhost/NorthWind.svc/Customers(‘ALFKI’)/Orders(10643).
245
     */
246 View Code Duplication
    public function getResourceFromRelatedResourceSet(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247
        ResourceSet $sourceResourceSet,
248
        $sourceEntityInstance,
249
        ResourceSet $targetResourceSet,
250
        ResourceProperty $targetProperty,
251
        KeyDescriptor $keyDescriptor
252
    ) {
253
        $entityClassName = $sourceResourceSet->getResourceType()->getInstanceType()->name;
254
        $entityName = $this->getEntityName($entityClassName);
255
        $fieldName = $this->getTableName($entityName) . '_id';
256
257
        return $this->getResource($targetResourceSet, $keyDescriptor, [
258
            $fieldName => $sourceEntityInstance->id,
259
        ]);
260
    }
261
262
    /**
263
     * For queries like http://localhost/NorthWind.svc/Orders(10643)/Customer.
264
     */
265 View Code Duplication
    public function getRelatedResourceReference(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
266
        ResourceSet $sourceResourceSet,
267
        $sourceEntityInstance,
268
        ResourceSet $targetResourceSet,
269
        ResourceProperty $targetProperty
270
    ) {
271
        $entityClassName = $targetResourceSet->getResourceType()->getInstanceType()->name;
272
        $entityName = $this->getEntityName($entityClassName);
273
        $fieldName = $this->getTableName($entityName) . '_id';
274
275
        return $this->getResource($targetResourceSet, null, [
276
            'id' => $sourceEntityInstance->$fieldName,
277
        ]);
278
    }
279
}
280