Completed
Pull Request — master (#66)
by
unknown
02:59
created

VirtualAnalyzer::getParentEntity()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 12
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
//[PHPCOMPRESSOR(remove,start)]
3
/**
4
 * Created by Vitaly Iegorov <[email protected]>.
5
 * on 23.03.16 at 11:45
6
 */
7
namespace samsoncms\api\generator\analyzer;
8
9
use samson\activerecord\dbMySQLConnector;
10
use samsoncms\api\Field;
11
use samsoncms\api\generator\exception\ParentEntityNotFound;
12
use samsoncms\api\generator\metadata\GenericMetadata;
13
use samsoncms\api\generator\metadata\VirtualMetadata;
14
use samsoncms\api\Navigation;
15
16
/**
17
 * Generic entities metadata analyzer.
18
 *
19
 * @package samsoncms\api\analyzer
20
 */
21
class VirtualAnalyzer extends GenericAnalyzer
22
{
23
    /** @var string Metadata class */
24
    protected $metadataClass = \samsoncms\api\generator\metadata\VirtualMetadata::class;
25
26
    /**
27
     * Analyze virtual entities and gather their metadata.
28
     *
29
     * @return \samsoncms\api\generator\metadata\VirtualMetadata[]
30
     * @throws ParentEntityNotFound
31
     */
32
    public function analyze()
33
    {
34
        /** @var RealMetadata[] $metadataCollection Set pointer to global metadata collection */
35
        $metadataCollection = [];
36
37
        // Iterate all structures, parents first
38
        foreach ($this->getVirtualEntities() as $structureRow) {
39
            /** @var VirtualMetadata $metadata Fill in entity metadata */
40
            $metadata = new $this->metadataClass;
41
42
            $this->analyzeEntityRecord($metadata, $structureRow);
43
44
            // TODO: Add multiple parent and fetching their data in a loop
45
46
            // Set pointer to parent entity
47
            if (null !== $metadata->parentID && (int)$structureRow[Navigation::F_TYPE] === \samsoncms\api\generator\metadata\VirtualMetadata::TYPE_STRUCTURE) {
48
                if (array_key_exists($metadata->parentID, $metadataCollection)) {
49
                    $metadata->parent = $metadataCollection[$metadata->parentID];
0 ignored issues
show
Documentation Bug introduced by
It seems like $metadataCollection[$metadata->parentID] can also be of type object<samsoncms\api\gen...\analyzer\RealMetadata>. However, the property $parent is declared as type object<samsoncms\api\gen...tadata\VirtualMetadata>. 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...
50
                    // Add all parent metadata to current object
51
                    $metadata->defaultValues = $metadata->parent->defaultValues;
52
                    $metadata->realNames = $metadata->parent->realNames;
53
                    $metadata->fields = $metadata->parent->fields;
54
                    $metadata->fieldNames = $metadata->parent->fieldNames;
55
                    $metadata->types = $metadata->parent->types;
56
                    $metadata->allFieldValueColumns = $metadata->parent->allFieldValueColumns;
57
                    $metadata->allFieldCmsTypes = $metadata->parent->allFieldCmsTypes;
58
                    $metadata->fieldDescriptions = $metadata->parent->fieldDescriptions;
59
                    $metadata->localizedFieldIDs = $metadata->parent->localizedFieldIDs;
60
                    $metadata->notLocalizedFieldIDs = $metadata->parent->notLocalizedFieldIDs;
61
                } else {
62
                    throw new ParentEntityNotFound($metadata->parentID);
63
                }
64
            }
65
66
            // Get old AR collections of metadata
67
            $metadata->arSelect = \samson\activerecord\material::$_sql_select;
68
            $metadata->arAttributes = \samson\activerecord\material::$_attributes;
69
            $metadata->arMap = \samson\activerecord\material::$_map;
70
            $metadata->arFrom = \samson\activerecord\material::$_sql_from;
71
            $metadata->arGroup = \samson\activerecord\material::$_own_group;
72
            $metadata->arRelationAlias = \samson\activerecord\material::$_relation_alias;
73
            $metadata->arRelationType = \samson\activerecord\material::$_relation_type;
74
            $metadata->arRelations = \samson\activerecord\material::$_relations;
75
76
            // Add SamsonCMS material needed data
77
            $metadata->arSelect['this'] = ' STRAIGHT_JOIN ' . $metadata->arSelect['this'];
78
            $metadata->arFrom['this'] .= "\n" .
79
                'LEFT JOIN ' . dbMySQLConnector::$prefix . 'materialfield as _mf
80
            ON ' . dbMySQLConnector::$prefix . 'material.MaterialID = _mf.MaterialID';
81
            $metadata->arGroup[] = dbMySQLConnector::$prefix . 'material.MaterialID';
82
83
            // Add material table real fields
84
85
86
            // Iterate entity fields
87
            foreach ($this->getEntityFields($structureRow[Navigation::F_PRIMARY]) as $fieldID => $fieldRow) {
88
                $this->analyzeFieldRecord($metadata, $fieldID, $fieldRow);
89
90
                // Get camelCase and transliterated field name
91
                $fieldName = $this->fieldName($fieldRow[Field::F_IDENTIFIER]);
92
93
                // Fill localization fields collections
94
                if ($fieldRow[Field::F_LOCALIZED] == 1) {
95
                    $metadata->localizedFieldIDs[$fieldID] = $fieldName;
96
                } else {
97
                    $metadata->notLocalizedFieldIDs[$fieldID] = $fieldName;
98
                }
99
100
                // Set old AR collections of metadata
101
                $metadata->arAttributes[$fieldName] = $fieldName;
102
                $metadata->arMap[$fieldName] = dbMySQLConnector::$prefix . 'material.' . $fieldName;
103
104
                // Add additional field column to entity query
105
                $equal = '((_mf.FieldID = ' . $fieldID . ')&&(_mf.locale ' . ($fieldRow['local'] ? ' = "@locale"' : 'IS NULL') . '))';
106
                $metadata->arSelect['this'] .= "\n\t\t" . ',MAX(IF(' . $equal . ', _mf.`' . Field::valueColumn($fieldRow['Type']) . '`, NULL)) as `' . $fieldName . '`';
107
            }
108
109
            // Store metadata by entity identifier
110
            $metadataCollection[(int)$structureRow[Navigation::F_PRIMARY]] = $metadata;
111
            // Store virtual metadata
112
            GenericMetadata::$instances[(int)$structureRow[Navigation::F_PRIMARY]] = $metadata;
113
        }
114
115
        return $metadataCollection;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $metadataCollection; (array<samsoncms\api\gene...tadata\VirtualMetadata>) is incompatible with the return type of the parent method samsoncms\api\generator\...enericAnalyzer::analyze of type samsoncms\api\generator\...\GenericMetadata[]|null.

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...
116
    }
117
118
    /**
119
     * Get virtual entities from database by their type.
120
     *
121
     * @param int $type Virtual entity type
122
     *
123
     * @return array Get collection of navigation objects
124
     */
125
    protected function getVirtualEntities($type = 0)
126
    {
127
        $navigations = $this->database->fetch('
128
        SELECT * FROM `structure`
129
        WHERE `Active` = "1" AND `Type` = "' . $type . '"
130
        ORDER BY `ParentID` ASC
131
        ');
132
133
        // Store navigation elements by identifiers
134
        $navigationsIDs = [];
135
        foreach ($navigations as $navigation) {
136
            $navigationsIDs[$navigation['StructureID']] = $navigation;
137
        }
138
139
        $tree = [];
140
        foreach ($navigationsIDs as $navigationID => $navigation) {
141
            $arrayDefinition = [];
142
            $parentPointer = $navigation;
143
            do {
144
                $arrayDefinition[] = '['.$parentPointer['StructureID'].']';
145
                $parentPointer = array_key_exists($parentPointer['ParentID'], $navigationsIDs) ? $navigationsIDs[$parentPointer['ParentID']] : null;
146
            } while (null !== $parentPointer);
147
148
            // Create multi-dimensional array
149
            eval('$tree'.implode('', array_reverse($arrayDefinition)).'[$navigationID] = $navigationID;');
150
        }
151
152
153
        // Walk recursive array and using array keys fill flat array in correct order to preserve parent/child relations
154
        $output = [];
155
        array_walk_recursive($tree, function($value, $key) use (&$output, &$navigationsIDs) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
156
            $output[$key] = $navigationsIDs[$key];
157
        });
158
159
        //trace($output, 1);die;
160
        //trace(array_reverse($output), 1);
161
        return array_reverse($output);
162
    }
163
164
    /**
165
     * Analyze entity.
166
     *
167
     * @param \samsoncms\api\generator\metadata\VirtualMetadata $metadata
168
     * @param array                                             $structureRow Entity database row
169
     */
170
    public function analyzeEntityRecord(&$metadata, array $structureRow)
171
    {
172
        $metadata->structureRow = $structureRow;
173
174
        // Get CapsCase and transliterated entity name
175
        $metadata->entity = $this->entityName($structureRow[Navigation::F_NAME]);
176
        $metadata->entityClassName = $this->fullEntityName($metadata->entity);
177
        $metadata->entityRealName = $structureRow[Navigation::F_NAME];
178
        $metadata->entityID = $structureRow[Navigation::F_PRIMARY];
179
        $metadata->type = (int)$structureRow[Navigation::F_TYPE];
180
181
        // Try to find entity parent identifier for building future relations
182
        $metadata->parentID = $this->getParentEntity($structureRow[Navigation::F_PRIMARY]);
183
    }
184
185
    /**
186
     * Find entity parent identifier.
187
     *
188
     * @param int $entityID Entity identifier
189
     *
190
     * @return null|int Parent entity identifier
191
     */
192
    public function getParentEntity($entityID)
193
    {
194
        $parentData = $this->database->fetch('
195
SELECT *
196
FROM structure_relation as sm
197
JOIN structure as s ON s.StructureID = sm.parent_id
198
WHERE sm.child_id = "' . $entityID . '"
199
AND s.StructureID != "' . $entityID . '"
200
');
201
        // Get parent entity identifier
202
        return count($parentData) ? $parentData[0]['StructureID'] : null;
203
    }
204
205
    /**
206
     * Get entity fields.
207
     *
208
     * @param int $entityID Entity identifier
209
     *
210
     * @return array Collection of entity fields
211
     */
212
    protected function getEntityFields($entityID)
213
    {
214
        $return = array();
215
        // TODO: Optimize queries make one single query with only needed data
216
        foreach ($this->database->fetch('SELECT * FROM `structurefield` WHERE `StructureID` = "' . $entityID . '" AND `Active` = "1"') as $fieldStructureRow) {
217
            foreach ($this->database->fetch('SELECT * FROM `field` WHERE `FieldID` = "' . $fieldStructureRow['FieldID'] . '"') as $fieldRow) {
218
                $return[$fieldRow['FieldID']] = $fieldRow;
219
            }
220
        }
221
222
        return $return;
223
    }
224
225
    /**
226
     * Virtual entity additional field analyzer.
227
     *
228
     * @param \samsoncms\api\generator\metadata\VirtualMetadata $metadata Metadata instance for filling
229
     * @param int                                               $fieldID  Additional field identifier
230
     * @param array                                             $fieldRow Additional field database row
231
     */
232
    public function analyzeFieldRecord(&$metadata, $fieldID, array $fieldRow)
233
    {
234
        // Get camelCase and transliterated field name
235
        $fieldName = $this->fieldName($fieldRow[Field::F_IDENTIFIER]);
236
237
        // TODO: Set default for additional field storing type accordingly.
238
239
        // Store field metadata
240
        $metadata->realNames[$fieldRow[Field::F_IDENTIFIER]] = $fieldName;
241
        $metadata->fields[$fieldID] = $fieldName;
242
        $metadata->fieldNames[$fieldName] = $fieldID;
243
        $metadata->allFieldValueColumns[$fieldID] = Field::valueColumn($fieldRow[Field::F_TYPE]);
244
        $metadata->types[$fieldID] = Field::phpType($fieldRow[Field::F_TYPE]);
245
        $metadata->allFieldCmsTypes[$fieldID] = (int)$fieldRow[Field::F_TYPE];
246
        $metadata->fieldDescriptions[$fieldID] = $fieldRow[Field::F_DESCRIPTION] . ', ' . $fieldRow[Field::F_IDENTIFIER] . '#' . $fieldID;
247
        $metadata->fieldRawDescriptions[$fieldID] = $fieldRow[Field::F_DESCRIPTION];
248
    }
249
250
    /**
251
     * Get child entities by parent identifier.
252
     *
253
     * @param int $parentId Parent entity identifier
254
     *
255
     * @return array Get collection of child navigation objects
256
     */
257
    protected function getChildEntities($parentId)
258
    {
259
        return $this->database->fetch('
260
        SELECT * FROM `structure`
261
        WHERE `Active` = "1" AND `ParentID` = ' . $parentId . '
262
        ORDER BY `ParentID` ASC
263
        ');
264
    }
265
}
266
//[PHPCOMPRESSOR(remove,end)]