Completed
Push — master ( be4243...1976df )
by Ori
19:42
created

src/Table.php (1 issue)

Labels

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace frictionlessdata\tableschema;
4
5
use frictionlessdata\tableschema\DataSources\CsvDataSource;
6
use frictionlessdata\tableschema\Exceptions\DataSourceException;
7
8
/**
9
 * represents a data source which validates against a table schema
10
 * provides interfaces for validating the data and iterating over it
11
 * casts all values to their native values according to the table schema.
12
 */
13
class Table implements \Iterator
14
{
15
    /**
16
     * @param DataSources\DataSourceInterface $dataSource
17
     * @param Schema                          $schema
18
     *
19
     * @throws Exceptions\DataSourceException
20
     */
21
    public function __construct($dataSource, $schema = null)
22
    {
23
        if (!is_a($dataSource, 'frictionlessdata\\tableschema\\DataSources\\BaseDataSource')) {
24
            // TODO: more advanced data source detection
25
            $dataSource = new CsvDataSource($dataSource);
26
        }
27
        $this->dataSource = $dataSource;
28
        if (!is_a($schema, 'frictionlessdata\\tableschema\\Schema')) {
29
            if ($schema) {
30
                $schema = new Schema($schema);
31
            } else {
32
                $schema = new InferSchema();
33
            }
34
        }
35
        $this->schema = $schema;
36
        $this->dataSource->open();
37
        $this->uniqueFieldValues = [];
38
    }
39
40
    /**
41
     * @param DataSources\DataSourceInterface $dataSource
42
     * @param Schema                          $schema
43
     * @param int                             $numPeekRows
44
     *
45
     * @return array of validation errors
46
     */
47
    public static function validate($dataSource, $schema, $numPeekRows = 10)
48
    {
49
        try {
50
            $table = new static($dataSource, $schema);
51
        } catch (Exceptions\DataSourceException $e) {
52
            return [new SchemaValidationError(SchemaValidationError::LOAD_FAILED, $e->getMessage())];
53
        }
54
        if ($numPeekRows > 0) {
55
            $i = 0;
56
            try {
57
                foreach ($table as $row) {
58
                    if (++$i > $numPeekRows) {
59
                        break;
60
                    }
61
                }
62
            } catch (Exceptions\DataSourceException $e) {
63
                // general error in getting the next row from the data source
64
                return [new SchemaValidationError(SchemaValidationError::ROW_VALIDATION, [
65
                    'row' => $i,
66
                    'error' => $e->getMessage(),
67
                ])];
68
            } catch (Exceptions\FieldValidationException $e) {
69
                // validation error in one of the fields
70
                return array_map(function ($validationError) use ($i) {
71
                    return new SchemaValidationError(SchemaValidationError::ROW_FIELD_VALIDATION, [
72
                        'row' => $i + 1,
73
                        'field' => $validationError->extraDetails['field'],
74
                        'error' => $validationError->extraDetails['error'],
75
                        'value' => $validationError->extraDetails['value'],
76
                    ]);
77
                }, $e->validationErrors);
78
            }
79
        }
80
81
        return [];
82
    }
83
84
    public function schema($numPeekRows = 10)
85
    {
86
        $this->ensureInferredSchema($numPeekRows);
87
88
        return $this->schema;
89
    }
90
91
    public function headers($numPeekRows = 10)
92
    {
93
        $this->ensureInferredSchema($numPeekRows);
94
95
        return array_keys($this->schema->fields());
96
    }
97
98
    public function read()
99
    {
100
        $rows = [];
101
        foreach ($this as $row) {
102
            $rows[] = $row;
103
        }
104
105
        return $rows;
106
    }
107
108
    public function save($outputDataSource)
109
    {
110
        return $this->dataSource->save($outputDataSource);
111
    }
112
113
    /**
114
     * called on each iteration to get the next row
115
     * does validation and casting on the row.
116
     *
117
     * @return mixed[]
118
     *
119
     * @throws Exceptions\FieldValidationException
120
     * @throws Exceptions\DataSourceException
121
     */
122
    public function current()
123
    {
124
        if (count($this->castRows) > 0) {
125
            $row = array_shift($this->castRows);
126
        } else {
127
            $row = $this->schema->castRow($this->dataSource->getNextLine());
128
            foreach ($this->schema->fields() as $field) {
129
                if ($field->unique()) {
130
                    if (!array_key_exists($field->name(), $this->uniqueFieldValues)) {
131
                        $this->uniqueFieldValues[$field->name()] = [];
132
                    }
133
                    $value = $row[$field->name()];
134
                    if (in_array($value, $this->uniqueFieldValues[$field->name()])) {
135
                        throw new DataSourceException('field must be unique', $this->currentLine);
136
                    } else {
137
                        $this->uniqueFieldValues[$field->name()][] = $value;
138
                    }
139
                }
140
            }
141
        }
142
143
        return $row;
144
    }
145
146
    // not interesting, standard iterator functions
147
    // to simplify we prevent rewinding - so you can only iterate once
148
    public function __destruct()
149
    {
150
        $this->dataSource->close();
151
    }
152
153
    public function rewind()
154
    {
155
        if ($this->currentLine == 0) {
156
            $this->currentLine = 1;
157
        } elseif (count($this->castRows) == 0) {
158
            $this->currentLine = 1;
159
            $this->dataSource->open();
160
        }
161
    }
162
163
    public function key()
164
    {
165
        return $this->currentLine - count($this->castRows);
166
    }
167
168
    public function next()
169
    {
170
        if (count($this->castRows) == 0) {
171
            ++$this->currentLine;
172
        }
173
    }
174
175
    public function valid()
176
    {
177
        return count($this->castRows) > 0 || !$this->dataSource->isEof();
178
    }
179
180
    protected $currentLine = 0;
181
    protected $dataSource;
182
    protected $schema;
183
    protected $uniqueFieldValues;
184
    protected $castRows = [];
185
186
    protected function isInferSchema()
187
    {
188
        return is_a($this->schema, 'frictionlessdata\\tableschema\\InferSchema');
189
    }
190
191
    protected function ensureInferredSchema($numPeekRows = 10)
192
    {
193
        if ($this->isInferSchema() && count($this->schema->fields()) == 0) {
194
            // need to fetch some rows first
195
            if ($numPeekRows > 0) {
196
                $i = 0;
197
                foreach ($this as $row) {
198
                    if (++$i > $numPeekRows) {
199
                        break;
200
                    }
201
                }
202
                // these rows will be returned by next current() call
203
                $this->castRows = $this->schema->lock();
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class frictionlessdata\tableschema\Schema as the method lock() does only exist in the following sub-classes of frictionlessdata\tableschema\Schema: frictionlessdata\tableschema\InferSchema. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
204
            }
205
        }
206
    }
207
}
208