├── 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 | [](https://travis-ci.org/emsifa/laci-db)
6 | [](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 |
--------------------------------------------------------------------------------