Test Failed
Push — master ( 087751...3d501f )
by George
03:59
created

Analyse::validateUnspecifiedColumns()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 17
rs 9.2
cc 4
eloc 9
nc 4
nop 0
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 $statistics = ['rows_with_errors' => []];
56
57
    /**
58
     * @access  protected
59
     * @static
60
     *
61
     * @var array   Error messages.
62
     */
63
    protected static $errors = [];
64
65
66
    /**
67
     * Analyse the specified file against the loaded schema.
68
     *
69
     * @access  public
70
     *
71
     * @param   boolean $stopIfInvalid Should the analysis stop when the file is found to be invalid.
72
     *                                          The default is false.
73
     *
74
     * @return  boolean true if the file passes the validation and false if not.
75
     */
76
    public function validate($stopIfInvalid = false)
77
    {
78
        $this->stopIfInvalid = (bool) $stopIfInvalid;
79
80
        self::$errors = [];
81
        $continueAnalysis = true;
82
83
        self::openFile();
84
        self::setCsvHeaderColumns();
85
86
        if (!$this->validateMandatoryColumns()) {
87
            $continueAnalysis = false;
88
        }
89
90
        if ($continueAnalysis && !$this->validateUnspecifiedColumns() && $this->stopIfInvalid) {
91
            $continueAnalysis = false;
92
        }
93
94
        $analyseLexical = new Lexical();
95
96
        if ($continueAnalysis && !$analyseLexical->validate() && $this->stopIfInvalid) {
97
            $continueAnalysis = false;
98
        }
99
100
        $analysePrimaryKey = new PrimaryKey();
101
        
102
        if ($continueAnalysis && !$analysePrimaryKey->validate() && $this->stopIfInvalid) {
103
            $continueAnalysis = false;
104
        }
105
106
        if ($continueAnalysis) {
107
            $analyseForeignKey = new ForeignKey();
108
            $analyseForeignKey->validate();
109
        }
110
111
        return $this->isFileValid();
112
    }
113
114
115
    /**
116
     * Get the statistics about the file analysis.
117
     *
118
     * @access  public
119
     *
120
     * @return  array   The statistics.
121
     */
122
    public function getStatistics()
123
    {
124
        $this->statistics['rows_with_errors'] = array_unique($this->statistics['rows_with_errors']);
125
        $this->statistics['percent_rows_with_errors'] = 0;
126
127
        if ($this->statistics['rows_analysed'] > 0) {
128
            $this->statistics['percent_rows_with_errors'] =
129
                (count($this->statistics['rows_with_errors']) / $this->statistics['rows_analysed']) * 100;
130
        }
131
132
        return $this->statistics;
133
    }
134
135
136
    /**
137
     * Validate that all mandatory columns are present.
138
     *
139
     * @access private
140
     *
141
     * @return boolean Are all mandatory columns present.
142
     */
143
    private function validateMandatoryColumns()
144
    {
145
        $validMandatoryColumns = true;
146
147
        foreach (self::$schemaJson->fields as $field) {
148
            if ($this->isColumnMandatory($field)) {
149
                if (!in_array($field->name, self::$headerColumns)) {
150
                    $this->setError(Analyse::ERROR_REQUIRED_COLUMN_MISSING, $field->name);
151
                    $validMandatoryColumns = false;
152
153
                    if ($this->stopIfInvalid) {
154
                        return false;
155
                    }
156
                }
157
            }
158
        }
159
160
        return $validMandatoryColumns;
161
    }
162
163
164
    /**
165
     * Check that there are no columns in the CSV that are not specified in the schema.
166
     *
167
     * @access private
168
     *
169
     * @return boolean Are all the CSV columns specified in the schema.
170
     */
171
    private function validateUnspecifiedColumns()
