├── 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 | [![Latest Stable Version](https://poser.pugx.org/yii2tech/filedb/v/stable.png)](https://packagist.org/packages/yii2tech/filedb) 19 | [![Total Downloads](https://poser.pugx.org/yii2tech/filedb/downloads.png)](https://packagist.org/packages/yii2tech/filedb) 20 | [![Build Status](https://travis-ci.org/yii2tech/filedb.svg?branch=master)](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 | } --------------------------------------------------------------------------------