Completed
Push — master ( 12981b...2f1b03 )
by Robbie
14s
created

UserFormsUpgradeService::migrateRules()   D

Complexity

Conditions 9
Paths 14

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 9.0139

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 17
cts 18
cp 0.9444
rs 4.909
c 0
b 0
f 0
cc 9
eloc 17
nc 14
nop 2
crap 9.0139
1
<?php
2
3
/**
4
 * Service to support upgrade of userforms module
5
 */
6
class UserFormsUpgradeService
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
7
{
8
9
    /**
10
     * @var bool
11
     */
12
    protected $quiet;
13
14 2
    public function run()
15
    {
16 2
        $this->log("Upgrading formfield rules and custom settings");
17
18
        // List of rules that have been created in all stages
19 2
        $fields = Versioned::get_including_deleted('EditableFormField');
20 2
        foreach ($fields as $field) {
21 2
            $this->upgradeField($field);
22
        }
23 2
    }
24
25
    /**
26
     * Migrate a versioned field in all stages
27
     *
28
     * @param EditableFormField $field
29
     */
30 2
    protected function upgradeField(EditableFormField $field)
31
    {
32 2
        $this->log("Upgrading formfield ID = ".$field->ID);
33
34
        // Check versions this field exists on
35 2
        $filter = sprintf('"EditableFormField"."ID" = \'%d\' AND "Migrated" = 0', $field->ID);
36 2
        $stageField = Versioned::get_one_by_stage('EditableFormField', 'Stage', $filter);
37 2
        $liveField = Versioned::get_one_by_stage('EditableFormField', 'Live', $filter);
38
39 2
        if ($stageField) {
40 2
            $this->upgradeFieldInStage($stageField, 'Stage');
0 ignored issues
show
Compatibility introduced by
$stageField of type object<DataObject> is not a sub-type of object<EditableFormField>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Documentation introduced by
'Stage' is of type string, but the function expects a object<stage>.

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...
41
        }
42
43 2
        if ($liveField) {
44
            $this->upgradeFieldInStage($liveField, 'Live');
0 ignored issues
show
Compatibility introduced by
$liveField of type object<DataObject> is not a sub-type of object<EditableFormField>. It seems like you assume a child class of the class DataObject to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Documentation introduced by
'Live' is of type string, but the function expects a object<stage>.

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...
45
        }
46 2
    }
47
48
    /**
49
     * Migrate a versioned field in a single stage
50
     *
51
     * @param EditableFormField $field
52
     * @param stage $stage
53
     */
54 2
    protected function upgradeFieldInStage(EditableFormField $field, $stage)
55
    {
56 2
        Versioned::reading_stage($stage);
57
58
        // Migrate field rules
59 2
        $this->migrateRules($field, $stage);
60
61
        // Migrate custom settings
62 2
        $this->migrateCustomSettings($field, $stage);
63
64
        // Flag as migrated
65 2
        $field->Migrated = true;
0 ignored issues
show
Documentation introduced by
The property Migrated does not exist on object<EditableFormField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
66 2
        $field->write();
67 2
    }
68
69
    /**
70
     * Migrate custom rules for the given field
71
     *
72
     * @param EditableFormField $field
73
     * @param string $stage
74
     */
75 2
    protected function migrateRules(EditableFormField $field, $stage)
76
    {
77 2
        $rulesData = $field->CustomRules
0 ignored issues
show
Documentation introduced by
The property CustomRules does not exist on object<EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
78 2
            ? unserialize($field->CustomRules)
0 ignored issues
show
Documentation introduced by
The property CustomRules does not exist on object<EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
79 2
            : array();
80
81
        // Skip blank rules or fields with custom rules already
82 2
        if (empty($rulesData) || $field->DisplayRules()->count()) {
83 2
            return;
84
        }
85
86
        // Check value of this condition
87 2
        foreach ($rulesData as $ruleDataItem) {
88 2
            if (empty($ruleDataItem['ConditionOption']) || empty($ruleDataItem['Display'])) {
89
                continue;
90
            }
91
92
            // Get data for this rule
93 2
            $conditionOption = $ruleDataItem['ConditionOption'];
94 2
            $display = $ruleDataItem['Display'];
95 2
            $conditionFieldName = empty($ruleDataItem['ConditionField']) ? null : $ruleDataItem['ConditionField'];
96 2
            $value = isset($ruleDataItem['Value'])
97 2
                ? $ruleDataItem['Value']
98 2
                : null;
99
100
            // Create rule
101 2
            $rule = $this->findOrCreateRule($field, $stage, $conditionOption, $display, $conditionFieldName, $value);
102 2
            $this->log("Upgrading rule ID = " . $rule->ID);
103
        }
104 2
    }
105
106
107
108
    /**
109
     * Migrate custom settings for the given field
110
     *
111
     * @param EditableFormField $field
112
     * @param string $stage
113
     */
114 2
    protected function migrateCustomSettings(EditableFormField $field, $stage)
0 ignored issues
show
Unused Code introduced by
The parameter $stage is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
115
    {
116
        // Custom settings include:
117
        // - ExtraClass
118
        // - RightTitle
119
        // - ShowOnLoad (show or '' are treated as true)
120
        //
121
        // - CheckedDefault (new field on EditableCheckbox - should be read from old "default" value)
122
        // - Default (EditableCheckbox)
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% 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...
123
        // - DefaultToToday (EditableDateField)
124
        // - Folder (EditableFileField)
125
        // - Level (EditableFormHeading)
126
        // - HideFromReports (EditableFormHeading / EditableLiteralField)
127
        // - Content (EditableLiteralField)
128
        // - GroupID (EditableMemberListField)
129
        // - MinValue (EditableNumericField)
130
        // - MaxValue (EditableNumericField)
131
        // - MinLength (EditableTextField)
132
        // - MaxLength (EditableTextField)
133
        // - Rows (EditableTextField)
134
        // - Placeholder (EditableTextField / EditableEmailField / EditableNumericField)
135
136 2
        $customSettings = $field->CustomSettings
0 ignored issues
show
Documentation introduced by
The property CustomSettings does not exist on object<EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
137 2
            ? unserialize($field->CustomSettings)
0 ignored issues
show
Documentation introduced by
The property CustomSettings does not exist on object<EditableFormField>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
138 2
            : array();
139
140
        // Skip blank rules or fields with custom rules already
141 2
        if (empty($customSettings)) {
142 2
            return;
143
        }
144
145 2
        $field->migrateSettings($customSettings);
146
147 2
        if ($field->config()->has_placeholder) {
148 2
            $this->migratePlaceholder($field, $field->ClassName);
149
        }
150
151 2
        $field->write();
152 2
    }
153
154
    /**
155
     * Create or find an existing field with the matched specification
156
     *
157
     * @param EditableFormField $field
158
     * @param string $stage
159
     * @param string $conditionOption
160
     * @param string $display
161
     * @param string $conditionFieldName
162
     * @param string $value
163
     * @return EditableCustomRule
164
     */
165 2
    protected function findOrCreateRule(EditableFormField $field, $stage, $conditionOption, $display, $conditionFieldName, $value)
166
    {
167
        // Get id of field
168 2
        $conditionField = $conditionFieldName
169 2
            ? EditableFormField::get()->filter('Name', $conditionFieldName)->first()
170 2
            : null;
171
172
        // If live, search stage record for matching one
173 2
        if ($stage === 'Live') {
174
            $list = Versioned::get_by_stage('EditableCustomRule', 'Stage')
175
                ->filter(array(
176
                    'ParentID' => $field->ID,
177
                    'ConditionFieldID' => $conditionField ? $conditionField->ID : 0,
178
                    'Display' => $display,
179
                    'ConditionOption' => $conditionOption
180
                ));
181
            if ($value) {
182
                $list = $list->filter('FieldValue', $value);
183
            } else {
184
                $list = $list->where('"FieldValue" IS NULL OR "FieldValue" = \'\'');
185
            }
186
            $rule = $list->first();
187
            if ($rule) {
188
                $rule->write();
189
                $rule->publish("Stage", "Live");
190
                return $rule;
191
            }
192
        }
193
194
        // If none found, or in stage, create new record
195 2
        $rule = new EditableCustomRule();
196 2
        $rule->ParentID = $field->ID;
0 ignored issues
show
Documentation introduced by
The property ParentID does not exist on object<EditableCustomRule>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
197 2
        $rule->ConditionFieldID = $conditionField ? $conditionField->ID : 0;
0 ignored issues
show
Documentation introduced by
The property ConditionFieldID does not exist on object<EditableCustomRule>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
198 2
        $rule->Display = $display;
199 2
        $rule->ConditionOption = $conditionOption;
200 2
        $rule->FieldValue = $value;
201 2
        $rule->write();
202 2
        return $rule;
203
    }
204
205 2
    public function log($message)
206
    {
207 2
        if ($this->getQuiet()) {
208 2
            return;
209
        }
210
        if (Director::is_cli()) {
211
            echo "{$message}\n";
212
        } else {
213
            echo "{$message}<br />";
214
        }
215
    }
216
217
    /**
218
     * Set if this service should be quiet
219
     *
220
     * @param bool $quiet
221
     * @return $ths
0 ignored issues
show
Documentation introduced by
The doc-type $ths could not be parsed: Unknown type name "$ths" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
222
     */
223 2
    public function setQuiet($quiet)
224
    {
225 2
        $this->quiet = $quiet;
226 2
        return $this;
227
    }
228
229 2
    public function getQuiet()
230
    {
231 2
        return $this->quiet;
232
    }
233
234
235
    /**
236
     * Migrate Placeholder data from field specific table to the EditableFormField table
237
     *
238
     * @param EditableFormField $field
239
     * @param string $tableName
240
     */
241 2
    private function migratePlaceholder($field, $tableName)
242
    {
243
        // Migrate Placeholder setting from $tableName table to EditableFormField table
244 2
        if ($field->Placeholder) {
0 ignored issues
show
Bug introduced by
The property Placeholder does not seem to exist. Did you mean has_placeholder?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
245
            return;
246
        }
247
        // Check if draft table exists
248 2
        if (!DB::get_schema()->hasTable($tableName)) {
249
            // Check if _obsolete_ draft table exists
250
            $tableName = '_obsolete_' . $tableName;
251
            if (!DB::get_schema()->hasTable($tableName)) {
252
                return;
253
            }
254
        }
255
        // Check if old Placeholder column exists
256 2
        if (!DB::get_schema()->hasField($tableName, 'Placeholder')) {
257 2
            return;
258
        }
259
        // Fetch existing draft Placeholder value
260
        $query = "SELECT \"Placeholder\" FROM \"$tableName\" WHERE \"ID\" = '$field->ID'";
261
        $draftPlaceholder = DB::query($query)->value();
262
263
        if (!$draftPlaceholder) {
264
            return;
265
        }
266
        // Update draft Placeholder value
267
        DB::prepared_query(
268
            "UPDATE \"EditableFormField\" SET \"Placeholder\" = ? WHERE \"ID\" = ?",
269
            array($draftPlaceholder, $field->ID)
270
        );
271
272
        $livePlaceholder = $draftPlaceholder;
0 ignored issues
show
Unused Code introduced by
$livePlaceholder is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
273
274
        // Check if live table exists
275
        $tableName = $tableName . '_Live';
276
        if (DB::get_schema()->hasTable($tableName)) {
277
            // Fetch existing live Placeholder value
278
            $query = "SELECT \"Placeholder\" FROM \"$tableName\" WHERE \"ID\" = '" . $field->ID . "'";
279
            $livePlaceholder = DB::query($query)->value();
280
            if (!$livePlaceholder) {
281
                $livePlaceholder = $draftPlaceholder;
0 ignored issues
show
Unused Code introduced by
$livePlaceholder is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
282
            }
283
        }
284
285
        // Update live Placeholder value
286
        DB::prepared_query(
287
            "UPDATE \"EditableFormField_Live\" SET \"Placeholder\" = ? WHERE \"ID\" = ?",
288
            array($draftPlaceholder, $field->ID)
289
        );
290
    }
291
}
292