Completed
Push — master ( 89d22a...101fe8 )
by Vitaly
03:34
created

EntityQuery::findByAdditionalFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 20
rs 9.4286
cc 3
eloc 6
nc 3
nop 2
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: VITALYIEGOROV
5
 * Date: 11.12.15
6
 * Time: 17:35
7
 */
8
namespace samsoncms\api\query;
9
10
use samson\activerecord\dbQuery;
11
use samsoncms\api\Field;
12
use samsonframework\orm\ArgumentInterface;
13
use samsonframework\orm\Condition;
14
use samsoncms\api\Material;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, samsoncms\api\query\Material.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
15
use samsonframework\orm\QueryInterface;
16
17
/**
18
 * Generic SamsonCMS Entity query.
19
 * @package samsoncms\api\query
20
 */
21
class EntityQuery extends Generic
22
{
23
    /** @var array Collection of all additional fields names */
24
    protected static $fieldNames = array();
25
26
    /** @var array Collection of localized additional fields identifiers */
27
    protected static $localizedFieldIDs = array();
28
29
    /** @var array Collection of NOT localized additional fields identifiers */
30
    protected static $notLocalizedFieldIDs = array();
31
32
    /** @var array Collection of all additional fields identifiers */
33
    protected static $fieldIDs = array();
34
35
    /** @var  @var array Collection of additional fields value column names */
36
    protected static $fieldValueColumns = array();
37
38
    /** @var array Collection of entity field filter */
39
    protected $fieldFilter = array();
40
41
    /** @var string Query locale */
42
    protected $locale = '';
43
44
    /**
45
     * Select specified entity fields.
46
     * If this method is called then only selected entity fields
47
     * would be return in entity instances.
48
     *
49
     * @param mixed $fieldNames Entity field name or collection of names
50
     * @return self Chaining
51
     */
52
    public function select($fieldNames)
53
    {
54
        // Convert argument to array and iterate
55
        foreach ((!is_array($fieldNames) ? array($fieldNames) : $fieldNames) as $fieldName) {
56
            // Try to find entity additional field
57
            $pointer = &static::$fieldNames[$fieldName];
58
            if (isset($pointer)) {
59
                // Store selected additional field buy FieldID and Field name
60
                $this->selectedFields[$pointer] = $fieldName;
61
            }
62
        }
63
64
        return $this;
65
    }
66
67
    /**
68
     * Add condition to current query.
69
     *
70
     * @param string $fieldName Entity field name
71
     * @param string $fieldValue Value
72
     * @return self Chaining
73
     */
74
    public function where($fieldName, $fieldValue = null, $fieldRelation = ArgumentInterface::EQUAL)
75
    {
76
        // Try to find entity additional field
77
        $pointer = &static::$fieldNames[$fieldName];
78
        if (isset($pointer)) {
79
            // Store additional field filter value
80
            $this->fieldFilter[$pointer] = $fieldValue;
81
        } else {
82
            parent::where($fieldName, $fieldValue, $fieldRelation);
83
        }
84
85
        return $this;
86
    }
87
88
    /**
89
     * Get collection of entity identifiers filtered by navigation identifiers.
90
     *
91
     * @param array $entityIDs Additional collection of entity identifiers for filtering
92
     * @return array Collection of material identifiers by navigation identifiers
93
     */
94
    protected function findByNavigationIDs($entityIDs = array())
95
    {
96
        return (new MaterialNavigation($entityIDs))->idsByRelationID(static::$navigationIDs);
97
    }
98
99
    /**
100
     * Get collection of entity identifiers filtered by additional field and its value.
101
     *
102
     * @param array $additionalFields Collection of additional field identifiers => values
103
     * @param array $entityIDs Additional collection of entity identifiers for filtering
104
     * @return array Collection of material identifiers by navigation identifiers
105
     */
106
    protected function findByAdditionalFields($additionalFields, $entityIDs = array())
107
    {
108
        /**
109
         * TODO: We have separate request to materialfield for each field, maybe faster to
110
         * make one single query with all fields conditions. Performance tests are needed.
111
         */
112
113
        // Iterate all additional fields needed for filter entity
114
        foreach ($additionalFields as $fieldID => $fieldValue) {
115
            // Get collection of entity identifiers passing already found identifiers
116
            $entityIDs = (new MaterialField($entityIDs))->idsByRelationID($fieldID, $fieldValue);
117
118
            // Stop execution if we have no entities found at this step
119
            if (!sizeof($entityIDs)) {
120
                break;
121
            }
122
        }
123
124
        return $entityIDs;
125
    }
126
127
    /**
128
     * Get entities additional field values.
129
     *
130
     * @param array $entityIDs Collection of entity identifiers
131
     * @return array Collection of entities additional fields EntityID => [Additional field name => Value]
132
     */
133
    protected function findAdditionalFields($entityIDs)
134
    {
135
        $return = array();
136
137
        // Copy fields arrays
138
        $localized = static::$localizedFieldIDs;
139
        $notLocalized = static::$notLocalizedFieldIDs;
140
141
        // If we filter additional fields that we need to receive
142
        if (sizeof($this->selectedFields)) {
143
            foreach ($this->selectedFields as $fieldID => $fieldName) {
144
                // Filter localized and not fields by selected fields
145
                if (!isset(static::$localizedFieldIDs[$fieldID])) {
146
                    unset($localized[$fieldID]);
147
                }
148
149
                if (!isset(static::$notLocalizedFieldIDs[$fieldID])) {
150
                    unset($notLocalized[$fieldID]);
151
                }
152
            }
153
        }
154
155
        // Prepare localized additional field query condition
156
        $condition = new Condition(Condition::DISJUNCTION);
157
        foreach ($localized as $fieldID => $fieldName) {
158
            $condition->addCondition(
159
                (new Condition())
160
                    ->add(Field::F_PRIMARY, $fieldID)
161
                    ->add(Field::F_LOCALIZED, $this->locale)
162
            );
163
        }
164
165
        // Prepare not localized fields condition
166
        foreach ($notLocalized as $fieldID => $fieldName) {
167
            $condition->add(Field::F_PRIMARY, $fieldID);
168
        }
169
170
        // Get additional fields values for current entity identifiers
171
        foreach ((new dbQuery())->entity(\samsoncms\api\MaterialField::ENTITY)
0 ignored issues
show
Coding Style introduced by
Space found before closing bracket of FOREACH loop
Loading history...
172
                     ->where(Material::F_PRIMARY, $entityIDs)
0 ignored issues
show
Documentation introduced by
$entityIDs is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
173
                     ->whereCondition($condition)
174
                     ->where(Material::F_DELETION, true)
0 ignored issues
show
Documentation introduced by
true is of type boolean, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
175
                     ->exec() as $additionalField
176
        ) {
177
            // Get needed metadata
178
            $fieldID = $additionalField[Field::F_PRIMARY];
179
            $materialID = $additionalField[Material::F_PRIMARY];
180
            $valueField = static::$fieldValueColumns[$fieldID];
181
            $fieldName = static::$fieldIDs[$fieldID];
182
            $fieldValue = $additionalField[$valueField];
183
184
            // Gather additional fields values by entity identifiers and field name
185
            $return[$materialID][$fieldName] = $fieldValue;
186
        }
187
188
        return $return;
189
    }
190
191
    /**
192
     * Perform SamsonCMS query and get entities collection.
193
     *
194
     * @return mixed[] Collection of found entities
195
     */
196
    public function find()
197
    {
198
        //elapsed('Start SamsonCMS '.static::$identifier.' query');
199
        // TODO: Find and describe approach with maximum generic performance
200
        $entityIDs = $this->findByNavigationIDs();
201
        //elapsed('End navigation filter');
202
        $entityIDs = $this->findByAdditionalFields($this->fieldFilter, $entityIDs);
0 ignored issues
show
Documentation introduced by
$entityIDs is of type boolean|object<samsonfra...rk\orm\RecordInterface>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
203
        //elapsed('End fields filter');
204
205
        $return = array();
206
        if (sizeof($entityIDs)) {
207
            $additionalFields = $this->findAdditionalFields($entityIDs);
208
            //elapsed('End fields values');
209
            /** @var \samsoncms\api\Entity $item Find entity instances */
210
            foreach ($this->query->entity(static::$identifier)->where(Material::F_PRIMARY, $entityIDs)->exec() as $item) {
0 ignored issues
show
Bug introduced by
The expression $this->query->entity(sta...RY, $entityIDs)->exec() of type boolean|object<samsonfra...rk\orm\RecordInterface> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
211
                // If we have list of additional fields that we need
212
                $fieldIDs = sizeof($this->selectedFields) ? $this->selectedFields : static::$fieldIDs;
213
214
                // Iterate all entity additional fields
215
                foreach ($fieldIDs as $variable) {
216
                    // Set only existing additional fields
217
                    $pointer = &$additionalFields[$item[Material::F_PRIMARY]][$variable];
218
                    if (isset($pointer)) {
219
                        $item->$variable = $pointer;
220
                    }
221
                }
222
                // Store entity by identifier
223
                $return[$item[Material::F_PRIMARY]] = $item;
224
            }
225
        }
226
227
        //elapsed('Finish SamsonCMS '.static::$identifier.' query');
228
229
        return $return;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $return; (array) is incompatible with the return type of the parent method samsoncms\api\query\Generic::find of type boolean|samsonframework\orm\RecordInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
230
    }
231
232
    /**
233
     * Generic constructor.
234
     *
235
     * @param QueryInterface $query Database query instance
236
     * @param string $locale Query localizaation
237
     */
238
    public function __construct(QueryInterface $query, $locale = '')
239
    {
240
        $this->locale = $locale;
241
242
        parent::__construct($query);
243
    }
244
}
245