Test Failed
Push — master ( 503637...fd902b )
by George
02:50
created

Analyse::getErrorRowPercent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
namespace JsonTable\Analyse;
3
4
use \JsonTable\Base;
5
6
/**
7
 * Analyse data to ensure it validates against a JSON table schema.
8
 *
9
 * @package    JSON table
10
 */
11
class Analyse extends Base implements AnalyseInterface
12
{
13
    /**
14
     * @var string The description for missing mandatory columns.
15
     */
16
    const ERROR_REQUIRED_COLUMN_MISSING = '<strong>%d</strong> required column(s) missing:';
17
18
    /**
19
     * @var string The description for CSV columns that are not in the schema.
20
     */
21
    const ERROR_UNSPECIFIED_COLUMN = '<strong>%d</strong> unexpected column(s):';
22
23
    /**
24
     * @var string The description for rows with missing columns.
25
     */
26
    const ERROR_INCORRECT_COLUMN_COUNT = 'There are the wrong number of columns';
27
28
    /**
29
     * @var string The description for rows with missing columns.
30
     */
31
    const ERROR_REQUIRED_FIELD_MISSING_DATA = 'There are <strong>%d</strong> required fields with missing data:';
32
33
    /**
34
     * @var string The format validation type.
35
     */
36
    const VALIDATION_TYPE_FORMAT = 'Format';
37
38
    /**
39
     * @var string The foreign key validation type.
40
     */
41
    const VALIDATION_TYPE_FOREIGN_KEY = 'ForeignKey';
42
43
    /**
44
     * @access  protected
45
     *
46
     * @var boolean Should the analysis stop when an error is found.
47
     */
48
    protected $stopIfInvalid;
49
50
    /**
51
     * @access  protected
52
     *
53
     * @var array   Statistics relating to the file analysis.
54
     */
55
    protected static $statistics = [
56
        'rows_with_errors' => [],
57
        'percent_rows_with_errors' => 0,
58
        'rows_analysed' => 0
59
    ];
60
61
    /**
62
     * @access  protected
63
     * @static
64
     *
65
     * @var array   Error messages.
66
     */
67
    protected static $errors = [];
68
    
69
70
    /**
71
     * Analyse the specified file against the loaded schema.
72
     *
73
     * @access  public
74
     *
75
     * @param   boolean $stopIfInvalid Should the analysis stop when the file is found to be invalid.
76 3
     *                                          The default is false.
77
     *
78 3
     * @return  boolean true if the file passes the validation and false if not.
79
     */
80 3
    public function validate($stopIfInvalid = false)
81 3
    {
82
        $this->stopIfInvalid = (bool) $stopIfInvalid;
83 3
84 3
        self::$errors = [];
85
        $continueAnalysis = true;
86 3
87 2
        self::openFile();
88 2
        self::setCsvHeaderColumns();
89
90 3
        if (!$this->validateMandatoryColumns()) {
91
            $continueAnalysis = false;
92
        }
93
94 3
        if ($continueAnalysis && !$this->validateUnspecifiedColumns() && $this->stopIfInvalid) {
95
            $continueAnalysis = false;
96 3
        }
97
98
        $analyseLexical = new Lexical();
99
100 3
        if ($continueAnalysis && !$analyseLexical->validate() && $this->stopIfInvalid) {
101
            $continueAnalysis = false;
102 3
        }
103
104
        $analysePrimaryKey = new PrimaryKey();
105
        
106 3
        if ($continueAnalysis && !$analysePrimaryKey->validate() && $this->stopIfInvalid) {
107 1
            $continueAnalysis = false;
108 1
        }
109 1
110
        if ($continueAnalysis) {
111 3
            $analyseForeignKey = new ForeignKey();
112
            $analyseForeignKey->validate();
113
        }
114
115
        return $this->isFileValid();
116
    }
117
118
119
    /**
120
     * Get the statistics about the file analysis.
121
     *
122
     * @access  public
123
     *
124
     * @return  array   The statistics.
125
     */
126
    public function getStatistics()
127
    {
128
        $this->cleanErrorRowStatistic();
129
130
        if (self::$statistics['rows_analysed'] > 0) {
131
            self::$statistics['percent_rows_with_errors'] = $this->getErrorRowPercent();
132
        }
133
134
        return self::$statistics;
135
    }
136
137
138
    /**
139
     * Validate that all mandatory columns are present.
140
     *
141
     * @access private
142
     *
143 3
     * @return boolean Are all mandatory columns present.
144
     */
145 3
    private function validateMandatoryColumns()
146
    {
147 3
        $validMandatoryColumns = true;
148 3
149 3
        foreach (self::$schemaJson->fields as $field) {
150 2
            if ($this->isColumnMandatory($field)) {
151 2
                if (!in_array($field->name, self::$headerColumns)) {
152
                    $this->setError(Analyse::ERROR_REQUIRED_COLUMN_MISSING, $field->name);
153 2
                    $validMandatoryColumns = false;
154
155
                    if ($this->stopIfInvalid) {
156 2
                        return false;
157 3
                    }
158 3
                }
159
            }
160 3
        }
161
162
        return $validMandatoryColumns;
163
    }
164
165
166
    /**
167
     * Check that there are no columns in the CSV that are not specified in the schema.
168
     *
169
     * @access private
170
     *
171 1
     * @return boolean Are all the CSV columns specified in the schema.
172
     */
173 1
    private function validateUnspecifiedColumns()
174
    {
175 1
        $validUnspecifiedColumns = true;
176 1
177
        foreach (self::$headerColumns as $csvColumnName) {
178
            if (false === $this->getSchemaKeyFromName($csvColumnName)) {
179
                $this->setError(Analyse::ERROR_UNSPECIFIED_COLUMN, $csvColumnName);
180
                $validUnspecifiedColumns = false;
181
182
                if ($this->stopIfInvalid) {
183
                    return false;
184 1
                }
185
            }
186 1
        }
187
188
        return $validUnspecifiedColumns;
189
    }
190
191
192
    /**
193
     * Check if the specified column is mandatory.
194
     *
195
     * @access  protected
196
     *
197
     * @param   object  $schemaColumn    The schema column object to examine.
198
     *
199 3
     * @return  boolean Whether the column is mandatory.
200
     */
201 3
    protected function isColumnMandatory($schemaColumn)
202 3
    {
203 3
        $propertyExists = property_exists($schemaColumn, 'constraints') &&
204 3
                              property_exists($schemaColumn->constraints, 'required') &&
205
                              (true === $schemaColumn->constraints->required);
206
        return $propertyExists;
207
    }
208
209
210
    /**
211
     * Load and instantiate the specified validator.
212
     *
213
     * @access protected
214
     *
215
     * @param string $validationType The type of validator to load.
216
     * @param string $type The type being validated.
217
     *                            For formats this will be the field type.
218
     *                            For foreign keys this will be the datapackage type
219
     *
220
     * @return object The validation object. Throws an exception on error.
221
     *
222
     * @throws  \Exception if the validator file couldn't be loaded.
223 1
     * @throws  \Exception if the validator class definition couldn't be found.
224
     */
225
    protected function instantiateValidator($validationType, $type)
226
    {
227 1
        // For format validation, "Date", "datetime" and "time" all follow the same schema definition rules
228 1
        // so just use the datetime format for them all.
229 1
        if (Analyse::VALIDATION_TYPE_FORMAT === $validationType && ('date' === $type || 'time' === $type)) {
230
            $type = 'datetime';
231 1
        }
232 1
233
        $typeClassName = ucwords($type) . 'Validator';
234 1
        $validatorFile = dirname(dirname(__FILE__)) . "/Validate/$validationType/$typeClassName.php";
235
236
        if (!file_exists($validatorFile) || !is_readable($validatorFile)) {
237
            throw new \Exception("Could not load the validator file for $validationType $type.");
238 1
        }
239
240 1
        include_once $validatorFile;
241
242 1
        $validatorClass = "\\JsonTable\\Validate\\$validationType\\$typeClassName";
243
244
        if (!class_exists($validatorClass)) {
245
            throw new \Exception("Could not find the validator class $validatorClass");
246 1
        }
247
248
        return new $validatorClass($type);
249
    }
250
251
252
    /**
253
     * Check if the file was found to be valid.
254
     * This checks for any validation errors.
255
     *
256
     * @access  private
257
     *
258 3
     * @return  boolean Is the file valid.
259
     */
260 3
    private function isFileValid()
261
    {
262
        return (0 === count(self::$errors));
263
    }
264
265
266
    /**
267
     * Return all errors.
268
     *
269
     * @access  public
270
     *
271 2
     * @return  array   The error messages.
272
     */
273 2
    public function getErrors()
274
    {
275
        $errorsFormatted = [];
276 2
277 1
        // Format the error type with the number of errors of that type.
278 1
        foreach (self::$errors as $errorType => $errors) {
279 2
            $errorTypeFormatted = sprintf($errorType, count($errors));
280
            $errorsFormatted[$errorTypeFormatted] = $errors;
281 2
        }
282
283
        return $errorsFormatted;
284
    }
285
286
287
    /**
288
     * Add an error message.
289
     *
290
     * @access  protected
291
     *
292
     * @param   string  $type   The type of error.
293
     * @param   string  $error  The error message (or field).
294
     *
295 2
     * @return  void
296
     */
297 2
    protected function setError($type, $error)
298 2
    {
299 2
        if (!array_key_exists($type, self::$errors)) {
300
            self::$errors[$type] = [];
301 2
        }
302 2
303
        array_push(self::$errors[$type], $error);
304
    }
305
306
307
    /**
308
     * Add the row number of a row with an error to the analysis statistics.
309
     *
310
     * @access  protected
311
     *
312
     * @param   int $row_number   The position of the row with the error in the CSV file.
313
     *
314
     * @return  void
315
     */
316
    protected function setErrorRowStatistic($row_number)
317
    {
318
        self::$statistics['rows_with_errors'][] = $row_number;
319
    }
320
321
322
    /**
323
     * Clean the rows with errors statistic.
324
     * This removes duplicated records where the same row has had multiple errors.
325
     *
326
     * @access  private
327
     *
328
     * @return  void
329
     */
330
    private function cleanErrorRowStatistic()
331
    {
332
        self::$statistics['rows_with_errors'] = array_unique(self::$statistics['rows_with_errors']);
333
    }
334
335
336
    /**
337
     * Get the percentage of analysed rows that have had a error with them.
338
     *
339
     * @access  private
340
     *
341
     * @return  int The percentage.
342
     */
343
    private function getErrorRowPercent()
344
    {
345
        return round((count(self::$statistics['rows_with_errors']) / self::$statistics['rows_analysed']) * 100);
346
    }
347
}
348