├── tests ├── db │ └── .gitkeep └── CollectionTest.php ├── .gitignore ├── src ├── Pipes │ ├── PipeInterface.php │ ├── MapperPipe.php │ ├── LimiterPipe.php │ ├── SorterPipe.php │ └── FilterPipe.php ├── Exceptions │ ├── InvalidJsonException.php │ ├── UndefinedMethodException.php │ └── DirectoryNotFoundException.php ├── DB.php ├── ArrayExtra.php ├── Query.php └── Collection.php ├── .travis.yml ├── phpunit.xml.dist ├── composer.json ├── LICENSE └── README.md /tests/db/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | -------------------------------------------------------------------------------- /src/Pipes/PipeInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests 5 | 6 | 7 | 8 | 9 | ./src 10 | 11 | 12 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emsifa/laci", 3 | "description": "PHP JSON flat file DBMS", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Muhammad Syifa", 9 | "email": "emsifa@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Emsifa\\Laci\\": "src/" 15 | } 16 | }, 17 | "require": { 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "4.*" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Pipes/MapperPipe.php: -------------------------------------------------------------------------------- 1 | mappers as $mapper) 15 | { 16 | $data = array_map($mapper, $data); 17 | } 18 | 19 | return $data; 20 | } 21 | 22 | public function add(Closure $mapper) 23 | { 24 | $this->mappers[] = $mapper; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Pipes/LimiterPipe.php: -------------------------------------------------------------------------------- 1 | limit = $limit; 16 | return $this; 17 | } 18 | 19 | public function setOffset($offset) 20 | { 21 | if (!is_null($offset)) { 22 | $this->offset = $offset; 23 | } 24 | return $this; 25 | } 26 | 27 | public function process(array $data) 28 | { 29 | $limit = (int) $this->limit ?: count($data); 30 | $offset = (int) $this->offset; 31 | return array_slice($data, $offset, $limit); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/DB.php: -------------------------------------------------------------------------------- 1 | $callback) { 22 | $collection->macro($name, $callback); 23 | } 24 | 25 | return $collection; 26 | } 27 | 28 | public static function macro($name, callable $callback) 29 | { 30 | static::$macros[$name] = $callback; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Muhammad Syifa 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/Pipes/SorterPipe.php: -------------------------------------------------------------------------------- 1 | value = $value; 16 | $this->ascending = strtolower($ascending); 17 | } 18 | 19 | public function process(array $data) 20 | { 21 | return $this->sort($data, $this->value, $this->ascending); 22 | } 23 | 24 | public function sort($array, $value, $ascending) 25 | { 26 | $values = array_map(function($row) use ($value) { 27 | return $value($row); 28 | }, $array); 29 | 30 | switch($ascending) { 31 | case 'asc': asort($values); break; 32 | case 'desc': arsort($values); break; 33 | } 34 | 35 | $keys = array_keys($values); 36 | 37 | $result = []; 38 | foreach($keys as $key) { 39 | $result[$key] = $array[$key]; 40 | } 41 | return $result; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Pipes/FilterPipe.php: -------------------------------------------------------------------------------- 1 | filters; 15 | return array_filter($data, function($row) use ($filters) { 16 | $result = true; 17 | foreach($filters as $i => $filter) { 18 | list($filter, $type) = $filter; 19 | switch($type) { 20 | case 'and': 21 | $result = ($result AND $filter($row)); 22 | break; 23 | case 'or': 24 | $result = ($result OR $filter($row)); 25 | break; 26 | default: 27 | throw new \InvalidArgumentException("Filter type must be 'AND' or 'OR'.", 1); 28 | } 29 | } 30 | return $result; 31 | }); 32 | } 33 | 34 | public function add(Closure $filter, $type = 'AND') 35 | { 36 | $this->filters[] = [$filter, strtolower($type)]; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/ArrayExtra.php: -------------------------------------------------------------------------------- 1 | items = $this->getArrayValue($items, 'Items must be array or ArrayExtra object'); 20 | } 21 | 22 | /** 23 | * Check if an item or items exist in an array using "dot" notation. 24 | * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/Arr.php#L81 25 | * 26 | * @param array $array 27 | * @param string|array $keys 28 | * @return bool 29 | */ 30 | public static function arrayHas(array $array, $key) 31 | { 32 | if (array_key_exists($key, $array)) { 33 | return true; 34 | } 35 | 36 | foreach (explode('.', $key) as $segment) { 37 | if (is_array($array) && array_key_exists($segment, $array)) { 38 | $array = $array[$segment]; 39 | } else { 40 | return false; 41 | } 42 | } 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * Get an item from an array using "dot" notation. 49 | * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/Arr.php#L246 50 | * 51 | * @param array $array 52 | * @param string $key 53 | * @return mixed 54 | */ 55 | public static function arrayGet(array $array, $key) 56 | { 57 | if (is_null($key)) { 58 | return $array; 59 | } 60 | 61 | if (array_key_exists($key, $array)) { 62 | return $array[$key]; 63 | } 64 | 65 | foreach (explode('.', $key) as $segment) { 66 | if (is_array($array) && array_key_exists($segment, $array)) { 67 | $array = $array[$segment]; 68 | } else { 69 | return null; 70 | } 71 | } 72 | 73 | return $array; 74 | } 75 | 76 | 77 | /** 78 | * Set an item on an array or object using dot notation. 79 | * Adapted from: https://github.com/illuminate/support/blob/v5.3.23/helpers.php#L437 80 | * 81 | * @param mixed $target 82 | * @param string|array $key 83 | * @param mixed $value 84 | * @param bool $overwrite 85 | * @return mixed 86 | */ 87 | public static function arraySet(&$target, $key, $value, $overwrite = true) 88 | { 89 | $segments = is_array($key) ? $key : explode('.', $key); 90 | 91 | if (($segment = array_shift($segments)) === '*') { 92 | if (! is_array($target)) { 93 | $target = []; 94 | } 95 | 96 | if ($segments) { 97 | foreach ($target as &$inner) { 98 | static::arraySet($inner, $segments, $value, $overwrite); 99 | } 100 | } elseif ($overwrite) { 101 | foreach ($target as &$inner) { 102 | $inner = $value; 103 | } 104 | } 105 | } elseif (is_array($target)) { 106 | if ($segments) { 107 | if (! array_key_exists($segment, $target)) { 108 | $target[$segment] = []; 109 | } 110 | 111 | static::arraySet($target[$segment], $segments, $value, $overwrite); 112 | } elseif ($overwrite || ! array_key_exists($segment, $target)) { 113 | $target[$segment] = $value; 114 | } 115 | } else { 116 | $target = []; 117 | 118 | if ($segments) { 119 | static::arraySet($target[$segment], $segments, $value, $overwrite); 120 | } elseif ($overwrite) { 121 | $target[$segment] = $value; 122 | } 123 | } 124 | 125 | return $target; 126 | } 127 | 128 | 129 | 130 | /** 131 | * Remove item in array 132 | * 133 | * @param array $array 134 | * @param string $key 135 | */ 136 | public static function arrayRemove(array &$array, $key) 137 | { 138 | $keys = explode('.', $key); 139 | 140 | while(count($keys) > 1) { 141 | $key = array_shift($keys); 142 | 143 | if(!isset($array[$key]) OR !is_array($array[$key])) { 144 | $array[$key] = array(); 145 | } 146 | 147 | $array =& $array[$key]; 148 | } 149 | 150 | unset($array[array_shift($keys)]); 151 | } 152 | 153 | public function merge($value) 154 | { 155 | $array = $this->getArrayValue($value, "Value is not mergeable."); 156 | 157 | foreach($value as $key => $val) { 158 | $this->items = static::arraySet($this->items, $key, $val, true); 159 | } 160 | } 161 | 162 | protected function getArrayValue($value, $message) 163 | { 164 | if (!is_array($value) AND false == $value instanceof ArrayExtra) { 165 | throw new \InvalidArgumentException($message); 166 | } 167 | 168 | return is_array($value)? $value : $value->toArray(); 169 | } 170 | 171 | public function toArray() 172 | { 173 | return $this->items; 174 | } 175 | 176 | public function offsetSet($key, $value) { 177 | $this->items = static::arraySet($this->items, $key, $value, true); 178 | } 179 | 180 | public function offsetExists($key) { 181 | return static::arrayHas($this->items, $key); 182 | } 183 | 184 | public function offsetUnset($key) { 185 | static::arrayRemove($this->items, $key); 186 | } 187 | 188 | public function offsetGet($key) { 189 | return static::arrayGet($this->items, $key); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | LaciDB - Flat File JSON DBMS 3 | ====================================== 4 | 5 | [![Build Status](https://img.shields.io/travis/emsifa/laci-db.svg?style=flat-square)](https://travis-ci.org/emsifa/laci-db) 6 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://doge.mit-license.org) 7 | 8 | ## Overview 9 | 10 | LaciDB adalah flat file DBMS dengan format penyimpanan berupa JSON. Karena format JSON, LaciDB bersifat *schemaless* seperti halnya NoSQL lainnya. Sebuah record dapat memiliki kolom yang berbeda-beda. 11 | 12 | Dalam LaciDB tidak ada istilah table, yang ada adalah collection. Collection pada LaciDB mewakili sebuah file yang menyimpan banyak records (dalam format JSON). 13 | 14 | Nama 'Laci' sendiri diambil karena fungsi dan prosesnya seperti laci pada meja/lemari. Laci pada meja/lemari umumnya tidak membutuhkan kunci (autentikasi), cukup buka > ambil sesuatu dan|atau taruh sesuatu > tutup. Pada LaciDB pun seperti itu, setiap query akan membuka file > eksekusi query (select|insert|update|delete) > file ditutup. Laci juga seperti yang kita ketahui adalah tempat untuk menaruh barang-barang kecil. Bukan barang-barang besar seperti gudang atau lemari. 15 | 16 | Untuk itu LaciDB bukan untuk: 17 | 18 | * Menyimpan database dengan ukuran yang besar. 19 | * Menyimpan database yang membutuhkan keamanan tingkat tinggi. 20 | 21 | LaciDB dibuat untuk: 22 | 23 | * Menangani data-data yang kecil seperti pengaturan, atau data-data kecil lain. 24 | * Untuk kalian yang menginginkan database portable yang mudah untuk diimport/export dan backup. 25 | * Untuk kalian yang menginginkan database yang mudah diedit sendiri tanpa menggunakan software khusus. Notepad pun bisa. 26 | 27 | ## Cara Kerja 28 | 29 | Cara kerja LaciDB pada dasarnya hanyalah mengalirkan array hasil `json_decode` kedalam 'pipa-pipa' yang berfungsi sebagai *filtering*, *mapping*, *sorting*, *limiting* sampai akhirnya hasilnya akan di eksekusi untuk diambil nilainya, diubah nilainya atau dibuang (baca: dihapus). 30 | 31 | Berikut penjelasan terkait prosesnya: 32 | 33 | ### Filtering 34 | 35 | Untuk melakukan filtering kamu dapat menggunakan method `where` dan `orWhere`. Ke2 method tersebut dapat menerima parameter `Closure` atau beberapa parameter `key, operator, value`. 36 | 37 | ### Mapping 38 | 39 | Mapping digunakan untuk membentuk nilai yang baru pada setiap record yang telah difilter. 40 | 41 | Berikut beberapa method untuk mapping record: 42 | 43 | #### `map(Closure $mapper)` 44 | 45 | Untuk mapping records pada collection yang telah difilter. 46 | 47 | #### `select(array $columns)` 48 | 49 | Mapping records untuk mengambil kolom-kolom tertentu saja. 50 | 51 | #### `withOne(Collection|Query $relation, $key, $otherKey, $operator, $thisKey)` 52 | 53 | Untuk mengambil relasi 1:1. 54 | 55 | #### `withMany(Collection|Query $relation, $key, $otherKey, $operator, $thisKey)` 56 | 57 | Untuk mengambil relasi 1:n. 58 | 59 | ### Sorting 60 | 61 | Sorting digunakan untuk mengurutkan data yang telah difilter dan dimapping. Untuk melakukan sorting kamu dapat menggunakan method `sortBy($key, $ascending)`. Parameter `$key` dapat berupa string key/kolom yang ingin diurutkan atau `Closure` jika ingin mengurutkan berdasarkan nilai yang dikomputasi terlebih dahulu. 62 | 63 | ### Limiting/Taking 64 | 65 | Setelah data selesai difilter, dimapping, dan disorting, kamu dapat memotong dan mengambil sebagian data dengan method `skip($offset)` atau `take($limit, $offset)`. 66 | 67 | ### Executing 68 | 69 | Setelah difilter, dimapping, disorting, dan disisihkan, langkah selanjutnya adalah ekseskusi hasilnya. 70 | 71 | Berikut beberapa method untuk executing: 72 | 73 | #### `get(array $columns = null)` 74 | 75 | Mengambil kumpulan records pada collection. Jika ingin mengambil kolom tertentu definisikan kolom kedalam array `$columns`. 76 | 77 | #### `first(array $columns = null)` 78 | 79 | Mengambil (sebuah) record pada collection. Jika ingin mengambil kolom tertentu definisikan kolom kedalam array `$columns`. 80 | 81 | #### `count()` 82 | 83 | Mengambil banyak data dari collection. 84 | 85 | #### `sum($key)` 86 | 87 | Mengambil total key tertentu pada collection. 88 | 89 | #### `avg($key)` 90 | 91 | Mengambil rata-rata key tertentu pada collection. 92 | 93 | #### `min($key)` 94 | 95 | Mengambil nilai terendah dari key tertentu pada collection. 96 | 97 | #### `max($key)` 98 | 99 | Mengambil nilai tertinggi dari key tertentu pada collection. 100 | 101 | #### `lists($key, $resultKey = null)` 102 | 103 | Mengumpulkan dan mengambil key tertentu kedalam array pada collection. 104 | 105 | #### `insert(array $data)` 106 | 107 | Insert data baru kedalam collection. 108 | 109 | #### `inserts(array $listData)` 110 | 111 | Insert beberapa data baru sekaligus kedalam collection. Note: `insert` dan `inserts` tidak dapat dilakukan setelah query di filter atau di mapping. 112 | 113 | #### `update(array $newData)` 114 | 115 | Mengupdate data pada records didalam collection yang difilter dan dimapping. 116 | 117 | #### `save()` 118 | 119 | Sama seperti update. Hanya saja `save` akan menyimpan record berdasarkan hasil mapping, bukan berdasarkan `$newData` seperti pada update. 120 | 121 | #### `delete()` 122 | 123 | Menghapus data pada collection yang difilter dan dimapping. 124 | 125 | #### `truncate()` 126 | 127 | Menghapus seluruh data. Tidak membutuhkan filtering dan mapping terlebih dahulu. 128 | 129 | ## Contoh 130 | 131 | #### Inisialisasi 132 | 133 | ```php 134 | use Emsifa\Laci\Collection; 135 | 136 | require 'vendor/autoload.php'; 137 | 138 | $collection = new Collection(__DIR__.'/users.json'); 139 | ``` 140 | 141 | #### Insert Data 142 | 143 | ```php 144 | $user = $collection->insert([ 145 | 'name' => 'John Doe', 146 | 'email' => 'johndoe@mail.com', 147 | 'password' => password_hash('password', PASSWORD_BCRYPT) 148 | ]); 149 | ``` 150 | 151 | `$user` akan berupa array seperti ini: 152 | 153 | ```php 154 | [ 155 | '_id' => '58745c13ad585', 156 | 'name' => 'John Doe', 157 | 'email' => 'johndoe@mail.com', 158 | 'password' => '$2y$10$eMF03850wE6uII7UeujyjOU5Q2XLWz0QEZ1A9yiKPjbo3sA4qYh1m' 159 | ] 160 | ``` 161 | 162 | > '_id' adalah `uniqid()` 163 | 164 | #### Find Single Record By ID 165 | 166 | ```php 167 | $user = $collection->find('58745c13ad585'); 168 | ``` 169 | 170 | #### Find One 171 | 172 | ```php 173 | $user = $collection->where('email', 'johndoe@mail.com')->first(); 174 | ``` 175 | 176 | #### Select All 177 | 178 | ```php 179 | $data = $collection->all(); 180 | ``` 181 | 182 | #### Update 183 | 184 | ```php 185 | $collection->where('email', 'johndoe@mail.com')->update([ 186 | 'name' => 'John', 187 | 'sex' => 'male' 188 | ]); 189 | ``` 190 | 191 | > Return value is count affected records 192 | 193 | #### Delete 194 | 195 | ```php 196 | $collection->where('email', 'johndoe@mail.com')->delete(); 197 | ``` 198 | 199 | > Return value is count affected records 200 | 201 | #### Multiple Inserts 202 | 203 | ```php 204 | $bookCollection = new Collection('db/books.json'); 205 | 206 | $bookCollection->inserts([ 207 | [ 208 | 'title' => 'Foobar', 209 | 'published_at' => '2016-02-23', 210 | 'author' => [ 211 | 'name' => 'John Doe', 212 | 'email' => 'johndoe@mail.com' 213 | ], 214 | 'star' => 3, 215 | 'views' => 100 216 | ], 217 | [ 218 | 'title' => 'Bazqux', 219 | 'published_at' => '2014-01-10', 220 | 'author' => [ 221 | 'name' => 'Jane Doe', 222 | 'email' => 'janedoe@mail.com' 223 | ], 224 | 'star' => 5, 225 | 'views' => 56 226 | ], 227 | [ 228 | 'title' => 'Lorem Ipsum', 229 | 'published_at' => '2013-05-12', 230 | 'author' => [ 231 | 'name' => 'Jane Doe', 232 | 'email' => 'janedoe@mail.com' 233 | ], 234 | 'star' => 4, 235 | 'views' => 96 236 | ], 237 | ]); 238 | 239 | ``` 240 | 241 | #### Find Where 242 | 243 | ```php 244 | // select * from books.json where author[name] = 'Jane Doe' 245 | $bookCollection->where('author.name', 'Jane Doe')->get(); 246 | 247 | // select * from books.json where star > 3 248 | $bookCollection->where('star', '>', 3)->get(); 249 | 250 | // select * from books.json where star > 3 AND author[name] = 'Jane Doe' 251 | $bookCollection->where('star', '>', 3)->where('author.name', 'Jane Doe')->get(); 252 | 253 | // select * from books.json where star > 3 OR author[name] = 'Jane Doe' 254 | $bookCollection->where('star', '>', 3)->orWhere('author.name', 'Jane Doe')->get(); 255 | 256 | // select * from books.json where (star > 3 OR author[name] = 'Jane Doe') 257 | $bookCollection->where(function($book) { 258 | return $book['star'] > 3 OR $book['author.name'] == 'Jane Doe'; 259 | })->get(); 260 | ``` 261 | 262 | > Operator can be '=', '<', '<=', '>', '>=', 'in', 'not in', 'between', 'match'. 263 | 264 | #### Mengambil Kolom/Key Tertentu 265 | 266 | ```php 267 | // select author, title from books.json where star > 3 268 | $bookCollection->where('star', '>', 3)->get(['author.name', 'title']); 269 | ``` 270 | 271 | #### Alias Kolom/Key 272 | 273 | ```php 274 | // select author[name] as author_name, title from books.json where star > 3 275 | $bookCollection->where('star', '>', 3)->get(['author.name:author_name', 'title']); 276 | ``` 277 | 278 | #### Mapping 279 | 280 | ```php 281 | $bookCollection->map(function($row) { 282 | $row['score'] = $row['star'] + $row['views']; 283 | return $row; 284 | }) 285 | ->sortBy('score', 'desc') 286 | ->get(); 287 | ``` 288 | 289 | #### Sorting 290 | 291 | ```php 292 | // select * from books.json order by star asc 293 | $bookCollection->sortBy('star')->get(); 294 | 295 | // select * from books.json order by star desc 296 | $bookCollection->sortBy('star', 'desc')->get(); 297 | 298 | // sorting calculated value 299 | $bookCollection->sortBy(function($row) { 300 | return $row['star'] + $row['views']; 301 | }, 'desc')->get(); 302 | ``` 303 | 304 | #### Limit & Offset 305 | 306 | ```php 307 | // select * from books.json offset 4 308 | $bookCollection->skip(4)->get(); 309 | 310 | // select * from books.json limit 10 offset 4 311 | $bookCollection->take(10, 4)->get(); 312 | ``` 313 | 314 | #### Join 315 | 316 | ```php 317 | $userCollection = new Collection('db/users.json'); 318 | $bookCollection = new Collection('db/books.json'); 319 | 320 | // get user with 'books' 321 | $userCollection->withMany($bookCollection, 'books', 'author.email', '=', 'email')->get(); 322 | 323 | // get books with 'user' 324 | $bookCollection->withOne($userCollection, 'user', 'email', '=', 'author.email')->get(); 325 | ``` 326 | 327 | #### Map & Save 328 | 329 | ```php 330 | $bookCollection->where('star', '>', 3)->map(function($row) { 331 | $row['star'] = $row['star'] += 2; 332 | return $row; 333 | })->save(); 334 | ``` 335 | 336 | #### Transaction 337 | 338 | ```php 339 | $bookCollection->begin(); 340 | 341 | try { 342 | 343 | // insert, update, delete, etc 344 | // will stored into variable (memory) 345 | 346 | $bookCollection->commit(); // until this 347 | 348 | } catch(Exception $e) { 349 | 350 | $bookCollection->rollback(); 351 | 352 | } 353 | ``` 354 | 355 | #### Macro Query 356 | 357 | Macro query memungkinkan kita menambahkan method baru kedalam instance `Emsifa\Laci\Collection` sehingga dapat kita gunakan berulang-ulang secara lebih fluent. 358 | 359 | Sebagai contoh kita ingin mengambil data user yang aktif, jika dengan cara biasa kita dapat melakukan query seperti ini: 360 | 361 | ```php 362 | $users->where('active', 1)->get(); 363 | ``` 364 | 365 | Cara seperti diatas jika digunakan berulang-ulang, terkadang kita lupa mengenali user aktif itu yang nilai `active`-nya `1`, atau `true`, atau `'yes'`, atau `'YES'`, atau `'yes'`, atau `'y'`, atau `'Y'`, atau `'Ya'`, atau `'ya'`, dsb? 366 | 367 | Jadi untuk mempermudahnya, kita dapat menggunakan macro sebagai berikut: 368 | 369 | ```php 370 | $users->macro('active', function ($query) { 371 | return $query->where('active', 1); 372 | }); 373 | ``` 374 | 375 | Sehingga kita dapat mengambil user aktif dengan cara seperti ini: 376 | 377 | ```php 378 | $users->active()->get(); 379 | ``` 380 | 381 | Tampak lebih praktis bukan? 382 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 28 | } 29 | 30 | public function getCollection() 31 | { 32 | return $this->collection; 33 | } 34 | 35 | public function setCollection(Collection $collection) 36 | { 37 | $this->collection = $collection; 38 | } 39 | 40 | public function where($filter) 41 | { 42 | $args = func_get_args(); 43 | array_unshift($args, 'AND'); 44 | call_user_func_array([$this, 'addWhere'], $args); 45 | return $this; 46 | } 47 | 48 | public function orWhere($filter) 49 | { 50 | $args = func_get_args(); 51 | array_unshift($args, 'OR'); 52 | call_user_func_array([$this, 'addWhere'], $args); 53 | return $this; 54 | } 55 | 56 | public function map(Closure $mapper) 57 | { 58 | $this->addMapper($mapper); 59 | return $this; 60 | } 61 | 62 | public function select(array $columns) 63 | { 64 | $resolvedColumns = []; 65 | foreach($columns as $column) { 66 | $exp = explode(':', $column); 67 | $col = $exp[0]; 68 | if (count($exp) > 1) { 69 | $keyAlias = $exp[1]; 70 | } else { 71 | $keyAlias = $exp[0]; 72 | } 73 | $resolvedColumns[$col] = $keyAlias; 74 | } 75 | 76 | $keyAliases = array_values($resolvedColumns); 77 | 78 | return $this->map(function($row) use ($resolvedColumns, $keyAliases) { 79 | foreach($resolvedColumns as $col => $keyAlias) { 80 | if (!isset($row[$keyAlias])) { 81 | $row[$keyAlias] = $row[$col]; 82 | } 83 | } 84 | 85 | foreach($row->toArray() as $col => $value) { 86 | if (!in_array($col, $keyAliases)) { 87 | unset($row[$col]); 88 | } 89 | } 90 | 91 | return $row; 92 | }); 93 | } 94 | 95 | public function withOne($relation, $as, $otherKey, $operator = '=', $thisKey = '_id') 96 | { 97 | if (false == $relation instanceof Query AND false == $relation instanceof Collection) { 98 | throw new \InvalidArgumentException("Relation must be instanceof Query or Collection", 1); 99 | } 100 | return $this->map(function($row) use ($relation, $as, $otherKey, $operator, $thisKey) { 101 | $otherData = $relation->where($otherKey, $operator, $row[$thisKey])->first(); 102 | $row[$as] = $otherData; 103 | return $row; 104 | }); 105 | } 106 | 107 | public function withMany($relation, $as, $otherKey, $operator = '=', $thisKey = '_id') 108 | { 109 | if (false !== $relation instanceof Query AND false == $relation instanceof Collection) { 110 | throw new \InvalidArgumentException("Relation must be instanceof Query or Collection", 1); 111 | } 112 | return $this->map(function($row) use ($relation, $as, $otherKey, $operator, $thisKey) { 113 | $otherData = $relation->where($otherKey, $operator, $row[$thisKey])->get(); 114 | $row[$as] = $otherData; 115 | return $row; 116 | }); 117 | } 118 | 119 | public function sortBy($key, $asc = 'asc') 120 | { 121 | $asc = strtolower($asc); 122 | if (!in_array($asc, ['asc', 'desc'])) { 123 | throw new \InvalidArgumentException("Ascending must be 'asc' or 'desc'", 1); 124 | } 125 | 126 | if ($key instanceof Closure) { 127 | $value = $key; 128 | } else { 129 | $value = function($row) use ($key) { 130 | return $row[$key]; 131 | }; 132 | } 133 | 134 | $this->addSorter(function($row) use ($value) { 135 | return $value(new ArrayExtra($row)); 136 | }, $asc); 137 | return $this; 138 | } 139 | 140 | public function skip($offset) 141 | { 142 | $this->getLimiter()->setOffset($offset); 143 | return $this; 144 | } 145 | 146 | public function take($limit, $offset = null) 147 | { 148 | $this->getLimiter()->setLimit($limit)->setOffset($offset); 149 | return $this; 150 | } 151 | 152 | public function get(array $select = []) 153 | { 154 | if (!empty($select)) { 155 | $this->select($select); 156 | } 157 | return $this->execute(self::TYPE_GET); 158 | } 159 | 160 | public function first(array $select = array()) 161 | { 162 | $data = $this->take(1)->get($select); 163 | return array_shift($data); 164 | } 165 | 166 | public function update(array $new) 167 | { 168 | return $this->execute(self::TYPE_UPDATE, $new); 169 | } 170 | 171 | public function delete() 172 | { 173 | return $this->execute(self::TYPE_DELETE); 174 | } 175 | 176 | public function save() 177 | { 178 | return $this->execute(self::TYPE_SAVE); 179 | } 180 | 181 | public function count() 182 | { 183 | return count($this->get()); 184 | } 185 | 186 | public function sum($key) 187 | { 188 | $sum = 0; 189 | foreach($this->get() as $data) { 190 | $data = new ArrayExtra($data); 191 | $sum += $data[$key]; 192 | } 193 | return $sum; 194 | } 195 | 196 | public function avg($key) 197 | { 198 | $sum = 0; 199 | $count = 0; 200 | foreach($this->get() as $data) { 201 | $data = new ArrayExtra($data); 202 | $sum += $data[$key]; 203 | $count++; 204 | } 205 | return $sum / $count; 206 | } 207 | 208 | public function lists($key, $resultKey = null) 209 | { 210 | $result = []; 211 | foreach($this->get() as $i => $data) { 212 | $data = new ArrayExtra($data); 213 | $k = $resultKey ? $data[$resultKey] : $i; 214 | $result[$k] = $data[$key]; 215 | } 216 | return $result; 217 | } 218 | 219 | public function pluck($key, $resultKey = null) 220 | { 221 | return $this->lists($key, $resultKey); 222 | } 223 | 224 | public function min($key) 225 | { 226 | return min($this->lists($key)); 227 | } 228 | 229 | public function max($key) 230 | { 231 | return max($this->lists($key)); 232 | } 233 | 234 | public function getPipes() 235 | { 236 | return $this->pipes; 237 | } 238 | 239 | protected function execute($type, $arg = null) 240 | { 241 | return $this->getCollection()->execute($this, $type, $arg); 242 | } 243 | 244 | protected function addWhere($type, $filter) 245 | { 246 | if ($filter instanceof Closure) { 247 | return $this->addFilter($filter, $type); 248 | } 249 | 250 | $args = func_get_args(); 251 | $key = $args[1]; 252 | if (count($args) > 3) { 253 | $operator = $args[2]; 254 | $value = $args[3]; 255 | } else { 256 | $operator = '='; 257 | $value = $args[2]; 258 | } 259 | 260 | switch($operator) { 261 | case '=': 262 | $filter = function($row) use ($key, $value) { 263 | return $row[$key] == $value; 264 | }; 265 | break; 266 | case '>': 267 | $filter = function($row) use ($key, $value) { 268 | return $row[$key] > $value; 269 | }; 270 | break; 271 | case '>=': 272 | $filter = function($row) use ($key, $value) { 273 | return $row[$key] >= $value; 274 | }; 275 | break; 276 | case '<': 277 | $filter = function($row) use ($key, $value) { 278 | return $row[$key] < $value; 279 | }; 280 | break; 281 | case '<=': 282 | $filter = function($row) use ($key, $value) { 283 | return $row[$key] <= $value; 284 | }; 285 | break; 286 | case 'in': 287 | $filter = function($row) use ($key, $value) { 288 | return in_array($row[$key], (array) $value); 289 | }; 290 | break; 291 | case 'not in': 292 | $filter = function($row) use ($key, $value) { 293 | return !in_array($row[$key], (array) $value); 294 | }; 295 | break; 296 | case 'match': 297 | $filter = function($row) use ($key, $value) { 298 | return (bool) preg_match($value, $row[$key]); 299 | }; 300 | break; 301 | case 'between': 302 | if (!is_array($value) OR count($value) < 2) { 303 | throw new \InvalidArgumentException("Query between need exactly 2 items in array"); 304 | } 305 | $filter = function($row) use ($key, $value) { 306 | $v = $row[$key]; 307 | return $v >= $value[0] AND $v <= $value[1]; 308 | }; 309 | break; 310 | } 311 | 312 | if (!$filter) { 313 | throw new \InvalidArgumentException("Operator {$operator} is not available"); 314 | } 315 | 316 | $this->addFilter($filter, $type); 317 | } 318 | 319 | protected function addFilter(Closure $filter, $type = 'AND') 320 | { 321 | $lastPipe = $this->getLastPipe(); 322 | if (false == $lastPipe instanceof FilterPipe) { 323 | $pipe = new FilterPipe($this); 324 | $this->addPipe($pipe); 325 | } else { 326 | $pipe = $lastPipe; 327 | } 328 | 329 | $newFilter = function($row) use ($filter) { 330 | $row = new ArrayExtra($row); 331 | return $filter($row); 332 | }; 333 | 334 | $pipe->add($newFilter, $type); 335 | } 336 | 337 | protected function addMapper(Closure $mapper) 338 | { 339 | $lastPipe = $this->getLastPipe(); 340 | if (false == $lastPipe instanceof MapperPipe) { 341 | $pipe = new MapperPipe($this); 342 | $this->addPipe($pipe); 343 | } else { 344 | $pipe = $lastPipe; 345 | } 346 | 347 | $keyId = $this->getCollection()->getKeyId(); 348 | $keyOldId = $this->getCollection()->getKeyOldId(); 349 | 350 | $newMapper = function($row) use ($mapper, $keyId, $keyOldId) { 351 | $row = new ArrayExtra($row); 352 | $result = $mapper($row); 353 | 354 | if (is_array($result)) { 355 | $new = $result; 356 | } elseif($result instanceof ArrayExtra) { 357 | $new = $result->toArray(); 358 | } else { 359 | $new = null; 360 | } 361 | 362 | if (is_array($new) AND isset($new[$keyId])) { 363 | if ($row[$keyId] != $new[$keyId]) { 364 | $new[$keyOldId] = $row[$keyId]; 365 | } 366 | } 367 | 368 | return $new; 369 | }; 370 | 371 | $pipe->add($newMapper); 372 | } 373 | 374 | protected function addSorter(Closure $value, $asc) 375 | { 376 | $pipe = new SorterPipe($value, $asc); 377 | $this->addPipe($pipe); 378 | } 379 | 380 | protected function getLimiter() 381 | { 382 | $lastPipe = $this->getLastPipe(); 383 | if (false == $lastPipe instanceof LimiterPipe) { 384 | $limiter = new LimiterPipe; 385 | $this->addPipe($limiter); 386 | } else { 387 | $limiter = $lastPipe; 388 | } 389 | 390 | return $limiter; 391 | } 392 | 393 | protected function addPipe(PipeInterface $pipe) 394 | { 395 | $this->pipes[] = $pipe; 396 | } 397 | 398 | protected function getLastPipe() 399 | { 400 | return !empty($this->pipes)? $this->pipes[count($this->pipes) - 1] : null; 401 | } 402 | 403 | public function __call($method, $args) 404 | { 405 | $macro = $this->collection->getMacro($method); 406 | 407 | if ($macro) { 408 | return call_user_func_array($macro, array_merge([$this], $args)); 409 | } else { 410 | throw new UndefinedMethodException("Undefined method or macro '{$method}'."); 411 | } 412 | } 413 | 414 | } -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | filepath = $filepath; 39 | $this->options = array_merge([ 40 | 'save_format' => JSON_PRETTY_PRINT, 41 | 'key_prefix' => '', 42 | 'more_entropy' => false, 43 | ], $options); 44 | } 45 | 46 | public function macro($name, callable $callback) 47 | { 48 | $this->macros[$name] = $callback; 49 | } 50 | 51 | public function hasMacro($name) 52 | { 53 | return array_key_exists($name, $this->macros); 54 | } 55 | 56 | public function getMacro($name) 57 | { 58 | return $this->hasMacro($name) ? $this->macros[$name] : null; 59 | } 60 | 61 | public function getKeyId() 62 | { 63 | return static::KEY_ID; 64 | } 65 | 66 | public function getKeyOldId() 67 | { 68 | return static::KEY_OLD_ID; 69 | } 70 | 71 | public function isModeTransaction() 72 | { 73 | return true === $this->transactionMode; 74 | } 75 | 76 | public function begin() 77 | { 78 | $this->transactionMode = true; 79 | } 80 | 81 | public function commit() 82 | { 83 | $this->transactionMode = false; 84 | return $this->save($this->transactionData); 85 | } 86 | 87 | public function rollback() 88 | { 89 | $this->transactionMode = false; 90 | $this->transactionData = null; 91 | } 92 | 93 | public function truncate() 94 | { 95 | return $this->persists([]); 96 | } 97 | 98 | public function on($event, callable $callback) 99 | { 100 | if (!isset($this->events[$event])) { 101 | $this->events[$event] = []; 102 | } 103 | 104 | $this->events[$event][] = $callback; 105 | } 106 | 107 | protected function trigger($event, array &$args) 108 | { 109 | $events = isset($this->events[$event])? $this->events[$event] : []; 110 | foreach($events as $callback) { 111 | call_user_func_array($callback, $args); 112 | } 113 | } 114 | 115 | public function loadData() 116 | { 117 | if ($this->isModeTransaction() AND !empty($this->transactionData)) { 118 | return $this->transactionData; 119 | } 120 | 121 | if (!file_exists($this->filepath)) { 122 | $data = []; 123 | } else { 124 | $content = file_get_contents($this->filepath); 125 | $data = json_decode($content, true); 126 | if (is_null($data)) { 127 | throw new InvalidJsonException("Failed to load data. File '{$this->filepath}' contain invalid JSON format."); 128 | } 129 | } 130 | 131 | return $data; 132 | } 133 | 134 | public function setResolver(callable $resolver) 135 | { 136 | $this->resolver = $resolver; 137 | } 138 | 139 | public function getResolver() 140 | { 141 | return $this->resolver; 142 | } 143 | 144 | public function query() 145 | { 146 | return new Query($this); 147 | } 148 | 149 | public function where($key) 150 | { 151 | return call_user_func_array([$this->query(), 'where'], func_get_args()); 152 | } 153 | 154 | public function filter(Closure $closure) 155 | { 156 | return $this->query()->filter($closure); 157 | } 158 | 159 | public function map(Closure $mapper) 160 | { 161 | return $this->query()->map($mapper); 162 | } 163 | 164 | public function sortBy($key, $asc = 'asc') 165 | { 166 | return $this->query()->sortBy($key, $asc); 167 | } 168 | 169 | public function sort(Closure $value) 170 | { 171 | return $this->query()->sort($value); 172 | } 173 | 174 | public function skip($offset) 175 | { 176 | return $this->query()->skip($offset); 177 | } 178 | 179 | public function take($limit, $offset = 0) 180 | { 181 | return $this->query()->take($limit, $offset); 182 | } 183 | 184 | public function all() 185 | { 186 | return array_values($this->loadData()); 187 | } 188 | 189 | public function find($id) 190 | { 191 | $data = $this->loadData(); 192 | return isset($data[$id])? $data[$id] : null; 193 | } 194 | 195 | public function lists($key, $resultKey = null) 196 | { 197 | return $this->query()->lists($key, $resultKey); 198 | } 199 | 200 | public function sum($key) 201 | { 202 | return $this->query()->sum($key); 203 | } 204 | 205 | public function count() 206 | { 207 | return $this->query()->count(); 208 | } 209 | 210 | public function avg($key) 211 | { 212 | return $this->query()->avg($key); 213 | } 214 | 215 | public function min($key) 216 | { 217 | return $this->query()->min($key); 218 | } 219 | 220 | public function max($key) 221 | { 222 | return $this->query()->max($key); 223 | } 224 | 225 | public function insert(array $data) 226 | { 227 | return $this->execute($this->query(), Query::TYPE_INSERT, $data); 228 | } 229 | 230 | public function inserts(array $listData) 231 | { 232 | $this->begin(); 233 | foreach($listData as $data) { 234 | $this->insert($data); 235 | } 236 | return $this->commit(); 237 | } 238 | 239 | public function update(array $data) 240 | { 241 | return $this->query()->update(); 242 | } 243 | 244 | public function delete() 245 | { 246 | return $this->query()->delete(); 247 | } 248 | 249 | public function withOne($relation, $as, $otherKey, $operator = '=', $thisKey = null) 250 | { 251 | return $this->query()->withOne($relation, $as, $otherKey, $operator, $thisKey ?: static::KEY_ID); 252 | } 253 | 254 | public function withMany($relation, $as, $otherKey, $operator = '=', $thisKey = null) 255 | { 256 | return $this->query()->withMany($relation, $as, $otherKey, $operator, $thisKey ?: static::KEY_ID); 257 | } 258 | 259 | public function generateKey() 260 | { 261 | return uniqid($this->options['key_prefix'], (bool) $this->options['more_entropy']); 262 | } 263 | 264 | public function execute(Query $query, $type, $arg = null) 265 | { 266 | if ($query->getCollection() != $this) { 267 | throw new \InvalidArgumentException("Cannot execute query. Query is for different collection"); 268 | } 269 | 270 | switch ($type) { 271 | case Query::TYPE_GET: return $this->executeGet($query); 272 | case Query::TYPE_SAVE: return $this->executeSave($query); 273 | case Query::TYPE_INSERT: return $this->executeInsert($query, $arg); 274 | case Query::TYPE_UPDATE: return $this->executeUpdate($query, $arg); 275 | case Query::TYPE_DELETE: return $this->executeDelete($query); 276 | } 277 | } 278 | 279 | protected function executePipes(array $pipes) 280 | { 281 | $data = $this->loadData() ?: []; 282 | foreach($pipes as $pipe) { 283 | $data = $pipe->process($data); 284 | } 285 | return $data; 286 | } 287 | 288 | protected function executeInsert(Query $query, array $new) 289 | { 290 | $data = $this->loadData(); 291 | $key = isset($new[static::KEY_ID])? $new[static::KEY_ID] : $this->generateKey(); 292 | 293 | $newExtra = new ArrayExtra([]); 294 | $newExtra->merge($new); 295 | 296 | $args = [$newExtra]; 297 | $this->trigger(static::INSERTING, $args); 298 | $data[$key] = array_merge([ 299 | static::KEY_ID => $key 300 | ], $args[0]->toArray()); 301 | 302 | $success = $this->persists($data); 303 | 304 | $args = [$data[$key]]; 305 | $this->trigger(static::INSERTED, $args); 306 | 307 | $args = [$data]; 308 | $this->trigger(static::CHANGED, $args); 309 | 310 | return $success? $data[$key] : null; 311 | } 312 | 313 | protected function executeUpdate(Query $query, array $new) 314 | { 315 | $data = $this->loadData(); 316 | 317 | $args = [$query, $new]; 318 | $this->trigger(static::UPDATING, $args); 319 | 320 | $pipes = $query->getPipes(); 321 | $rows = $this->executePipes($pipes); 322 | $count = count($rows); 323 | if (0 == $count) { 324 | return true; 325 | } 326 | 327 | $updatedData = []; 328 | foreach($rows as $key => $row) { 329 | $record = new ArrayExtra($data[$key]); 330 | $record->merge($new); 331 | $data[$key] = $record->toArray(); 332 | 333 | if (isset($new[static::KEY_ID])) { 334 | $data[$new[static::KEY_ID]] = $data[$key]; 335 | unset($data[$key]); 336 | $key = $new[static::KEY_ID]; 337 | } 338 | $updatedData[$key] = $data[$key]; 339 | } 340 | 341 | $success = $this->persists($data); 342 | 343 | $args = [$updatedData]; 344 | $this->trigger(static::UPDATED, $args); 345 | 346 | $args = [$data]; 347 | $this->trigger(static::CHANGED, $args); 348 | 349 | return $success? $count : 0; 350 | } 351 | 352 | protected function executeDelete(Query $query) 353 | { 354 | $data = $this->loadData(); 355 | 356 | $args = [$query]; 357 | $this->trigger(static::DELETING, $args); 358 | 359 | $pipes = $query->getPipes(); 360 | $rows = $this->executePipes($pipes); 361 | $count = count($rows); 362 | if (0 == $count) { 363 | return true; 364 | } 365 | 366 | foreach($rows as $key => $row) { 367 | unset($data[$key]); 368 | } 369 | 370 | $success = $this->persists($data); 371 | 372 | $args = [$rows]; 373 | $this->trigger(static::DELETED, $args); 374 | 375 | $args = [$data]; 376 | $this->trigger(static::CHANGED, $args); 377 | 378 | return $success? $count : 0; 379 | } 380 | 381 | protected function executeGet(Query $query) 382 | { 383 | $pipes = $query->getPipes(); 384 | $data = $this->executePipes($pipes); 385 | return array_values($data); 386 | } 387 | 388 | protected function executeSave(Query $query) 389 | { 390 | $data = $this->loadData(); 391 | $pipes = $query->getPipes(); 392 | $processed = $this->executePipes($pipes); 393 | $count = count($processed); 394 | 395 | foreach($processed as $key => $row) { 396 | // update ID if there is '_old' key 397 | if (isset($row[static::KEY_OLD_ID])) { 398 | unset($data[$row[static::KEY_OLD_ID]]); 399 | } 400 | // keep ID if there is no '_id' 401 | if (!isset($row[static::KEY_ID])) { 402 | $row[static::KEY_ID] = $key; 403 | } 404 | $data[$key] = $row; 405 | } 406 | 407 | $success = $this->persists($data); 408 | 409 | return $success? $count : 0; 410 | } 411 | 412 | public function persists(array $data) 413 | { 414 | if ($this->resolver) { 415 | $data = array_map($this->getResolver(), $data); 416 | } 417 | 418 | return $this->save($data); 419 | } 420 | 421 | protected function save(array $data) 422 | { 423 | if ($this->isModeTransaction()) { 424 | $this->transactionData = $data; 425 | return true; 426 | } else { 427 | if (empty($data)) { 428 | $data = new \stdClass; 429 | } 430 | 431 | $json = json_encode($data, $this->options['save_format']); 432 | 433 | $filepath = $this->filepath; 434 | $pathinfo = pathinfo($filepath); 435 | $dir = $pathinfo['dirname']; 436 | if (!is_dir($dir)) { 437 | throw new DirectoryNotFoundException("Cannot save database. Directory {$dir} not found or it is not directory."); 438 | } 439 | 440 | return file_put_contents($filepath, $json, LOCK_EX); 441 | } 442 | } 443 | 444 | 445 | public function __call($method, $args) 446 | { 447 | $macro = $this->getMacro($method); 448 | 449 | if ($macro) { 450 | return call_user_func_array($macro, array_merge([$this->query()], $args)); 451 | } else { 452 | throw new UndefinedMethodException("Undefined method or macro '{$method}'."); 453 | } 454 | } 455 | 456 | } 457 | -------------------------------------------------------------------------------- /tests/CollectionTest.php: -------------------------------------------------------------------------------- 1 | [ 14 | "_id" => "58745c13ad585", 15 | "email" => "a@site.com", 16 | "name" => "A", 17 | "score" => 80 18 | ], 19 | "58745c19b4c51" => [ 20 | "_id" => "58745c19b4c51", 21 | "email" => "b@site.com", 22 | "name" => "B", 23 | "score" => 76 24 | ], 25 | "58745c1ef0b13" => [ 26 | "_id" => "58745c1ef0b13", 27 | "email" => "c@site.com", 28 | "name" => "C", 29 | "score" => 95 30 | ] 31 | ]; 32 | 33 | public function setUp() 34 | { 35 | $this->filepath = __DIR__.'/db/data.json'; 36 | // initialize data 37 | file_put_contents($this->filepath, json_encode($this->dummyData)); 38 | 39 | $this->db = new Collection($this->filepath); 40 | } 41 | 42 | public function testAll() 43 | { 44 | $result = $this->db->all(); 45 | $this->assertEquals($result, array_values($this->dummyData)); 46 | } 47 | 48 | public function testFind() 49 | { 50 | $result = $this->db->find('58745c19b4c51'); 51 | $this->assertEquals($result, [ 52 | "_id" => "58745c19b4c51", 53 | "email" => "b@site.com", 54 | "name" => "B", 55 | "score" => 76 56 | ]); 57 | } 58 | 59 | public function testFirst() 60 | { 61 | $result = $this->db->query()->first(); 62 | $this->assertEquals($result, [ 63 | "_id" => "58745c13ad585", 64 | "email" => "a@site.com", 65 | "name" => "A", 66 | "score" => 80 67 | ]); 68 | } 69 | 70 | public function testGetAll() 71 | { 72 | $this->assertEquals($this->db->query()->get(), array_values($this->dummyData)); 73 | } 74 | 75 | public function testFilter() 76 | { 77 | $result = $this->db->where(function($row) { 78 | return $row['score'] > 90; 79 | })->get(); 80 | 81 | $this->assertEquals($result, [ 82 | [ 83 | "_id" => "58745c1ef0b13", 84 | "email" => "c@site.com", 85 | "name" => "C", 86 | "score" => 95 87 | ] 88 | ]); 89 | } 90 | 91 | public function testMap() 92 | { 93 | $result = $this->db->map(function($row) { 94 | return [ 95 | 'x' => $row['score'] 96 | ]; 97 | })->get(); 98 | 99 | $this->assertEquals($result, [ 100 | ["x" => 80], 101 | ["x" => 76], 102 | ["x" => 95], 103 | ]); 104 | } 105 | 106 | public function testGetSomeColumns() 107 | { 108 | $result = $this->db->query()->get(['email', 'name']); 109 | $this->assertEquals($result, [ 110 | [ 111 | "email" => "a@site.com", 112 | "name" => "A", 113 | ], 114 | [ 115 | "email" => "b@site.com", 116 | "name" => "B", 117 | ], 118 | [ 119 | "email" => "c@site.com", 120 | "name" => "C", 121 | ] 122 | ]); 123 | } 124 | 125 | public function testSortAscending() 126 | { 127 | $result = $this->db->query()->sortBy('score', 'asc')->get(); 128 | $this->assertEquals($result, [ 129 | [ 130 | "_id" => "58745c19b4c51", 131 | "email" => "b@site.com", 132 | "name" => "B", 133 | "score" => 76 134 | ], 135 | [ 136 | "_id" => "58745c13ad585", 137 | "email" => "a@site.com", 138 | "name" => "A", 139 | "score" => 80 140 | ], 141 | [ 142 | "_id" => "58745c1ef0b13", 143 | "email" => "c@site.com", 144 | "name" => "C", 145 | "score" => 95 146 | ] 147 | ]); 148 | } 149 | 150 | public function testSortDescending() 151 | { 152 | $result = $this->db->query()->sortBy('score', 'desc')->get(); 153 | $this->assertEquals($result, [ 154 | [ 155 | "_id" => "58745c1ef0b13", 156 | "email" => "c@site.com", 157 | "name" => "C", 158 | "score" => 95 159 | ], 160 | [ 161 | "_id" => "58745c13ad585", 162 | "email" => "a@site.com", 163 | "name" => "A", 164 | "score" => 80 165 | ], 166 | [ 167 | "_id" => "58745c19b4c51", 168 | "email" => "b@site.com", 169 | "name" => "B", 170 | "score" => 76 171 | ] 172 | ]); 173 | } 174 | 175 | public function testSkip() 176 | { 177 | $result = $this->db->query()->skip(1)->get(); 178 | $this->assertEquals($result, [ 179 | [ 180 | "_id" => "58745c19b4c51", 181 | "email" => "b@site.com", 182 | "name" => "B", 183 | "score" => 76 184 | ], 185 | [ 186 | "_id" => "58745c1ef0b13", 187 | "email" => "c@site.com", 188 | "name" => "C", 189 | "score" => 95 190 | ] 191 | ]); 192 | } 193 | 194 | public function testTake() 195 | { 196 | $result = $this->db->query()->take(1, 1)->get(); 197 | $this->assertEquals($result, [ 198 | [ 199 | "_id" => "58745c19b4c51", 200 | "email" => "b@site.com", 201 | "name" => "B", 202 | "score" => 76 203 | ] 204 | ]); 205 | } 206 | 207 | public function testCount() 208 | { 209 | $this->assertEquals($this->db->count(), 3); 210 | } 211 | 212 | public function testSum() 213 | { 214 | $this->assertEquals($this->db->sum('score'), 76+80+95); 215 | } 216 | 217 | public function testAvg() 218 | { 219 | $this->assertEquals($this->db->avg('score'), (76+80+95)/3); 220 | } 221 | 222 | public function testMin() 223 | { 224 | $this->assertEquals($this->db->min('score'), 76); 225 | } 226 | 227 | public function testMax() 228 | { 229 | $this->assertEquals($this->db->max('score'), 95); 230 | } 231 | 232 | public function testLists() 233 | { 234 | $this->assertEquals($this->db->lists('score'), [80, 76, 95]); 235 | } 236 | 237 | public function testListsWithKey() 238 | { 239 | $result = $this->db->lists('score', 'email'); 240 | $this->assertEquals($result, [ 241 | 'a@site.com' => 80, 242 | 'b@site.com' => 76, 243 | 'c@site.com' => 95 244 | ]); 245 | } 246 | 247 | public function testGetWhereEquals() 248 | { 249 | $result = $this->db->where('name', 'C')->get(); 250 | $this->assertEquals($result, [ 251 | [ 252 | "_id" => "58745c1ef0b13", 253 | "email" => "c@site.com", 254 | "name" => "C", 255 | "score" => 95 256 | ] 257 | ]); 258 | } 259 | 260 | public function testGetOrWhere() 261 | { 262 | $result = $this->db->where('name', 'C')->orWhere('name', 'B')->get(); 263 | $this->assertEquals($result, [ 264 | [ 265 | "_id" => "58745c19b4c51", 266 | "email" => "b@site.com", 267 | "name" => "B", 268 | "score" => 76 269 | ], 270 | [ 271 | "_id" => "58745c1ef0b13", 272 | "email" => "c@site.com", 273 | "name" => "C", 274 | "score" => 95 275 | ] 276 | ]); 277 | } 278 | 279 | public function testGetWhereBiggerThan() 280 | { 281 | $result = $this->db->where('score', '>', 80)->get(); 282 | $this->assertEquals($result, [ 283 | [ 284 | "_id" => "58745c1ef0b13", 285 | "email" => "c@site.com", 286 | "name" => "C", 287 | "score" => 95 288 | ] 289 | ]); 290 | } 291 | 292 | public function testGetWhereBiggerThanEquals() 293 | { 294 | $result = $this->db->where('score', '>=', 80)->get(); 295 | $this->assertEquals($result, [ 296 | [ 297 | "_id" => "58745c13ad585", 298 | "email" => "a@site.com", 299 | "name" => "A", 300 | "score" => 80 301 | ], 302 | [ 303 | "_id" => "58745c1ef0b13", 304 | "email" => "c@site.com", 305 | "name" => "C", 306 | "score" => 95 307 | ] 308 | ]); 309 | } 310 | 311 | public function testGetWhereLowerThan() 312 | { 313 | $result = $this->db->where('score', '<', 80)->get(); 314 | $this->assertEquals($result, [ 315 | [ 316 | "_id" => "58745c19b4c51", 317 | "email" => "b@site.com", 318 | "name" => "B", 319 | "score" => 76 320 | ] 321 | ]); 322 | } 323 | 324 | public function testGetWhereLowerThanEquals() 325 | { 326 | $result = $this->db->where('score', '<=', 80)->get(); 327 | $this->assertEquals($result, [ 328 | [ 329 | "_id" => "58745c13ad585", 330 | "email" => "a@site.com", 331 | "name" => "A", 332 | "score" => 80 333 | ], 334 | [ 335 | "_id" => "58745c19b4c51", 336 | "email" => "b@site.com", 337 | "name" => "B", 338 | "score" => 76 339 | ] 340 | ]); 341 | } 342 | 343 | public function testGetWhereIn() 344 | { 345 | $result = $this->db->where('score', 'in', [80])->get(); 346 | $this->assertEquals($result, [ 347 | [ 348 | "_id" => "58745c13ad585", 349 | "email" => "a@site.com", 350 | "name" => "A", 351 | "score" => 80 352 | ] 353 | ]); 354 | } 355 | 356 | public function testGetWhereNotIn() 357 | { 358 | $result = $this->db->where('score', 'not in', [80])->get(); 359 | $this->assertEquals($result, [ 360 | [ 361 | "_id" => "58745c19b4c51", 362 | "email" => "b@site.com", 363 | "name" => "B", 364 | "score" => 76 365 | ], 366 | [ 367 | "_id" => "58745c1ef0b13", 368 | "email" => "c@site.com", 369 | "name" => "C", 370 | "score" => 95 371 | ] 372 | ]); 373 | } 374 | 375 | public function testGetWhereMatch() 376 | { 377 | $result = $this->db->where('email', 'match', '/^b@/')->get(); 378 | $this->assertEquals($result, [ 379 | [ 380 | "_id" => "58745c19b4c51", 381 | "email" => "b@site.com", 382 | "name" => "B", 383 | "score" => 76 384 | ] 385 | ]); 386 | } 387 | 388 | public function testGetWhereBetween() 389 | { 390 | $result = $this->db->where('score', 'between', [80, 95])->get(); 391 | $this->assertEquals($result, [ 392 | [ 393 | "_id" => "58745c13ad585", 394 | "email" => "a@site.com", 395 | "name" => "A", 396 | "score" => 80 397 | ], 398 | [ 399 | "_id" => "58745c1ef0b13", 400 | "email" => "c@site.com", 401 | "name" => "C", 402 | "score" => 95 403 | ] 404 | ]); 405 | } 406 | 407 | public function testInsert() 408 | { 409 | $this->db->insert([ 410 | 'test' => 'foo' 411 | ]); 412 | 413 | $this->assertEquals($this->db->count(), 4); 414 | $data = $this->db->where('test', 'foo')->first(); 415 | $this->assertEquals(array_keys($data), ['_id', 'test']); 416 | $this->assertEquals($data['test'], 'foo'); 417 | } 418 | 419 | public function testInserts() 420 | { 421 | $this->db->inserts([ 422 | ['test' => 'foo'], 423 | ['test' => 'bar'], 424 | ['test' => 'baz'] 425 | ]); 426 | 427 | $this->assertEquals($this->db->count(), 6); 428 | } 429 | 430 | public function testUpdate() 431 | { 432 | $this->db->where('score', '>=', 80)->update([ 433 | 'score' => 90 434 | ]); 435 | 436 | $this->assertEquals($this->db->all(), [ 437 | [ 438 | "_id" => "58745c13ad585", 439 | "email" => "a@site.com", 440 | "name" => "A", 441 | "score" => 90 442 | ], 443 | [ 444 | "_id" => "58745c19b4c51", 445 | "email" => "b@site.com", 446 | "name" => "B", 447 | "score" => 76 448 | ], 449 | [ 450 | "_id" => "58745c1ef0b13", 451 | "email" => "c@site.com", 452 | "name" => "C", 453 | "score" => 90 454 | ], 455 | ]); 456 | } 457 | 458 | public function testUpdateWithFilterMapAndSave() 459 | { 460 | $this->db->where('score', '>=', 80)->map(function($row) { 461 | return [ 462 | 'x' => $row['score'] 463 | ]; 464 | })->save(); 465 | 466 | $this->assertEquals($this->db->all(), [ 467 | [ 468 | "_id" => "58745c13ad585", 469 | "x" => 80 470 | ], 471 | [ 472 | "_id" => "58745c19b4c51", 473 | "email" => "b@site.com", 474 | "name" => "B", 475 | "score" => 76 476 | ], 477 | [ 478 | "_id" => "58745c1ef0b13", 479 | "x" => 95 480 | ], 481 | ]); 482 | } 483 | 484 | public function testDelete() 485 | { 486 | $this->db->where('score', '>=', 80)->delete(); 487 | $this->assertEquals($this->db->all(), [ 488 | [ 489 | "_id" => "58745c19b4c51", 490 | "email" => "b@site.com", 491 | "name" => "B", 492 | "score" => 76 493 | ] 494 | ]); 495 | } 496 | 497 | public function testWithOne() 498 | { 499 | $result = $this->db->withOne($this->db, 'other', 'email', '=', 'email')->first(); 500 | $this->assertEquals($result, [ 501 | "_id" => "58745c13ad585", 502 | "email" => "a@site.com", 503 | "name" => "A", 504 | "score" => 80, 505 | 'other' => [ 506 | "_id" => "58745c13ad585", 507 | "email" => "a@site.com", 508 | "name" => "A", 509 | "score" => 80 510 | ], 511 | ]); 512 | } 513 | 514 | public function testWithMany() 515 | { 516 | $result = $this->db->withMany($this->db, 'other', 'email', '=', 'email')->first(); 517 | $this->assertEquals($result, [ 518 | "_id" => "58745c13ad585", 519 | "email" => "a@site.com", 520 | "name" => "A", 521 | "score" => 80, 522 | 'other' => [ 523 | [ 524 | "_id" => "58745c13ad585", 525 | "email" => "a@site.com", 526 | "name" => "A", 527 | "score" => 80 528 | ] 529 | ], 530 | ]); 531 | } 532 | 533 | public function testSelectAs() 534 | { 535 | $result = $this->db->query()->withOne($this->db, 'other', 'email', '=', 'email')->first([ 536 | 'email', 537 | 'other.email:other_email' 538 | ]); 539 | 540 | $this->assertEquals($result, [ 541 | "email" => "a@site.com", 542 | "other_email" => "a@site.com", 543 | ]); 544 | } 545 | 546 | public function testMoreEntropy() 547 | { 548 | $db = new Collection($this->filepath, [ 549 | 'more_entropy' => true 550 | ]); 551 | 552 | $data = $db->insert([ 553 | 'label' => 'Test more entropy' 554 | ]); 555 | 556 | $this->assertEquals(strlen($data['_id']), 23); 557 | } 558 | 559 | public function testKeyPrefix() 560 | { 561 | $db = new Collection($this->filepath, [ 562 | 'key_prefix' => 'foobar' 563 | ]); 564 | 565 | $data = $db->insert([ 566 | 'label' => 'Test key prefix' 567 | ]); 568 | 569 | $this->assertEquals(substr($data['_id'], 0, 6), "foobar"); 570 | } 571 | 572 | public function testMacro() 573 | { 574 | // delete current db 575 | $this->tearDown(); 576 | 577 | $db = new Collection($this->filepath); 578 | 579 | // Register macro 580 | $db->macro('replace', function ($query, $key, array $replacers) { 581 | $keys = (array) $key; 582 | 583 | return $query->map(function ($item) use ($keys, $replacers) { 584 | foreach ($keys as $key) { 585 | if (isset($item[$key])) { 586 | $item[$key] = str_replace(array_keys($replacers), array_values($replacers), $item[$key]); 587 | } 588 | } 589 | return $item; 590 | }); 591 | }); 592 | 593 | // Insert items 594 | foreach (range(1, 10) as $n) { 595 | $db->insert([ 596 | 'number' => (string) $n 597 | ]); 598 | } 599 | 600 | // Use macro within collection 601 | $result = $db->replace('number', [ 602 | '1' => 'one', 603 | '2' => 'two' 604 | ])->get(); 605 | 606 | $this->assertEquals($result[0]['number'], 'one'); 607 | $this->assertEquals($result[1]['number'], 'two'); 608 | $this->assertEquals($result[9]['number'], 'one0'); 609 | 610 | // Use macro in query chain 611 | $result2 = $db->query()->replace('number', [ 612 | '1' => 'one', 613 | '2' => 'two' 614 | ])->get(); 615 | 616 | $this->assertEquals($result2[0]['number'], 'one'); 617 | $this->assertEquals($result2[1]['number'], 'two'); 618 | $this->assertEquals($result2[9]['number'], 'one0'); 619 | } 620 | 621 | public function testGlobalMacro() 622 | { 623 | DB::macro('replace', function ($query, $key, array $replacers) { 624 | $keys = (array) $key; 625 | 626 | return $query->map(function ($item) use ($keys, $replacers) { 627 | foreach ($keys as $key) { 628 | if (isset($item[$key])) { 629 | $item[$key] = str_replace(array_keys($replacers), array_values($replacers), $item[$key]); 630 | } 631 | } 632 | return $item; 633 | }); 634 | }); 635 | 636 | $this->tearDown(); 637 | $db = DB::open($this->filepath); 638 | 639 | 640 | // Insert items 641 | foreach (range(1, 10) as $n) { 642 | $db->insert([ 643 | 'number' => (string) $n 644 | ]); 645 | } 646 | 647 | $result = $db->replace('number', [ 648 | '1' => 'one', 649 | '2' => 'two' 650 | ])->get(); 651 | 652 | $this->assertEquals($result[0]['number'], 'one'); 653 | $this->assertEquals($result[1]['number'], 'two'); 654 | $this->assertEquals($result[9]['number'], 'one0'); 655 | } 656 | 657 | public function tearDown() 658 | { 659 | unlink($this->filepath); 660 | } 661 | 662 | } 663 | --------------------------------------------------------------------------------