├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
└── src
├── ActiveQuery.php
├── ActiveRecord.php
├── Connection.php
├── FileManager.php
├── FileManagerJson.php
├── FileManagerPhp.php
├── Query.php
└── QueryProcessor.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii 2 File DB extension Change Log
2 | ==================================
3 |
4 | 1.0.6, April 9, 2018
5 | --------------------
6 |
7 | - Bug #10: Fixed `ActiveQuery::indexBy()` does not apply while using Yii 2.0.14 (klimov-paul)
8 |
9 |
10 | 1.0.5, November 3, 2017
11 | -----------------------
12 |
13 | - Bug: Fixed `count()` usage at `QueryProcessor::filterInCondition()` for compatibility with PHP 7.2 (klimov-paul)
14 |
15 |
16 | 1.0.4, July 7, 2017
17 | -------------------
18 |
19 | - Bug #8: Fixed `QueryProcessor` is unable to process comparison condition, e.g. `column operator value` (klimov-paul)
20 |
21 |
22 | 1.0.3, February 6, 2017
23 | -----------------------
24 |
25 | - Enh: `QueryProcessor::filterHashCondition()` allows to specify filter value as `\Closure` instance (klimov-paul)
26 | - Enh #5: `QueryProcessor::filterInCondition()` advanced allowing comparison against array-type columns (klimov-paul)
27 | - Enh #6: Added `QueryProcessor::filterCallbackCondition()` allowing to specify PHP callback as filter (klimov-paul)
28 |
29 |
30 | 1.0.2, June 28, 2016
31 | --------------------
32 |
33 | - Bug #4: Fixed `Query` unable to fetch default connection component for data fetching (klimov-paul)
34 |
35 |
36 | 1.0.1, June 3, 2016
37 | -------------------
38 |
39 | - Enh #3: `FileManagerPhp::writeData()` now invalidates script file cache performed by 'OPCache' or 'APC' (klimov-paul)
40 | - Bug #2: Fixed `ActiveRecord` looses not 'dirty' data on update (fps01)
41 |
42 |
43 | 1.0.0, December 26, 2015
44 | ------------------------
45 |
46 | - Initial release.
47 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The Yii framework is free software. It is released under the terms of
2 | the following BSD License.
3 |
4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech)
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions
9 | are met:
10 |
11 | * Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 | * Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in
15 | the documentation and/or other materials provided with the
16 | distribution.
17 | * Neither the name of Yii2tech nor the names of its
18 | contributors may be used to endorse or promote products derived
19 | from this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 | POSSIBILITY OF SUCH DAMAGE.
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
File DB Extension for Yii 2
6 |
7 |
8 |
9 | This extension provides ActiveRecord interface for the data declared in static files.
10 | Such solution allows declaration of static entities like groups, statuses and so on via files, which are
11 | stored under version control instead of database.
12 |
13 | > Note: although this extension allows writing of the data, it is not recommended. You should consider
14 | using regular relational database based on SQLite in case you need sophisticated local data storage.
15 |
16 | For license information check the [LICENSE](LICENSE.md)-file.
17 |
18 | [](https://packagist.org/packages/yii2tech/filedb)
19 | [](https://packagist.org/packages/yii2tech/filedb)
20 | [](https://travis-ci.org/yii2tech/filedb)
21 |
22 |
23 | Installation
24 | ------------
25 |
26 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
27 |
28 | Either run
29 |
30 | ```
31 | php composer.phar require --prefer-dist yii2tech/filedb
32 | ```
33 |
34 | or add
35 |
36 | ```json
37 | "yii2tech/filedb": "*"
38 | ```
39 |
40 | to the require section of your composer.json.
41 |
42 |
43 | Usage
44 | -----
45 |
46 | This extension works similar to regular Yii2 database access layer.
47 | First of all you should add a [[\yii2tech\filedb\Connection]] component to your application configuration:
48 |
49 | ```php
50 | return [
51 | 'components' => [
52 | 'filedb' => [
53 | 'class' => 'yii2tech\filedb\Connection',
54 | 'path' => '@app/data/static',
55 | ],
56 | // ...
57 | ],
58 | // ...
59 | ];
60 | ```
61 |
62 | Now you can declare actual entities and their data via files stored under '@app/data/static' path.
63 | By default regular PHP code files are used for this, but you can choose different format via [[\yii2tech\filedb\Connection::format]].
64 | Each entity should have a file with corresponding name, like 'UserGroup', 'ItemStatus' etc. So full file names for
65 | them would be '/path/to/project/data/static/UserGroup.php', '/path/to/project/data/static/ItemStatus.php' and so on.
66 | Each file should return an array containing actual entity data, for example:
67 |
68 | ```php
69 | // file 'UserGroup.php'
70 | return [
71 | [
72 | 'id' => 1,
73 | 'name' => 'admin',
74 | 'description' => 'Site administrator',
75 | ],
76 | [
77 | 'id' => 2,
78 | 'name' => 'member',
79 | 'description' => 'Registered front-end user',
80 | ],
81 | ];
82 | ```
83 |
84 | In file DB each data row should have a unique field, which identifies it - a primary key. Its name is specified
85 | by [[\yii2tech\filedb\Connection::primaryKeyName]].
86 | You may ommit primary key at rows declaration in this case key, under which row is declared in the data array will
87 | be used as primary key value. So previous data file example can be rewritten in following way:
88 |
89 | ```php
90 | // file 'UserGroup.php'
91 | return [
92 | 1 => [
93 | 'name' => 'admin',
94 | 'description' => 'Site administrator',
95 | ],
96 | 2 => [
97 | 'name' => 'member',
98 | 'description' => 'Registered front-end user',
99 | ],
100 | ];
101 | ```
102 |
103 |
104 | ## Querying Data
105 |
106 | You may execute complex query on the data declared in files using [[\yii2tech\filedb\Query]] class.
107 | This class works similar to regular [[\yii\db\Query]] and uses same syntax.
108 | For example:
109 |
110 | ```php
111 | use yii2tech\filedb\Query;
112 |
113 | $query = new Query();
114 | $query->from('UserGroup')
115 | ->limit(10);
116 | $rows = $query->all();
117 |
118 | $query = new Query();
119 | $row = $query->from('UserGroup')
120 | ->where(['name' => 'admin'])
121 | ->one();
122 | ```
123 |
124 |
125 | ## Using ActiveRecord
126 |
127 | The main purpose of this extension is provide an ActiveRecord interface for the static data.
128 | It is done via [[\yii2tech\filedb\ActiveRecord]] and [[\yii2tech\filedb\ActiveQuery]] classes.
129 | Particular ActiveRecord class should extend [[\yii2tech\filedb\ActiveRecord]] and override its `fileName()` method,
130 | specifying source data file name. For example:
131 |
132 | ```php
133 | class UserGroup extends \yii2tech\filedb\ActiveRecord
134 | {
135 | public static function fileName()
136 | {
137 | return 'UserGroup';
138 | }
139 | }
140 | ```
141 |
142 | > Note: by default `fileName()` returns own class base name (without namespace), so if you declare source data
143 | file with name equal to the class base name, you can ommit `fileName()` method overriding.
144 |
145 | [[\yii2tech\filedb\ActiveRecord]] works similar to regular [[\yii\db\ActiveRecord]], allowing finding, validation
146 | and saving models. It can establish relations to other ActiveRecord classes, which are usually represents entities
147 | from relational database. For example:
148 |
149 | ```php
150 | class UserGroup extends \yii2tech\filedb\ActiveRecord
151 | {
152 | public function getUsers()
153 | {
154 | return $this->hasMany(User::className(), ['groupId' => 'id']);
155 | }
156 | }
157 |
158 | class User extends \yii\db\ActiveRecord
159 | {
160 | public function getGroup()
161 | {
162 | return $this->hasOne(UserGroup::className(), ['id' => 'groupId']);
163 | }
164 | }
165 | ```
166 |
167 | So relational queries can be performed like following:
168 |
169 | ```php
170 | $users = User::find()->with('group')->all();
171 | foreach ($users as $user) {
172 | echo 'username: ' . $user->name . "\n";
173 | echo 'group: ' . $user->group->name . "\n\n";
174 | }
175 |
176 | $adminGroup = UserGroup::find()->where(['name' => 'admin'])->one();
177 | foreach ($adminGroup->users as $user) {
178 | echo $user->name . "\n";
179 | }
180 | ```
181 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yii2tech/filedb",
3 | "description": "Provides ActiveRecord interface for data declared in static files",
4 | "keywords": ["yii2", "filedb", "static", "active", "record"],
5 | "type": "yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "support": {
8 | "issues": "https://github.com/yii2tech/filedb/issues",
9 | "forum": "http://www.yiiframework.com/forum/",
10 | "wiki": "https://github.com/yii2tech/filedb/wiki",
11 | "source": "https://github.com/yii2tech/filedb"
12 | },
13 | "authors": [
14 | {
15 | "name": "Paul Klimov",
16 | "email": "klimov.paul@gmail.com"
17 | }
18 | ],
19 | "require": {
20 | "yiisoft/yii2": "~2.0.14"
21 | },
22 | "repositories": [
23 | {
24 | "type": "composer",
25 | "url": "https://asset-packagist.org"
26 | }
27 | ],
28 | "autoload": {
29 | "psr-4": {"yii2tech\\filedb\\": "src"}
30 | },
31 | "extra": {
32 | "branch-alias": {
33 | "dev-master": "1.0.x-dev"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/ActiveQuery.php:
--------------------------------------------------------------------------------
1 | with('tags')->asArray()->all();
45 | * ```
46 | *
47 | * Relational query
48 | * ----------------
49 | *
50 | * In relational context ActiveQuery represents a relation between two Active Record classes.
51 | *
52 | * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and
53 | * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining
54 | * a getter method which calls one of the above methods and returns the created ActiveQuery object.
55 | *
56 | * A relation is specified by [[link]] which represents the association between columns
57 | * of different tables; and the multiplicity of the relation is indicated by [[multiple]].
58 | *
59 | * @see ActiveRecord
60 | *
61 | * @author Paul Klimov
62 | * @since 1.0
63 | */
64 | class ActiveQuery extends Query implements ActiveQueryInterface
65 | {
66 | use ActiveQueryTrait;
67 | use ActiveRelationTrait;
68 |
69 | /**
70 | * @event Event an event that is triggered when the query is initialized via [[init()]].
71 | */
72 | const EVENT_INIT = 'init';
73 |
74 | /**
75 | * Constructor.
76 | * @param string $modelClass the model class associated with this query
77 | * @param array $config configurations to be applied to the newly created query object
78 | */
79 | public function __construct($modelClass, $config = [])
80 | {
81 | $this->modelClass = $modelClass;
82 | parent::__construct($config);
83 | }
84 |
85 | /**
86 | * Initializes the object.
87 | * This method is called at the end of the constructor. The default implementation will trigger
88 | * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
89 | * to ensure triggering of the event.
90 | */
91 | public function init()
92 | {
93 | parent::init();
94 | $this->trigger(self::EVENT_INIT);
95 | }
96 |
97 | /**
98 | * Executes query and returns all results as an array.
99 | * @param Connection $db the database connection used to execute the query.
100 | * If null, the connection returned by [[modelClass]] will be used.
101 | * @return array|ActiveRecord[] the query results. If the query results in nothing, an empty array will be returned.
102 | */
103 | public function all($db = null)
104 | {
105 | return parent::all($db);
106 | }
107 |
108 | /**
109 | * Executes query and returns a single row of result.
110 | * @param Connection $db the database connection used to execute the query.
111 | * If null, the DB connection returned by [[modelClass]] will be used.
112 | * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]],
113 | * the query result may be either an array or an ActiveRecord object. Null will be returned
114 | * if the query results in nothing.
115 | */
116 | public function one($db = null)
117 | {
118 | $row = parent::one($db);
119 | if ($row !== false) {
120 | $models = $this->populate([$row]);
121 | return reset($models) ?: null;
122 | }
123 | return null;
124 | }
125 |
126 | /**
127 | * {@inheritdoc}
128 | */
129 | public function populate($rows)
130 | {
131 | if (empty($rows)) {
132 | return [];
133 | }
134 |
135 | $models = $this->createModels($rows);
136 | if (!empty($this->with)) {
137 | $this->findWith($this->with, $models);
138 | }
139 | if (!$this->asArray) {
140 | foreach ($models as $model) {
141 | $model->afterFind();
142 | }
143 | }
144 |
145 | return parent::populate($models);
146 | }
147 |
148 | /**
149 | * {@inheritdoc}
150 | */
151 | protected function fetchData($db)
152 | {
153 | if ($this->primaryModel !== null) {
154 | // lazy loading
155 | if ($this->via instanceof self) {
156 | // via pivot data file
157 | $viaModels = $this->via->findJunctionRows([$this->primaryModel]);
158 | $this->filterByModels($viaModels);
159 | } elseif (is_array($this->via)) {
160 | // via relation
161 | /* @var $viaQuery ActiveQuery */
162 | list($viaName, $viaQuery) = $this->via;
163 | if ($viaQuery->multiple) {
164 | $viaModels = $viaQuery->all();
165 | $this->primaryModel->populateRelation($viaName, $viaModels);
166 | } else {
167 | $model = $viaQuery->one();
168 | $this->primaryModel->populateRelation($viaName, $model);
169 | $viaModels = $model === null ? [] : [$model];
170 | }
171 | $this->filterByModels($viaModels);
172 | } else {
173 | $this->filterByModels([$this->primaryModel]);
174 | }
175 | }
176 |
177 | if ($db === null) {
178 | /* @var $modelClass ActiveRecord */
179 | $modelClass = $this->modelClass;
180 | $db = $modelClass::getDb();
181 | }
182 |
183 | if (empty($this->from)) {
184 | /* @var $modelClass ActiveRecord */
185 | $modelClass = $this->modelClass;
186 | $this->from = $modelClass::fileName();
187 | }
188 |
189 | return $db->getQueryProcessor()->process($this);
190 | }
191 | }
--------------------------------------------------------------------------------
/src/ActiveRecord.php:
--------------------------------------------------------------------------------
1 |
39 | * @since 1.0
40 | */
41 | class ActiveRecord extends BaseActiveRecord
42 | {
43 | /**
44 | * Returns the static DB connection used by this AR class.
45 | * By default, the "filedb" application component is used as the connection.
46 | * You may override this method if you want to use a different connection.
47 | * @return Connection the database connection used by this AR class.
48 | */
49 | public static function getDb()
50 | {
51 | return Yii::$app->get('filedb');
52 | }
53 |
54 | /**
55 | * Returns the primary key name(s) for this AR class.
56 | * The default implementation will return ['id'].
57 | *
58 | * Note that an array should be returned even when the record only has a single primary key.
59 | *
60 | * For the primary key **value** see [[getPrimaryKey()]] instead.
61 | *
62 | * @return string[] the primary key name(s) for this AR class.
63 | */
64 | public static function primaryKey()
65 | {
66 | return ['id'];
67 | }
68 |
69 | /**
70 | * {@inheritdoc}
71 | * @return ActiveQuery the newly created [[ActiveQuery]] instance.
72 | */
73 | public static function find()
74 | {
75 | return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
76 | }
77 |
78 | /**
79 | * Declares the name of the data file associated with this AR class.
80 | * By default this method returns the class name as the data file name.
81 | * You may override this method if the collection is not named after this convention.
82 | * @return string|array the collection name.
83 | */
84 | public static function fileName()
85 | {
86 | return StringHelper::basename(get_called_class());
87 | }
88 |
89 | /**
90 | * {@inheritdoc}
91 | */
92 | public function attributes()
93 | {
94 | static $attributes;
95 | if ($attributes === null) {
96 | $rows = static::getDb()->readData(static::fileName());
97 | $attributes = array_keys(reset($rows));
98 | }
99 | return $attributes;
100 | }
101 |
102 | /**
103 | * Inserts the record into the database using the attribute values of this record.
104 | *
105 | * Usage example:
106 | *
107 | * ```php
108 | * $customer = new Customer;
109 | * $customer->name = $name;
110 | * $customer->email = $email;
111 | * $customer->insert();
112 | * ```
113 | *
114 | * @param bool $runValidation whether to perform validation before saving the record.
115 | * If the validation fails, the record will not be inserted into the database.
116 | * @param array $attributes list of attributes that need to be saved. Defaults to null,
117 | * meaning all attributes that are loaded from DB will be saved.
118 | * @return bool whether the attributes are valid and the record is inserted successfully.
119 | */
120 | public function insert($runValidation = true, $attributes = null)
121 | {
122 | if ($runValidation && !$this->validate($attributes)) {
123 | return false;
124 | }
125 | $result = $this->insertInternal($attributes);
126 |
127 | return $result;
128 | }
129 |
130 | /**
131 | * @see ActiveRecord::insert()
132 | */
133 | protected function insertInternal($attributes = null)
134 | {
135 | if (!$this->beforeSave(true)) {
136 | return false;
137 | }
138 | $values = $this->getDirtyAttributes($attributes);
139 | if (empty($values)) {
140 | $currentAttributes = $this->getAttributes();
141 | foreach ($this->primaryKey() as $key) {
142 | if (isset($currentAttributes[$key])) {
143 | $values[$key] = $currentAttributes[$key];
144 | }
145 | }
146 | }
147 |
148 | $db = static::getDb();
149 | $pkName = $db->primaryKeyName;
150 | if (!isset($values[$pkName])) {
151 | throw new InvalidConfigException("'" . get_class($this) . "::{$pkName}' must be set.");
152 | }
153 | $dataFileName = static::fileName();
154 | $data = $db->readData($dataFileName);
155 | if (isset($data[$values[$pkName]])) {
156 | throw new InvalidConfigException("'{$pkName}' value '{$values[$pkName]}' is already taken.");
157 | }
158 | $data[$values[$pkName]] = $values;
159 | $db->writeData($dataFileName, $data);
160 |
161 | $changedAttributes = array_fill_keys(array_keys($values), null);
162 | $this->setOldAttributes($values);
163 | $this->afterSave(true, $changedAttributes);
164 |
165 | return true;
166 | }
167 |
168 | /**
169 | * @see ActiveRecord::update()
170 | */
171 | protected function updateInternal($attributes = null)
172 | {
173 | if (!$this->beforeSave(false)) {
174 | return false;
175 | }
176 | $values = $this->getDirtyAttributes($attributes);
177 | if (empty($values)) {
178 | $this->afterSave(false, $values);
179 | return 0;
180 | }
181 |
182 | $db = static::getDb();
183 | $pkName = $db->primaryKeyName;
184 | $attributes = $this->getAttributes();
185 | if (!isset($attributes[$pkName])) {
186 | throw new InvalidConfigException("'" . get_class($this) . "::{$pkName}' must be set.");
187 | }
188 | $dataFileName = static::fileName();
189 | $data = $db->readData($dataFileName);
190 | if (!isset($data[$attributes[$pkName]])) {
191 | throw new InvalidConfigException("'{$pkName}' value '{$values[$pkName]}' does not exist.");
192 | }
193 |
194 | $changedAttributes = [];
195 | foreach ($values as $name => $value) {
196 | $data[$attributes[$pkName]][$name] = $value;
197 | $changedAttributes[$name] = $this->getOldAttribute($name);
198 | $this->setOldAttribute($name, $value);
199 | }
200 | $db->writeData($dataFileName, $data);
201 |
202 | $this->afterSave(false, $changedAttributes);
203 |
204 | return 1;
205 | }
206 |
207 | /**
208 | * {@inheritdoc}
209 | */
210 | public function delete()
211 | {
212 | $result = false;
213 | if ($this->beforeDelete()) {
214 |
215 | $db = static::getDb();
216 | $pkName = $db->primaryKeyName;
217 | $attributes = $this->getAttributes();
218 | if (!isset($attributes[$pkName])) {
219 | throw new InvalidConfigException("'" . get_class($this) . "::{$pkName}' must be set.");
220 | }
221 | $dataFileName = static::fileName();
222 | $data = $db->readData($dataFileName);
223 | if (!isset($data[$attributes[$pkName]])) {
224 | return false;
225 | }
226 | unset($data[$attributes[$pkName]]);
227 | $db->writeData($dataFileName, $data);
228 |
229 | $result = 1;
230 |
231 | $this->afterDelete();
232 | }
233 |
234 | return $result;
235 | }
236 |
237 | /**
238 | * {@inheritdoc}
239 | */
240 | public static function updateAll($attributes, $condition = '')
241 | {
242 | $count = 0;
243 | $records = static::findAll($condition);
244 | foreach ($records as $record) {
245 | $record->setAttributes($attributes, false);
246 | if ($record->save(false)) {
247 | $count++;
248 | }
249 | }
250 | return $count;
251 | }
252 |
253 | /**
254 | * {@inheritdoc}
255 | */
256 | public static function deleteAll($condition = null)
257 | {
258 | $count = 0;
259 | $records = static::findAll($condition);
260 | foreach ($records as $record) {
261 | $count += $record->delete();
262 | }
263 | return $count;
264 | }
265 |
266 | /**
267 | * {@inheritdoc}
268 | */
269 | public static function updateAllCounters($counters, $condition = '')
270 | {
271 | $count = 0;
272 | $records = static::findAll($condition);
273 | foreach ($records as $record) {
274 | foreach ($counters as $attribute => $increment) {
275 | $record->$attribute += $increment;
276 | }
277 | $record->save(false);
278 | $count++;
279 | }
280 | return $count;
281 | }
282 |
283 | /**
284 | * Returns a value indicating whether the given active record is the same as the current one.
285 | * The comparison is made by comparing the data file names and the primary key values of the two active records.
286 | * If one of the records [[isNewRecord|is new]] they are also considered not equal.
287 | * @param ActiveRecord $record record to compare to
288 | * @return bool whether the two active records refer to the same row in the same data file.
289 | */
290 | public function equals($record)
291 | {
292 | if ($this->isNewRecord || $record->isNewRecord) {
293 | return false;
294 | }
295 |
296 | return $this->fileName() === $record->fileName() && (string) $this->getPrimaryKey() === (string) $record->getPrimaryKey();
297 | }
298 | }
--------------------------------------------------------------------------------
/src/Connection.php:
--------------------------------------------------------------------------------
1 | [
25 | * 'filedb' => [
26 | * 'class' => 'yii2tech\filedb\Connection',
27 | * 'path' => '@app/data/static',
28 | * ]
29 | * ],
30 | * ];
31 | * ```
32 | *
33 | * @property QueryProcessor|array|string $queryProcessor the query processor object or its configuration.
34 | * @property FileManager $fileManager the file manager instance. This property is read-only.
35 | *
36 | * @author Paul Klimov
37 | * @since 1.0
38 | */
39 | class Connection extends Component
40 | {
41 | /**
42 | * @var string path to directory, which holds data files.
43 | */
44 | public $path = '@app/filedb';
45 | /**
46 | * @var string data files format.
47 | * Format determines, which file manager should be used to read/write data files, using [[fileManagerMap]].
48 | */
49 | public $format = 'php';
50 | /**
51 | * @var array mapping between data file format (see [[format]]) and file manager classes.
52 | * The keys of the array are format names while the values the corresponding
53 | * file manager class name or configuration. Please refer to [[Yii::createObject()]] for
54 | * details on how to specify a configuration.
55 | */
56 | public $fileManagerMap = [
57 | 'php' => 'yii2tech\filedb\FileManagerPhp',
58 | 'json' => 'yii2tech\filedb\FileManagerJson',
59 | ];
60 | /**
61 | * @var string name of the data key, which should be used as row unique id - primary key.
62 | * If source data holds no corresponding key, the key of the row in source array will be used as its value.
63 | */
64 | public $primaryKeyName = 'id';
65 | /**
66 | * @var bool whether to cache read data in memory.
67 | * While enabled this option may speed up program execution, but will cost extra memory usage.
68 | */
69 | public $enableDataCache = true;
70 |
71 | /**
72 | * @var QueryProcessor|array|string the query processor object or its configuration.
73 | */
74 | private $_queryProcessor = 'yii2tech\filedb\QueryProcessor';
75 | /**
76 | * @var bool whether [[queryProcessor]] has been initialized or not.
77 | */
78 | private $isQueryProcessorInitialized = false;
79 | /**
80 | * @var FileManager file manager instance.
81 | */
82 | private $_fileManager;
83 | /**
84 | * @var array read data cache.
85 | */
86 | private $dataCache = [];
87 |
88 |
89 | /**
90 | * @param array|string|QueryProcessor $queryProcessor query processor instance or its configuration.
91 | */
92 | public function setQueryProcessor($queryProcessor)
93 | {
94 | $this->_queryProcessor = $queryProcessor;
95 | $this->isQueryProcessorInitialized = false;
96 | }
97 |
98 | /**
99 | * @return QueryProcessor query processor instance
100 | */
101 | public function getQueryProcessor()
102 | {
103 | if (!$this->isQueryProcessorInitialized) {
104 | $this->_queryProcessor = Instance::ensure($this->_queryProcessor, QueryProcessor::className());
105 | $this->_queryProcessor->db = $this;
106 | }
107 | return $this->_queryProcessor;
108 | }
109 |
110 | /**
111 | * @return FileManager file manager instance.
112 | * @throws InvalidConfigException on invalid configuration.
113 | */
114 | public function getFileManager()
115 | {
116 | if (!is_object($this->_fileManager)) {
117 | if (!isset($this->fileManagerMap[$this->format])) {
118 | throw new InvalidConfigException("Unsupported format '{$this->format}'.");
119 | }
120 | $config = $this->fileManagerMap[$this->format];
121 | if (!is_array($config)) {
122 | $config = ['class' => $config];
123 | }
124 | $this->_fileManager = Yii::createObject($config);
125 | }
126 | return $this->_fileManager;
127 | }
128 |
129 | /**
130 | * Reads the data from data file.
131 | * @param string $name data file name.
132 | * @param bool $refresh whether to reload the data even if it is found in the cache.
133 | * @return array[] data.
134 | * @throws InvalidConfigException on failure.
135 | */
136 | public function readData($name, $refresh = false)
137 | {
138 | if (isset($this->dataCache[$name]) && !$refresh) {
139 | return $this->dataCache[$name];
140 | }
141 | $rawData = $this->getFileManager()->readData($this->composeFullFileName($name));
142 | $data = [];
143 | foreach ($rawData as $key => $value) {
144 | if (isset($value[$this->primaryKeyName])) {
145 | $pk = $value[$this->primaryKeyName];
146 | } else {
147 | $pk = $key;
148 | $value[$this->primaryKeyName] = $pk;
149 | }
150 | $data[$pk] = $value;
151 | }
152 | if ($this->enableDataCache) {
153 | $this->dataCache[$name] = $data;
154 | }
155 | return $data;
156 | }
157 |
158 | /**
159 | * Writes data into data file.
160 | * @param string $name data file name.
161 | * @param array[] $data data to be written.
162 | * @throws Exception on failure.
163 | */
164 | public function writeData($name, array $data)
165 | {
166 | $this->getFileManager()->writeData($this->composeFullFileName($name), $data);
167 | unset($this->dataCache[$name]);
168 | }
169 |
170 | /**
171 | * Composes data file name with full path, but without extension.
172 | * @param string $name data file self name.
173 | * @return string data file name without extension.
174 | */
175 | protected function composeFullFileName($name)
176 | {
177 | return Yii::getAlias($this->path) . DIRECTORY_SEPARATOR . $name;
178 | }
179 | }
--------------------------------------------------------------------------------
/src/FileManager.php:
--------------------------------------------------------------------------------
1 |
16 | * @since 1.0
17 | */
18 | abstract class FileManager extends Component
19 | {
20 | /**
21 | * @var string data file extension to be used.
22 | */
23 | public $fileExtension = 'dat';
24 |
25 |
26 | /**
27 | * Reads the data from data file.
28 | * @param string $fileName file name without extension.
29 | * @return array[] data.
30 | */
31 | abstract public function readData($fileName);
32 |
33 | /**
34 | * Writes data into data file.
35 | * @param string $fileName file name without extension.
36 | * @param array $data data to be written.
37 | */
38 | abstract public function writeData($fileName, array $data);
39 |
40 | /**
41 | * Composes data file actual name.
42 | * @param string $fileName data file name without extension.
43 | * @return string data file full name.
44 | */
45 | protected function composeActualFileName($fileName)
46 | {
47 | return $fileName . '.' . $this->fileExtension;
48 | }
49 | }
--------------------------------------------------------------------------------
/src/FileManagerJson.php:
--------------------------------------------------------------------------------
1 |
18 | * @since 1.0
19 | */
20 | class FileManagerJson extends FileManager
21 | {
22 | /**
23 | * @var string data file extension to be used.
24 | */
25 | public $fileExtension = 'json';
26 |
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function readData($fileName)
32 | {
33 | $fileName = $this->composeActualFileName($fileName);
34 | if (!is_file($fileName)) {
35 | throw new InvalidConfigException("File '{$fileName}' does not exist.");
36 | }
37 | $content = file_get_contents($fileName);
38 | return Json::decode($content);
39 | }
40 |
41 | /**
42 | * {@inheritdoc}
43 | */
44 | public function writeData($fileName, array $data)
45 | {
46 | $fileName = $this->composeActualFileName($fileName);
47 | $content = Json::encode($data);
48 | $bytesWritten = file_put_contents($fileName, $content);
49 | if ($bytesWritten <= 0) {
50 | throw new Exception("Unable to write file '{$fileName}'.");
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/FileManagerPhp.php:
--------------------------------------------------------------------------------
1 |
21 | * @since 1.0
22 | */
23 | class FileManagerPhp extends FileManager
24 | {
25 | /**
26 | * @var string data file extension to be used.
27 | */
28 | public $fileExtension = 'php';
29 |
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function readData($fileName)
35 | {
36 | $fileName = $this->composeActualFileName($fileName);
37 | $data = require $fileName;
38 | if (!is_array($data)) {
39 | throw new InvalidConfigException("File '{$fileName}' should return an array.");
40 | }
41 | return $data;
42 | }
43 |
44 | /**
45 | * {@inheritdoc}
46 | */
47 | public function writeData($fileName, array $data)
48 | {
49 | $fileName = $this->composeActualFileName($fileName);
50 | $content = "invalidateScriptCache($fileName);
56 | }
57 |
58 | /**
59 | * Invalidates precompiled script cache (such as OPCache or APC) for the given file.
60 | * @param string $fileName file name.
61 | * @since 1.0.1
62 | */
63 | protected function invalidateScriptCache($fileName)
64 | {
65 | if (function_exists('opcache_invalidate')) {
66 | opcache_invalidate($fileName, true);
67 | }
68 | if (function_exists('apc_delete_file')) {
69 | @apc_delete_file($fileName);
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/src/Query.php:
--------------------------------------------------------------------------------
1 | from('status')
25 | * ->where(['type' => 'public'])
26 | * ->limit(10);
27 | * // build and execute the query
28 | * $rows = $query->all();
29 | * ```
30 | *
31 | * @author Paul Klimov
32 | * @since 1.0
33 | */
34 | class Query extends Component implements QueryInterface
35 | {
36 | use QueryTrait;
37 |
38 | /**
39 | * @var string data file name to be selected from.
40 | * @see from()
41 | */
42 | public $from;
43 |
44 |
45 | /**
46 | * Executes the query and returns all results as an array.
47 | * @param Connection $db the database connection used to execute the query.
48 | * If this parameter is not given, the `db` application component will be used.
49 | * @return array the query results. If the query results in nothing, an empty array will be returned.
50 | */
51 | public function all($db = null)
52 | {
53 | $rows = $this->fetchData($db);
54 | return $this->populate($rows);
55 | }
56 |
57 | /**
58 | * Executes the query and returns a single row of result.
59 | * @param Connection $db the database connection used to execute the query.
60 | * If this parameter is not given, the `filedb` application component will be used.
61 | * @return array|bool the first row (in terms of an array) of the query result. False is returned if the query
62 | * results in nothing.
63 | */
64 | public function one($db = null)
65 | {
66 | $rows = $this->fetchData($db);
67 | return empty($rows) ? false : reset($rows);
68 | }
69 |
70 | /**
71 | * Returns the number of records.
72 | * @param string $q the COUNT expression. Defaults to '*'.
73 | * @param Connection $db the database connection used to execute the query.
74 | * If this parameter is not given, the `filedb` application component will be used.
75 | * @return int number of records.
76 | */
77 | public function count($q = '*', $db = null)
78 | {
79 | $data = $this->fetchData($db);
80 | return count($data);
81 | }
82 |
83 | /**
84 | * Returns a value indicating whether the query result contains any row of data.
85 | * @param Connection $db the database connection used to execute the query.
86 | * If this parameter is not given, the `db` application component will be used.
87 | * @return bool whether the query result contains any row of data.
88 | */
89 | public function exists($db = null)
90 | {
91 | $data = $this->fetchData($db);
92 | return !empty($data);
93 | }
94 |
95 | /**
96 | * Sets data file name to be selected from.
97 | * @param string $name data file name.
98 | * @return $this the query object itself
99 | */
100 | public function from($name)
101 | {
102 | $this->from = $name;
103 | return $this;
104 | }
105 |
106 | /**
107 | * Fetches data from storage.
108 | * @param Connection|null $db connection to be used for data fetching.
109 | * If this parameter is not given, the `filedb` application component will be used.
110 | * @return array[] fetched data.
111 | */
112 | protected function fetchData($db)
113 | {
114 | if ($db === null) {
115 | $db = Yii::$app->get('filedb');
116 | }
117 |
118 | return $db->getQueryProcessor()->process($this);
119 | }
120 |
121 | /**
122 | * Converts the raw query results into the format as specified by this query.
123 | * This method is internally used to convert the data fetched from database
124 | * into the format as required by this query.
125 | * @param array $rows the raw query result from database
126 | * @return array the converted query result
127 | */
128 | public function populate($rows)
129 | {
130 | if ($this->indexBy === null) {
131 | return array_values($rows); // reset storage internal keys
132 | }
133 | $result = [];
134 | foreach ($rows as $row) {
135 | $result[ArrayHelper::getValue($row, $this->indexBy)] = $row;
136 | }
137 | return $result;
138 | }
139 | }
--------------------------------------------------------------------------------
/src/QueryProcessor.php:
--------------------------------------------------------------------------------
1 |
21 | * @since 1.0
22 | */
23 | class QueryProcessor extends Component
24 | {
25 | /**
26 | * @var Connection the database connection.
27 | */
28 | public $db;
29 |
30 | /**
31 | * @var array map of query condition to filter methods.
32 | * These methods are used by [[filterCondition]] to filter raw data by 'where' array syntax.
33 | */
34 | protected $conditionFilters = [
35 | 'NOT' => 'filterNotCondition',
36 | 'AND' => 'filterAndCondition',
37 | 'OR' => 'filterOrCondition',
38 | 'BETWEEN' => 'filterBetweenCondition',
39 | 'NOT BETWEEN' => 'filterBetweenCondition',
40 | 'IN' => 'filterInCondition',
41 | 'NOT IN' => 'filterInCondition',
42 | 'LIKE' => 'filterLikeCondition',
43 | 'NOT LIKE' => 'filterLikeCondition',
44 | 'OR LIKE' => 'filterLikeCondition',
45 | 'OR NOT LIKE' => 'filterLikeCondition',
46 | 'CALLBACK' => 'filterCallbackCondition',
47 | ];
48 |
49 |
50 | /**
51 | * @param Query $query
52 | * @return array[]
53 | */
54 | public function process($query)
55 | {
56 | $data = $this->db->readData($query->from);
57 | $data = $this->applyWhere($data, $query->where);
58 | $data = $this->applyOrderBy($data, $query->orderBy);
59 | $data = $this->applyLimit($data, $query->limit, $query->offset);
60 | return $data;
61 | }
62 |
63 | /**
64 | * Applies sort for given data.
65 | * @param array $data raw data.
66 | * @param array|null $orderBy order by.
67 | * @return array sorted data.
68 | */
69 | public function applyOrderBy(array $data, $orderBy)
70 | {
71 | if (!empty($orderBy)) {
72 | ArrayHelper::multisort($data, array_keys($orderBy), array_values($orderBy));
73 | }
74 | return $data;
75 | }
76 |
77 | /**
78 | * Applies limit and offset for given data.
79 | * @param array $data raw data.
80 | * @param int|null $limit limit value.
81 | * @param int|null $offset offset value.
82 | * @return array data.
83 | */
84 | public function applyLimit(array $data, $limit, $offset)
85 | {
86 | if (empty($limit) && empty($offset)) {
87 | return $data;
88 | }
89 | if (!ctype_digit((string) $limit)) {
90 | $limit = null;
91 | }
92 | if (!ctype_digit((string) $offset)) {
93 | $offset = 0;
94 | }
95 | return array_slice($data, $offset, $limit);
96 | }
97 |
98 | /**
99 | * Applies where conditions.
100 | * @param array $data raw data.
101 | * @param array|null $where where conditions.
102 | * @return array data.
103 | */
104 | public function applyWhere(array $data, $where)
105 | {
106 | return $this->filterCondition($data, $where);
107 | }
108 |
109 | /**
110 | * Applies filter conditions.
111 | * @param array $data data to be filtered.
112 | * @param array $condition filter condition.
113 | * @return array filtered data.
114 | * @throws InvalidParamException
115 | */
116 | public function filterCondition(array $data, $condition)
117 | {
118 | if (empty($condition)) {
119 | return $data;
120 | }
121 | if (!is_array($condition)) {
122 | throw new InvalidParamException('Condition must be an array');
123 | }
124 |
125 | if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
126 | $operator = strtoupper($condition[0]);
127 | if (isset($this->conditionFilters[$operator])) {
128 | $method = $this->conditionFilters[$operator];
129 | } else {
130 | $method = 'filterSimpleCondition';
131 | }
132 | array_shift($condition);
133 | return $this->$method($data, $operator, $condition);
134 | } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
135 | return $this->filterHashCondition($data, $condition);
136 | }
137 | }
138 |
139 | /**
140 | * Applies a condition based on column-value pairs.
141 | * @param array $data data to be filtered
142 | * @param array $condition the condition specification.
143 | * @return array filtered data
144 | */
145 | public function filterHashCondition(array $data, $condition)
146 | {
147 | foreach ($condition as $column => $value) {
148 | if (is_array($value)) {
149 | // IN condition
150 | $data = $this->filterInCondition($data, 'IN', [$column, $value]);
151 | } else {
152 | $data = array_filter($data, function($row) use ($column, $value) {
153 | if ($value instanceof \Closure) {
154 | return call_user_func($value, $row[$column]);
155 | }
156 | return ($row[$column] == $value);
157 | });
158 | }
159 | }
160 | return $data;
161 | }
162 |
163 | /**
164 | * Applies 2 or more conditions using 'AND' logic.
165 | * @param array $data data to be filtered
166 | * @param string $operator operator.
167 | * @param array $operands conditions to be united.
168 | * @return array filtered data
169 | */
170 | public function filterAndCondition(array $data, $operator, $operands)
171 | {
172 | foreach ($operands as $operand) {
173 | if (is_array($operand)) {
174 | $data = $this->filterCondition($data, $operand);
175 | }
176 | }
177 | return $data;
178 | }
179 |
180 | /**
181 | * Applies 2 or more conditions using 'OR' logic.
182 | * @param array $data data to be filtered.
183 | * @param string $operator operator.
184 | * @param array $operands conditions to be united.
185 | * @return array filtered data
186 | */
187 | public function filterOrCondition(array $data, $operator, $operands)
188 | {
189 | $parts = [];
190 | foreach ($operands as $operand) {
191 | if (is_array($operand)) {
192 | $parts[] = $this->filterCondition($data, $operand);
193 | }
194 | }
195 | if (empty($parts)) {
196 | return $data;
197 | }
198 | $data = [];
199 | foreach ($parts as $part) {
200 | foreach ($part as $row) {
201 | $pk = $row[$this->db->primaryKeyName];
202 | $data[$pk] = $row;
203 | }
204 | }
205 | return $data;
206 | }
207 |
208 | /**
209 | * Inverts a filter condition.
210 | * @param array $data data to be filtered.
211 | * @param string $operator operator.
212 | * @param array $operands operands to be inverted.
213 | * @return array filtered data.
214 | * @throws InvalidParamException if wrong number of operands have been given.
215 | */
216 | public function filterNotCondition(array $data, $operator, $operands)
217 | {
218 | if (count($operands) != 1) {
219 | throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
220 | }
221 |
222 | $operand = reset($operands);
223 | $filteredData = $this->filterCondition($data, $operand);
224 | if (empty($filteredData)) {
225 | return $data;
226 | }
227 |
228 | $pkName = $this->db->primaryKeyName;
229 | foreach ($data as $key => $row) {
230 | foreach ($filteredData as $filteredRowKey => $filteredRow) {
231 | if ($row[$pkName] === $filteredRow[$pkName]) {
232 | unset($data[$key]);
233 | unset($filteredData[$filteredRowKey]);
234 | break;
235 | }
236 | }
237 | }
238 |
239 | return $data;
240 | }
241 |
242 | /**
243 | * Applies `BETWEEN` condition.
244 | * @param array $data data to be filtered.
245 | * @param string $operator operator.
246 | * @param array $operands the first operand is the column name. The second and third operands
247 | * describe the interval that column value should be in.
248 | * @return array filtered data.
249 | * @throws InvalidParamException if wrong number of operands have been given.
250 | */
251 | public function filterBetweenCondition(array $data, $operator, $operands)
252 | {
253 | if (!isset($operands[0], $operands[1], $operands[2])) {
254 | throw new InvalidParamException("Operator '$operator' requires three operands.");
255 | }
256 |
257 | list($column, $value1, $value2) = $operands;
258 |
259 | if (strncmp('NOT', $operator, 3) === 0) {
260 | return array_filter($data, function($row) use ($column, $value1, $value2) {
261 | return ($row[$column] < $value1 || $row[$column] > $value2);
262 | });
263 | }
264 |
265 | return array_filter($data, function($row) use ($column, $value1, $value2) {
266 | return ($row[$column] >= $value1 && $row[$column] <= $value2);
267 | });
268 | }
269 |
270 | /**
271 | * Applies 'IN' condition.
272 | * @param array $data data to be filtered.
273 | * @param string $operator operator.
274 | * @param array $operands the first operand is the column name.
275 | * The second operand is an array of values that column value should be among.
276 | * @return array filtered data.
277 | * @throws InvalidParamException if wrong number of operands have been given.
278 | */
279 | public function filterInCondition(array $data, $operator, $operands)
280 | {
281 | if (!isset($operands[0], $operands[1])) {
282 | throw new InvalidParamException("Operator '$operator' requires two operands.");
283 | }
284 |
285 | list($column, $values) = $operands;
286 |
287 | if ($values === [] || $column === []) {
288 | return $operator === 'IN' ? [] : $data;
289 | }
290 |
291 | $values = (array) $values;
292 |
293 | if (is_array($column)) {
294 | if (count($column) > 1) {
295 | throw new InvalidParamException("Operator '$operator' allows only a single column.");
296 | }
297 | $column = reset($column);
298 | }
299 | foreach ($values as $i => $value) {
300 | if (is_array($value)) {
301 | $values[$i] = isset($value[$column]) ? $value[$column] : null;
302 | }
303 | }
304 |
305 | if (strncmp('NOT', $operator, 3) === 0) {
306 | return array_filter($data, function($row) use ($column, $values) {
307 | $columnValue = $row[$column];
308 | if (is_array($columnValue)) {
309 | return array_intersect($values, $columnValue) === [];
310 | }
311 | return !in_array($columnValue, $values);
312 | });
313 | }
314 |
315 | return array_filter($data, function($row) use ($column, $values) {
316 | $columnValue = $row[$column];
317 | if (is_array($columnValue)) {
318 | return array_intersect($values, $columnValue) !== [];
319 | }
320 | return in_array($columnValue, $values);
321 | });
322 | }
323 |
324 | /**
325 | * Applies 'LIKE' condition.
326 | * @param array $data data to be filtered.
327 | * @param string $operator operator.
328 | * @param array $operands the first operand is the column name. The second operand is a single value
329 | * or an array of values that column value should be compared with.
330 | * @return array filtered data.
331 | * @throws InvalidParamException if wrong number of operands have been given.
332 | */
333 | public function filterLikeCondition(array $data, $operator, $operands)
334 | {
335 | if (!isset($operands[0], $operands[1])) {
336 | throw new InvalidParamException("Operator '$operator' requires two operands.");
337 | }
338 |
339 | list($column, $values) = $operands;
340 |
341 | if (!is_array($values)) {
342 | $values = [$values];
343 | }
344 |
345 | $not = (stripos($operator, 'NOT ') !== false);
346 | $or = (stripos($operator, 'OR ') !== false);
347 |
348 | if ($not) {
349 | if (empty($values)) {
350 | return $data;
351 | }
352 |
353 | if ($or) {
354 | return array_filter($data, function($row) use ($column, $values) {
355 | foreach ($values as $value) {
356 | if (stripos($row[$column], $value) === false) {
357 | return true;
358 | }
359 | }
360 | return false;
361 | });
362 | }
363 |
364 | return array_filter($data, function($row) use ($column, $values) {
365 | foreach ($values as $value) {
366 | if (stripos($row[$column], $value) !== false) {
367 | return false;
368 | }
369 | }
370 | return true;
371 | });
372 | }
373 |
374 | if (empty($values)) {
375 | return [];
376 | }
377 |
378 | if ($or) {
379 | return array_filter($data, function($row) use ($column, $values) {
380 | foreach ($values as $value) {
381 | if (stripos($row[$column], $value) !== false) {
382 | return true;
383 | }
384 | }
385 | return false;
386 | });
387 | }
388 |
389 | return array_filter($data, function($row) use ($column, $values) {
390 | foreach ($values as $value) {
391 | if (stripos($row[$column], $value) === false) {
392 | return false;
393 | }
394 | }
395 | return true;
396 | });
397 | }
398 |
399 | /**
400 | * Applies 'CALLBACK' condition.
401 | * @param array $data data to be filtered.
402 | * @param string $operator operator.
403 | * @param array $operands the only one operand is the PHP callback, which should be compatible with
404 | * `array_filter()` PHP function, e.g.:
405 | *
406 | * ```php
407 | * function ($row) {
408 | * //return bool whether row matches condition or not
409 | * }
410 | * ```
411 | *
412 | * @return array filtered data.
413 | * @throws InvalidParamException if wrong number of operands have been given.
414 | * @since 1.0.3
415 | */
416 | public function filterCallbackCondition(array $data, $operator, $operands)
417 | {
418 | if (count($operands) != 1) {
419 | throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
420 | }
421 | $callback = reset($operands);
422 | return array_filter($data, $callback);
423 | }
424 |
425 | /**
426 | * Applies comparison condition, e.g. `column operator value`.
427 | * @param array $data data to be filtered.
428 | * @param string $operator operator.
429 | * @param array $operands
430 | * @return array filtered data.
431 | * @throws InvalidParamException if wrong number of operands have been given or operator is not supported.
432 | * @since 1.0.4
433 | */
434 | public function filterSimpleCondition(array $data, $operator, $operands)
435 | {
436 | if (count($operands) !== 2) {
437 | throw new InvalidParamException("Operator '$operator' requires two operands.");
438 | }
439 |
440 | list($column, $value) = $operands;
441 |
442 | return array_filter($data, function($row) use ($operator, $column, $value) {
443 | switch ($operator) {
444 | case '=':
445 | case '==':
446 | return $row[$column] == $value;
447 | case '===':
448 | return $row[$column] === $value;
449 | case '!=':
450 | case '<>':
451 | return $row[$column] != $value;
452 | case '!==':
453 | return $row[$column] !== $value;
454 | case '>':
455 | return $row[$column] > $value;
456 | case '<':
457 | return $row[$column] < $value;
458 | case '>=':
459 | return $row[$column] >= $value;
460 | case '<=':
461 | return $row[$column] <= $value;
462 | default:
463 | throw new InvalidParamException("Operator '$operator' is not supported.");
464 | }
465 | });
466 | }
467 | }
--------------------------------------------------------------------------------