172
    {
173
        $validUnspecifiedColumns = true;
174
175
        foreach (self::$headerColumns as $csvColumnName) {
176
            if (false === $this->getSchemaKeyFromName($csvColumnName)) {
177
                $this->setError(Analyse::ERROR_UNSPECIFIED_COLUMN, $csvColumnName);
178
                $validUnspecifiedColumns = false;
179
180
                if ($this->stopIfInvalid) {
181
                    return false;
182
                }
183
            }
184
        }
185
186
        return $validUnspecifiedColumns;
187
    }
188
189
190
    /**
191
     * Check if the specified column is mandatory.
192
     *
193
     * @access  protected
194
     *
195
     * @param   object  $schemaColumn    The schema column object to examine.
196
     *
197
     * @return  boolean Whether the column is mandatory.
198
     */
199
    protected function isColumnMandatory($schemaColumn)
200
    {
201
        $propertyExists = property_exists($schemaColumn, 'constraints') &&
202
                              property_exists($schemaColumn->constraints, 'required') &&
203
                              (true === $schemaColumn->constraints->required);
204
        return $propertyExists;
205
    }
206
207
208
    /**
209
     * Load and instantiate the specified validator.
210
     *
211
     * @access protected
212
     *
213
     * @param string $validationType The type of validator to load.
214
     * @param string $type The type being validated.
215
     *                            For formats this will be the field type.
216
     *                            For foreign keys this will be the datapackage type
217
     *
218
     * @return object The validation object. Throws an exception on error.
219
     *
220
     * @throws  \Exception if the validator file couldn't be loaded.
221
     * @throws  \Exception if the validator class definition couldn't be found.
222
     */
223
    protected function instantiateValidator($validationType, $type)
224
    {
225
        // For format validation, "Date", "datetime" and "time" all follow the same schema definition rules
226
        // so just use the datetime format for them all.
227
        if (Analyse::VALIDATION_TYPE_FORMAT === $validationType && ('date' === $type || 'time' === $type)) {
228
            $type = 'datetime';
229
        }
230
231
        $typeClassName = ucwords($type) . 'Validator';
232
        $validatorFile = dirname(__FILE__) . "/Validate/$validationType/$typeClassName.php";
233
234
        if (!file_exists($validatorFile) || !is_readable($validatorFile)) {
235
            throw new \Exception("Could not load the validator file for $validationType $type.");
236
        }
237
238
        include_once $validatorFile;
239
240
        $validatorClass = "\\JsonTable\\Validate\\$validationType\\$typeClassName";
241
242
        if (!class_exists($validatorClass)) {
243
            throw new \Exception("Could not find the validator class $validatorClass");
244
        }
245
246
        return new $validatorClass($type);
247
    }
248
249
250
    /**
251
     * Check if the file was found to be valid.
252
     * This checks for any validation errors.
253
     *
254
     * @access  private
255
     *
256
     * @return  boolean Is the file valid.
257
     */
258
    private function isFileValid()
259
    {
260
        return (0 === count(self::$errors));
261
    }
262
263
264
    /**
265
     * Return all errors.
266
     *
267
     * @access  public
268
     *
269
     * @return  array   The error messages.
270
     */
271
    public function getErrors()
272
    {
273
        $errorsFormatted = [];
274
275
        // Format the error type with the number of errors of that type.
276
        foreach (self::$errors as $errorType => $errors) {
277
            $errorTypeFormatted = sprintf($errorType, count($errors));
278
            $errorsFormatted[$errorTypeFormatted] = $errors;
279
        }
280
281
        return $errorsFormatted;
282
    }
283
284
285
    /**
286
     * Add an error message.
287
     *
288
     * @access  protected
289
     *
290
     * @param   string  $type   The type of error.
291
     * @param   string  $error  The error message (or field).
292
     *
293
     * @return  void
294
     */
295
    protected function setError($type, $error)
296
    {
297
        if (!array_key_exists($type, self::$errors)) {
298
            self::$errors[$type] = [];
299
        }
300
301
        array_push(self::$errors[$type], $error);
302
    }
303
}
304