Passed
Push — master ( fd902b...0cd8c1 )
by George
02:56
created

Analyse::validateMandatoryColumns()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.0113

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 19
ccs 12
cts 13
cp 0.9231
rs 8.8571
cc 5
eloc 10
nc 5
nop 0
crap 5.0113
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
     *                                          The default is false.
77
     *
78
     * @return  boolean true if the file passes the validation and false if not.
79
     */
80 30
    public function validate($stopIfInvalid = false)
81
    {
82 30
        $this->stopIfInvalid = (bool) $stopIfInvalid;
83
84 30
        self::$errors = [];
85 30
        $continueAnalysis = true;
86
87 30
        self::openFile();
88 30
        self::setCsvHeaderColumns();
89
90 30
        if (!$this->validateMandatoryColumns()) {
91 2
            $continueAnalysis = false;
92 2
        }
93
94 30
        if ($continueAnalysis && !$this->validateUnspecifiedColumns() && $this->stopIfInvalid) {
95
            $continueAnalysis = false;
96
        }
97
98 30
        $analyseLexical = new Lexical();
99
100 30
        if ($continueAnalysis && !$analyseLexical->validate() && $this->stopIfInvalid) {
101
            $continueAnalysis = false;
102
        }
103
104 30
        $analysePrimaryKey = new PrimaryKey();
105
        
106 30
        if ($continueAnalysis && !$analysePrimaryKey->validate() && $this->stopIfInvalid) {
107
            $continueAnalysis = false;
108
        }
109
110 30
        if ($continueAnalysis) {
111 28
            $analyseForeignKey = new ForeignKey();
112 28
            $analyseForeignKey->validate();
113 28
        }
114
115 30
        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 10
    public function getStatistics()
127
    {
128 10
        $this->cleanErrorRowStatistic();
129
130 10
        if (self::$statistics['rows_analysed'] > 0) {
131 10
            self::$statistics['percent_rows_with_errors'] = $this->getErrorRowPercent();
132 10
        }
133
134 10
        return self::$statistics;
135
    }
136
137
138
    /**
139
     * Validate that all mandatory columns are present.
140
     *
141
     * @access private
142
     *
143
     * @return boolean Are all mandatory columns present.
144
     */
145 30
    private function validateMandatoryColumns()
146
    {
147 30
        $validMandatoryColumns = true;
148
149 30
        foreach (self::$schemaJson->fields as $field) {
150 30
            if ($this->isColumnMandatory($field)) {
151 30
                if (!in_array($field->name, self::$headerColumns)) {
152 2
                    $this->setError(Analyse::ERROR_REQUIRED_COLUMN_MISSING, $field->name);
153 2
                    $validMandatoryColumns = false;
154
155 2
                    if ($this->stopIfInvalid) {
156
                        return false;
157
                    }
158 2
                }
159 30
            }
160 30
        }
161
162 30
        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
     * @return boolean Are all the CSV columns specified in the schema.
172
     */
173 28
    private function validateUnspecifiedColumns()
174
    {
175 28
        $validUnspecifiedColumns = true;
176
177 28
        foreach (self::$headerColumns as $csvColumnName) {
178 28
            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
                }
185
            }
186 28
        }
187
188 28
        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
     * @return  boolean Whether the column is mandatory.
200
     */
201 30
    protected function isColumnMandatory($schemaColumn)
202
    {
203 30
        $propertyExists = property_exists($schemaColumn, 'constraints') &&
204 30
                              property_exists($schemaColumn->constraints, 'required') &&
205 30
                              (true === $schemaColumn->constraints->required);
206 30
        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
     * @throws  \Exception if the validator class definition couldn't be found.
224
     */
225 28
    protected function instantiateValidator($validationType, $type)
226
    {
227
        // For format validation, "Date", "datetime" and "time" all follow the same schema definition rules
228
        // so just use the datetime format for them all.
229 28
        if (Analyse::VALIDATION_TYPE_FORMAT === $validationType && ('date' === $type || 'time' === $type)) {
230 2
            $type = 'datetime';
231 2
        }
232
233 28
        $typeClassName = ucwords($type) . 'Validator';
234 28
        $validatorFile = dirname(dirname(__FILE__)) . "/Validate/$validationType/$typeClassName.php";
235
236 28
        if (!file_exists($validatorFile) || !is_readable($validatorFile)) {
237
            throw new \Exception("Could not load the validator file for $validationType $type.");
238
        }
239
240 28
        include_once $validatorFile;
241
242 28
        $validatorClass = "\\JsonTable\\Validate\\$validationType\\$typeClassName";
243
244 28
        if (!class_exists($validatorClass)) {
245
            throw new \Exception("Could not find the validator class $validatorClass");
246
        }
247
248 28
        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
     * @return  boolean Is the file valid.
259
     */
260 30
    private function isFileValid()
261
    {
262 30
        return (0 === count(self::$errors));
263
    }
264
265
266
    /**
267
     * Return all errors.
268
     *
269
     * @access  public
270
     *
271
     * @return  array   The error messages.
272
     */
273 2
    public function getErrors()
274
    {
275 2
        $errorsFormatted = [];
276
277
        // Format the error type with the number of errors of that type.
278 2
        foreach (self::$errors as $errorType => $errors) {
279 1
            $errorTypeFormatted = sprintf($errorType, count($errors));
280 1
            $errorsFormatted[$errorTypeFormatted] = $errors;
281 2
        }
282
283 2
        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
     * @return  void
296
     */
297 11
    protected function setError($type, $error)
298
    {
299 11
        if (!array_key_exists($type, self::$errors)) {
300 11
            self::$errors[$type] = [];
301 11
        }
302
303 11
        array_push(self::$errors[$type], $error);
304 11
    }
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 9
    protected function setErrorRowStatistic($row_number)
317
    {
318 9
        self::$statistics['rows_with_errors'][] = $row_number;
319 9
    }
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 10
    private function cleanErrorRowStatistic()
331
    {
332 10
        self::$statistics['rows_with_errors'] = array_unique(self::$statistics['rows_with_errors']);
333 10
    }
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 10
    private function getErrorRowPercent()
344
    {
345 10
        return round((count(self::$statistics['rows_with_errors']) / self::$statistics['rows_analysed']) * 100);
346
    }
347
}
348