Passed
Push — master ( e3afe0...6ec87a )
by George
03:01
created

Lexical::handleUnexpectedColumnCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 6
ccs 0
cts 5
cp 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
crap 2
1
<?php
2
namespace JsonTable\Analyse;
3
4
/**
5
 * Perform lexical analysis.
6
 * 
7
 * @package JsonTable
8
 */
9
class Lexical extends Analyse implements AnalyseInterface
10
{
11
    /**
12
     * @var string The description for fields with invalid formats.
13
     */
14
    const ERROR_INVALID_PATTERN = 'There are <strong>%d</strong> fields that don\'t have the correct pattern:';
15
16
    /**
17
     * @var string The description for fields with invalid formats.
18
     */
19
    const ERROR_INVALID_FORMAT = 'There are <strong>%d</strong> fields that don\'t have the correct format:';
20
21
    /**
22
     * @var string The description for rows with missing columns.
23
     */
24
    const ERROR_REQUIRED_FIELD_MISSING_DATA = 'There are <strong>%d</strong> required fields with missing data:';
25
26
    /**
27
     * @var string The description for rows with missing columns.
28
     */
29
    const ERROR_INCORRECT_COLUMN_COUNT = 'There are the wrong number of columns';
30
31
    /**
32
     * @var array   The current CSV row being analysed.
33
     */
34
    private $currentCsvRow;
35
36
    /**
37
     * @var int The position of the CSV column currently being analysed.
38
     */
39
    private $csvColumnPosition;
40
41
    /**
42
     * @var int The position of the current CSV row row in the CSV file.
43
     */
44
    private $rowNumber;
45
46
    /**
47
     * @var object  The schema definition for the column currently being analysed.
48
     */
49
    private $schemaColumn;
50
51
    /**
52
     * @var int The number of columns in the currently analysed row.
53
     */
54
    private $columnCount;
55
56
    /**
57
     * @var int The number of columns expected in each row.
58
     * This is taken from the CSV header row.
59
     */
60
    private $expectedColumnCount;
61
62
    /**
63
     * @var string  The pattern to validate the current field against.
64
     */
65
    private $pattern;
66
67
    /**
68
     * @var string  The format to validate the current field against.
69
     */
70
    private $format;
71
72
    /**
73
     * @var bool    Whether the file is valid.
74
     */
75
    private $valid;
76
77
78
    /**
79
     * Validate that all fields are of the correct type, format and pattern.
80
     * This also checks that each CSV row has the expected number of columns.
81
     *
82
     * @return  boolean Is all data lexically valid.
83
     */
84 28
    public function validate()
85
    {
86 28
        $this->valid = true;
87 28
        $this->rowNumber = 1;
88
89 28
        parent::rewindFilePointerToFirstData();
90
91 28
        while ($currentCsvRow = parent::loopThroughFileRows()) {
92 28
            $this->currentCsvRow = $currentCsvRow;
1 ignored issue
show
Documentation Bug introduced by
It seems like $currentCsvRow can also be of type boolean. However, the property $currentCsvRow is declared as type array. 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...
93
            
94 28
            if (!$this->checkRowHasExpectedColumnCount()) {
95
                $this->handleUnexpectedColumnCount();
96
            }
97
98 28
            for ($this->csvColumnPosition = 0; $this->csvColumnPosition < $this->columnCount; $this->csvColumnPosition++) {
99 28
                $this->schemaColumn = $this->getSchemaColumnFromCsvColumnPosition($this->csvColumnPosition);
100
101 28
                if (!$this->checkMandatoryColumnHasData()) {
102 2
                    $this->handleInvalidMandatoryColumn();
103
104 2
                    if ($this->stopIfInvalid) {
105
                        return false;
106
                    }
107 2
                }
108
109 28
                if (!$this->validateSpecificFormat()) {
110 8
                    $this->handleInvalidFormat();
111
112 8
                    if ($this->stopIfInvalid) {
113
                        return false;
114
                    }
115 8
                }
116
117 28
                if (!$this->validatePattern()) {
118
                    $this->handleInvalidPattern();
119
120
                    if ($this->stopIfInvalid) {
121
                        return false;
122
                    }
123
                }
124 28
            }
125
126 28
            $this->rowNumber++;
127 28
        }
128
129 28
        $this->setRowsAnalysedStatistic();
130
131 28
        return $this->valid;
132
    }
133
134
135
    /**
136
     * Check that the specified row has the expected number of columns.
137
     * The expected number of columns is the number of columns in the CSV header row.
138
     *
139
     * @return boolean  Whether the current row has the expected number of columns.
140
     */
141 28
    private function checkRowHasExpectedColumnCount()
142
    {
143 28
        $this->columnCount = count($this->currentCsvRow);
144 28
        $this->expectedColumnCount = count(parent::$headerColumns);
145
146 28
        return ($this->expectedColumnCount === $this->columnCount);
147
    }
148
149
150
    /**
151
     * Set an error and update the application as the current row has an unexpected number of columns.
152
     *
153
     * @return  void
154
     */
155
    private function handleUnexpectedColumnCount()
156
    {
157
        $errorMessage = "Row $this->rowNumber has $this->columnCount columns but should have $this->expectedColumnCount.";
158
        $this->error->setError(self::ERROR_INCORRECT_COLUMN_COUNT, $errorMessage);
159
        $this->statistics->setErrorRow($this->rowNumber);
160
    }
161
162
163
    /**
164
     * Check whether the current column is mandatory and if so, whether it has data in it.
165
     *
166
     * @return  boolean Whether the column has data in it.
167
     */
168 28
    private function checkMandatoryColumnHasData()
169
    {
170 28
        if ($this->isColumnMandatory($this->schemaColumn)) {
171 28
            return ('' !== $this->currentCsvRow[$this->csvColumnPosition]);
172
        }
173
174 25
        return true;
175
    }
176
177
178
    /**
179
     * Set an error and update the application as the current column is mandatory and has no data in it.
180
     *
181
     * @return  void
182
     */
183 2 View Code Duplication
    private function handleInvalidMandatoryColumn()
184
    {
185 2
        $errorMessage = $this->schemaColumn->name . " on row $this->rowNumber is missing.";
186 2
        $this->error->setError(self::ERROR_REQUIRED_FIELD_MISSING_DATA, $errorMessage);
187 2
        $this->statistics->setErrorRow($this->rowNumber);
188 2
        $this->valid = false;
189 2
    }
190
191
192
    /**
193
     * Check that the data in the current field is of a valid format as specified in the schema for this column.
194
     * This instantiates and passed the data to the format validator for this field type.
195
     *
196
     * @return  boolean Whether the current field is of a valid format.
197
     */
198 28
    private function validateSpecificFormat()
199
    {
200 28
        $type = $this->getColumnType($this->schemaColumn);
201 28
        $this->format = $this->getColumnFormat($this->schemaColumn);
202 28
        $validator = $this->instantiateValidator(Analyse::VALIDATION_TYPE_FORMAT, $type);
203 28
        $validator->setInput($this->currentCsvRow[$this->csvColumnPosition]);
204
205 28
        return $validator->validateFormat($this->format);
206
    }
207
208
209
    /**
210
     * Set an error and update the application as the current data didn't match the specified format.
211
     *
212
     * @return  void
213
     */
214 8 View Code Duplication
    private function handleInvalidFormat()
215
    {
216 8
        $errorMessage  = "The data in column " . $this->schemaColumn->name ." on row $this->rowNumber doesn't ";
217 8
        $errorMessage .= "match the required format of $this->format.";
218 8
        $this->error->setError(self::ERROR_INVALID_FORMAT, $errorMessage);
219 8
        $this->statistics->setErrorRow($this->rowNumber);
220 8
        $this->valid = false;
221 8
    }
222
223
224
    /**
225
     * Get the pattern of the specified column.
226
     *
227
     * @return  string  The pattern or null if no pattern is specified.
228
     */
229 28
    private function getColumnPattern()
230
    {
231 28
        $propertyExists = property_exists($this->schemaColumn, 'constraints') &&
232 28
            property_exists($this->schemaColumn->constraints, 'pattern');
233
234 28
        return $propertyExists ? $this->schemaColumn->constraints->pattern : null;
235
    }
236
237
238
    /**
239
     * Check that the input matches the specified pattern.
240
     *
241
     * @return  boolean Is the data valid.
242
     */
243 28
    private function validatePattern()
244
    {
245 28
        $this->pattern = $this->getColumnPattern();
246 28
        $input = $this->currentCsvRow[$this->csvColumnPosition];
247
248 28
        if (is_null($this->pattern) || '' === $input) {
249 28
            return true;
250
        }
251
252 2
        return (false !== filter_var($input, FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => $this->pattern]]));
253
    }
254
255
256
    /**
257
     * Set an error and update the application as the current data didn't match the specified pattern.
258
     *
259
     * @return  void
260
     */
261 View Code Duplication
    private function handleInvalidPattern()
262
    {
263
        $errorMessage  = "The data in column " . $this->schemaColumn->name . " on row $this->rowNumber doesn't ";
264
        $errorMessage .= "match the required pattern of $this->pattern.";
265
        $this->error->setError(self::ERROR_INVALID_PATTERN, $errorMessage);
266
        $this->statistics->setErrorRow($this->rowNumber);
267
        $this->valid = false;
268
    }
269
270
271
    /**
272
     * Add the number of rows analysed to the statistics.
273
     *
274
     * @return  void
275
     */
276 28
    private function setRowsAnalysedStatistic()
277
    {
278 28
        $this->statistics->setRowsAnalysed($this->rowNumber - 1);
279
    }
280
}