├── .github
└── workflows
│ ├── code_analysis.yaml
│ └── tests.yaml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── ecs.php
├── helpers
├── dataTypes.php
└── json.php
├── phpstan.neon
├── phpunit.xml
├── src
└── JSONDB.php
└── tests
├── JSONDBTest.php
├── PerformanceTest.php
├── WhereTest.php
├── pets.json
└── users.json
/.github/workflows/code_analysis.yaml:
--------------------------------------------------------------------------------
1 | name: Code Analysis
2 |
3 | on:
4 | pull_request: null
5 | push:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | code_analysis:
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | actions:
15 | -
16 | name: 'Coding Standard'
17 | run: composer check-cs
18 |
19 | name: ${{ matrix.actions.name }}
20 | runs-on: ubuntu-latest
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | # see https://github.com/shivammathur/setup-php
25 | - uses: shivammathur/setup-php@v2
26 | with:
27 | php-version: 7.3
28 | coverage: none
29 |
30 | # composer install cache - https://github.com/ramsey/composer-install
31 | - uses: "ramsey/composer-install@v1"
32 |
33 | - run: ${{ matrix.actions.run }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yaml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on:
4 | pull_request: null
5 | push:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | unit_tests:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | php-versions: ['7.3', '7.4', '8.0', '8.1']
15 |
16 | name: PHP ${{ matrix.php-versions }} Tests
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 |
21 | # see https://github.com/shivammathur/setup-php
22 | - uses: shivammathur/setup-php@v2
23 | with:
24 | php-version: ${{ matrix.php-versions }}
25 | coverage: none
26 |
27 | # composer install cache - https://github.com/ramsey/composer-install
28 | - uses: "ramsey/composer-install@v1"
29 |
30 | - run: vendor/bin/phpunit
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /composer.lock
3 |
4 | .phpunit.result.cache
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jajo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## php-jsondb
2 | A PHP Class that reads JSON file as a database. Use for sample DBs.
3 |
4 | ### Usage
5 | Install package `composer require jajo/jsondb`
6 | #### Initialize
7 | ```php
8 | insert( 'users.json',
21 | [
22 | 'name' => 'Thomas',
23 | 'state' => 'Nigeria',
24 | 'age' => 22
25 | ]
26 | );
27 | ```
28 |
29 | #### Get
30 | Get back data, just like MySQL in PHP
31 |
32 | ##### All columns:
33 | ```php
34 | select( '*' )
36 | ->from( 'users.json' )
37 | ->get();
38 | print_r( $users );
39 | ```
40 |
41 | ##### Custom Columns:
42 | ```php
43 | select( 'name, state' )
45 | ->from( 'users.json' )
46 | ->get();
47 | print_r( $users );
48 |
49 | ```
50 |
51 | ##### Where Statement:
52 | This WHERE works as AND Operator at the moment or OR
53 | ```php
54 | select( 'name, state' )
56 | ->from( 'users.json' )
57 | ->where( [ 'name' => 'Thomas' ] )
58 | ->get();
59 | print_r( $users );
60 |
61 | // Defaults to Thomas OR Nigeria
62 | $users = $json_db->select( 'name, state' )
63 | ->from( 'users.json' )
64 | ->where( [ 'name' => 'Thomas', 'state' => 'Nigeria' ] )
65 | ->get();
66 | print_r( $users );
67 |
68 | // Now is THOMAS AND Nigeria
69 | $users = $json_db->select( 'name, state' )
70 | ->from( 'users.json' )
71 | ->where( [ 'name' => 'Thomas', 'state' => 'Nigeria' ], 'AND' )
72 | ->get();
73 | print_r( $users );
74 |
75 |
76 | ```
77 | ##### Where Statement with regex:
78 | By passing`JSONDB::regex` to where statement, you can apply regex searching. It can be used for implementing `LIKE` or `REGEXP_LIKE` clause in SQL.
79 |
80 | ```php
81 | $users = $json_db->select( 'name, state' )
82 | ->from( "users" )
83 | ->where( array( "state" => JSONDB::regex( "/ria/" )), JSONDB::AND )
84 | ->get();
85 | print_r( $users );
86 | // Outputs are rows which contains "ria" string in "state" column.
87 | ```
88 |
89 | ##### Order By:
90 | Thanks to [Tarun Shanker](http://in.linkedin.com/in/tarunshankerpandey) for this feature. By passing the `order_by()` method, the result is sorted with 2 arguments of the column name and sort method - `JSONDB::ASC` and `JSONDB::DESC`
91 | ```php
92 | select( 'name, state' )
94 | ->from( 'users.json' )
95 | ->where( [ 'name' => 'Thomas' ] )
96 | ->order_by( 'age', JSONDB::ASC )
97 | ->get();
98 | print_r( $users );
99 | ```
100 |
101 | #### Updating Row
102 | You can also update same JSON file with these methods
103 | ```php
104 | update( [ 'name' => 'Oji', 'age' => 10 ] )
106 | ->from( 'users.json' )
107 | ->where( [ 'name' => 'Thomas' ] )
108 | ->trigger();
109 |
110 | ```
111 | *Without the **where()** method, it will update all rows*
112 |
113 | #### Deleting Row
114 | ```php
115 | delete()
117 | ->from( 'users.json' )
118 | ->where( [ 'name' => 'Thomas' ] )
119 | ->trigger();
120 |
121 | ```
122 | *Without the **where()** method, it will deletes all rows*
123 |
124 | #### Exporting to MySQL
125 | You can export the JSON back to SQL file by using this method and providing an output
126 | ```php
127 | to_mysql( 'users.json', 'users.sql' );
129 | ```
130 | Disable CREATE TABLE
131 | ```php
132 | to_mysql( 'users.json', 'users.sql', false );
134 | ```
135 |
136 | #### Exporting to XML
137 | [Tarun Shanker](http://in.linkedin.com/in/tarunshankerpandey) also provided a feature to export data to an XML file
138 | ```php
139 | to_xml( 'users.json', 'users.xml' ) ) {
141 | echo 'Saved!';
142 | }
143 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jajo/jsondb",
3 | "description": "A PHP Class that reads JSON file as a database. Use for sample DBs",
4 | "type": "library",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Jajo",
9 | "email": "me@donjajo.com"
10 | }
11 | ],
12 | "autoload" : {
13 | "psr-4" : {
14 | "Jajo\\" : "src/"
15 | },
16 | "files" : [
17 | "helpers/dataTypes.php",
18 | "helpers/json.php"
19 | ]
20 | },
21 | "require": {
22 | "php": ">=7.3"
23 | },
24 | "require-dev": {
25 | "symplify/easy-coding-standard": "dev-main",
26 | "phpunit/phpunit": "^9.5",
27 | "phpstan/phpstan": "^1.4"
28 | },
29 | "scripts": {
30 | "check-cs": "vendor/bin/ecs check --ansi",
31 | "fix-cs": "vendor/bin/ecs check --fix --ansi",
32 | "phpstan": "vendor/bin/phpstan --ansi"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ecs.php:
--------------------------------------------------------------------------------
1 | parameters();
12 | $parameters->set(Option::PATHS, [
13 | __DIR__ . '/src',
14 | __DIR__ . '/tests',
15 | ]);
16 |
17 | $parameters->set(Option::PARALLEL, true);
18 |
19 | $parameters->set(Option::SKIP, [
20 | \PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer::class,
21 | ]);
22 |
23 | $services = $containerConfigurator->services();
24 | $services->set(ArraySyntaxFixer::class)
25 | ->call('configure', [[
26 | 'syntax' => 'short',
27 | ]]);
28 |
29 | // run and fix, one by one
30 | $containerConfigurator->import(SetList::SPACES);
31 | $containerConfigurator->import(SetList::ARRAY);
32 | $containerConfigurator->import(SetList::DOCBLOCK);
33 | $containerConfigurator->import(SetList::COMMENTS);
34 | $containerConfigurator->import(SetList::CONTROL_STRUCTURES);
35 |
36 | $containerConfigurator->import(SetList::PSR_12);
37 | };
38 |
--------------------------------------------------------------------------------
/helpers/dataTypes.php:
--------------------------------------------------------------------------------
1 | $value ) {
17 | $arr[ $key ] = obj_to_array( $value );
18 | }
19 |
20 | return $arr;
21 | }
--------------------------------------------------------------------------------
/helpers/json.php:
--------------------------------------------------------------------------------
1 | $bufsz ) ? $bufsz : $size;
28 | $size -= $read_count;
29 |
30 | if ( false === $start ) {
31 | // Find first occurence of the curly bracket
32 | $start = strpos( $buffer, '{' );
33 | if ( false === $start ) {
34 | $total_bytes_read += $read_count;
35 | continue;
36 | } else {
37 | $i = $start+1;
38 | $start += $total_bytes_read;
39 | $total_bytes_read += $read_count;
40 | }
41 | } else {
42 | $total_bytes_read += $read_count;
43 | }
44 |
45 | for ( ; isset( $buffer[ $i ] ); $i++ ) {
46 | if ( "'" == $buffer[ $i ] && ! $quotes[1] ) {
47 | // If quote is escaped, ignore
48 | if ( ! empty( $buffer[ $i - 1 ] ) && '\\' == $buffer[ $i - 1 ] ) {
49 | continue;
50 | }
51 |
52 | $quotes[0] = ! $quotes[0];
53 | continue;
54 | }
55 |
56 | if ( '"' == $buffer[ $i ] && ! $quotes[0] ) {
57 | // If quote is escaped, ignore
58 | if ( ! empty( $buffer[ $i - 1 ] ) && '\\' == $buffer[ $i - 1 ] ) {
59 | continue;
60 | }
61 |
62 | $quotes[1] = ! $quotes[1];
63 | continue;
64 | }
65 |
66 | $is_quoted = in_array( true, $quotes, true );
67 |
68 | if ( '{' == $buffer[ $i ] && ! $is_quoted ) {
69 | if ( $depth == $start_depth ) {
70 | $start = $total_bytes_read - $read_count + $i;
71 | }
72 | $depth++;
73 | }
74 |
75 | if ( '}' == $buffer[ $i ] && ! $is_quoted ) {
76 | $depth--;
77 | if ( $depth == $start_depth ) {
78 | $end = $total_bytes_read - $read_count + $i +1;
79 | break 2;
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
86 | $chunk = '';
87 |
88 | if ( false !== $start && false !== $end ) {
89 | fseek( $fp, $start, SEEK_SET );
90 | $chunk = fread( $fp, $end - $start );
91 | }
92 |
93 | fseek( $fp, $cur_pos, SEEK_SET );
94 |
95 | return $chunk;
96 | }
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: 4
3 |
4 | paths:
5 | - src
6 | - helpers
7 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 | tests
13 |
14 |
15 |
16 |
17 |
18 | ./
19 |
20 | ./test
21 | ./vendor
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/JSONDB.php:
--------------------------------------------------------------------------------
1 | dir = $dir;
45 | $this->json_opts['encode'] = $json_encode_opt;
46 | }
47 |
48 | public function check_fp_size()
49 | {
50 | $size = 0;
51 | $cur_size = 0;
52 |
53 | if ($this->fp) {
54 | $cur_size = ftell($this->fp);
55 | fseek($this->fp, 0, SEEK_END);
56 | $size = ftell($this->fp);
57 | fseek($this->fp, $cur_size, SEEK_SET);
58 | }
59 |
60 | return $size;
61 | }
62 |
63 | private function check_file()
64 | {
65 | /**
66 | * Checks and validates if JSON file exists
67 | *
68 | * @return bool
69 | */
70 |
71 | // Checks if DIR exists, if not create
72 | if (! is_dir($this->dir)) {
73 | mkdir($this->dir, 0700);
74 | }
75 | // Checks if JSON file exists, if not create
76 | if (! file_exists($this->file)) {
77 | touch($this->file);
78 | // $this->commit();
79 | }
80 |
81 | if ($this->load == 'partial') {
82 | $this->fp = fopen($this->file, 'r+');
83 | if (! $this->fp) {
84 | throw new \Exception('Unable to open json file');
85 | }
86 |
87 | $size = $this->check_fp_size();
88 | if ($size) {
89 | $content = get_json_chunk($this->fp);
90 |
91 | // We could not get the first chunk of JSON. Lets try to load everything then
92 | if (! $content) {
93 | $content = fread($this->fp, $size);
94 | } else {
95 | // We got the first chunk, we still need to put it into an array
96 | $content = sprintf('[%s]', $content);
97 | }
98 |
99 | $content = json_decode($content, true);
100 | } else {
101 | // Empty file. File was just created
102 | $content = [];
103 | }
104 | } else {
105 | // Read content of JSON file
106 | $content = file_get_contents($this->file);
107 | $content = json_decode($content, true);
108 | }
109 |
110 | // Check if its arrays of jSON
111 | if (! is_array($content) && is_object($content)) {
112 | throw new \Exception('An array of json is required: Json data enclosed with []');
113 | }
114 | // An invalid jSON file
115 | if (! is_array($content) && ! is_object($content)) {
116 | throw new \Exception('json is invalid');
117 | }
118 | $this->content = $content;
119 | return true;
120 | }
121 |
122 | public function select($args = '*')
123 | {
124 | /**
125 | * Explodes the selected columns into array
126 | *
127 | * @param type $args Optional. Default *
128 | * @return type object
129 | */
130 |
131 | // Explode to array
132 | $this->select = explode(',', $args);
133 | // Remove whitespaces
134 | $this->select = array_map('trim', $this->select);
135 | // Remove empty values
136 | $this->select = array_filter($this->select);
137 |
138 | return $this;
139 | }
140 |
141 | public function from($file, $load = 'full')
142 | {
143 | /**
144 | * Loads the jSON file
145 | *
146 | * @param type $file. Accepts file path to jSON file
147 | * @return type object
148 | */
149 |
150 | $this->file = sprintf('%s/%s.json', $this->dir, str_replace('.json', '', $file)); // Adding .json extension is no longer necessary
151 |
152 | // Reset where
153 | $this->where([]);
154 | $this->content = '';
155 | $this->load = $load;
156 |
157 | // Reset order by
158 | $this->order_by = [];
159 |
160 | if ($this->check_file()) {
161 | //$this->content = ( array ) json_decode( file_get_contents( $this->file ) );
162 | }
163 | return $this;
164 | }
165 |
166 | public function where(array $columns, $merge = 'OR')
167 | {
168 | $this->where = $columns;
169 | $this->merge = $merge;
170 | return $this;
171 | }
172 |
173 | /**
174 | * Implements regex search on where statement.
175 | *
176 | * @param string $pattern Regex pattern
177 | * @param int $preg_match_flags Flags for preg_grep(). See - https://www.php.net/manual/en/function.preg-match.php
178 | */
179 | public static function regex(string $pattern, int $preg_match_flags = 0): object
180 | {
181 | $c = new \stdClass();
182 | $c->is_regex = true;
183 | $c->value = $pattern;
184 | $c->options = $preg_match_flags;
185 |
186 | return $c;
187 | }
188 |
189 | public function delete()
190 | {
191 | $this->delete = true;
192 | return $this;
193 | }
194 |
195 | public function update(array $columns)
196 | {
197 | $this->update = $columns;
198 | return $this;
199 | }
200 |
201 | /**
202 | * Inserts data into json file
203 | *
204 | * @param string $file json filename without extension
205 | * @param array $values Array of columns as keys and values
206 | */
207 | public function insert($file, array $values)
208 | {
209 | $this->from($file, 'partial');
210 |
211 | $first_row = current($this->content);
212 | $this->content = [];
213 |
214 | if (! empty($first_row)) {
215 | $unmatched_columns = 0;
216 |
217 | foreach ($first_row as $column => $value) {
218 | if (! isset($values[$column])) {
219 | $values[$column] = null;
220 | }
221 | }
222 |
223 | foreach ($values as $col => $val) {
224 | if (! array_key_exists($col, $first_row)) {
225 | $unmatched_columns = 1;
226 | break;
227 | }
228 | }
229 |
230 | if ($unmatched_columns) {
231 | throw new \Exception('Columns must match as of the first row');
232 | }
233 | }
234 |
235 | $this->content[] = $values;
236 | $this->commit();
237 | }
238 |
239 | public function commit()
240 | {
241 | if ($this->fp && is_resource($this->fp)) {
242 | $f = $this->fp;
243 | } else {
244 | $f = fopen($this->file, 'w+');
245 | }
246 |
247 | if ($this->load === 'full') {
248 | // Write everything back into the file
249 | fwrite($f, (! $this->content ? '[]' : json_encode($this->content, $this->json_opts['encode'])));
250 | } elseif ($this->load === 'partial') {
251 | // Append it
252 | $this->append();
253 | } else {
254 | // Unknown load type
255 | fclose($f);
256 | throw new \Exception('Write fail: Unkown load type provided', 'write_error');
257 | }
258 |
259 | fclose($f);
260 | }
261 |
262 | private function append()
263 | {
264 | $size = $this->check_fp_size();
265 | $per_read = $size > 64 ? 64 : $size;
266 | $read_size = -$per_read;
267 | $lstblkbrkt = false;
268 | $lastinput = false;
269 | $i = $size;
270 | $data = json_encode($this->content, $this->json_opts['encode']);
271 |
272 | if ($size) {
273 | fseek($this->fp, $read_size, SEEK_END);
274 |
275 | while (($read = fread($this->fp, $per_read))) {
276 | $per_read = $i - $per_read < 0 ? $i : $per_read;
277 | if ($lstblkbrkt === false) {
278 | $lstblkbrkt = strrpos($read, ']', 0);
279 | if ($lstblkbrkt !== false) {
280 | $lstblkbrkt = ($i - $per_read) + $lstblkbrkt;
281 | }
282 | }
283 |
284 | if ($lstblkbrkt !== false) {
285 | $lastinput = strrpos($read, '}');
286 | if ($lastinput !== false) {
287 | $lastinput = ($i - $per_read) + $lastinput;
288 | break;
289 | }
290 | }
291 |
292 | $i -= $per_read;
293 | $read_size += -$per_read;
294 | if (abs($read_size) >= $size) {
295 | break;
296 | }
297 | fseek($this->fp, $read_size, SEEK_END);
298 | }
299 | }
300 |
301 | if ($lstblkbrkt !== false) {
302 | // We found existing json data, don't write extra [
303 | $data = substr($data, 1);
304 | if ($lastinput !== false) {
305 | $data = sprintf(',%s', $data);
306 | }
307 | } else {
308 | if ($size > 0) {
309 | throw new \Exception('Append error: JSON file looks malformed');
310 | }
311 |
312 | $lstblkbrkt = 0;
313 | }
314 |
315 | fseek($this->fp, $lstblkbrkt, SEEK_SET);
316 | fwrite($this->fp, $data);
317 | }
318 |
319 | private function _update()
320 | {
321 | if (! empty($this->last_indexes) && ! empty($this->where)) {
322 | foreach ($this->content as $i => $v) {
323 | if (in_array($i, $this->last_indexes)) {
324 | $content = (array) $this->content[$i];
325 | if (! array_diff_key($this->update, $content)) {
326 | $this->content[$i] = (object) array_merge($content, $this->update);
327 | } else {
328 | throw new \Exception('Update method has an off key');
329 | }
330 | } else {
331 | continue;
332 | }
333 | }
334 | } elseif (! empty($this->where) && empty($this->last_indexes)) {
335 | return;
336 | } else {
337 | foreach ($this->content as $i => $v) {
338 | $content = (array) $this->content[$i];
339 | if (! array_diff_key($this->update, $content)) {
340 | $this->content[$i] = (object) array_merge($content, $this->update);
341 | } else {
342 | throw new \Exception('Update method has an off key ');
343 | }
344 | }
345 | }
346 | }
347 |
348 | /**
349 | * Prepares data and written to file
350 | *
351 | * @return object $this
352 | */
353 | public function trigger()
354 | {
355 | $content = (! empty($this->where) ? $this->where_result() : $this->content);
356 | $return = false;
357 | if ($this->delete) {
358 | if (! empty($this->last_indexes) && ! empty($this->where)) {
359 | $this->content = array_filter($this->content, function ($index) {
360 | return ! in_array($index, $this->last_indexes);
361 | }, ARRAY_FILTER_USE_KEY);
362 |
363 | $this->content = array_values($this->content);
364 | } elseif (empty($this->where) && empty($this->last_indexes)) {
365 | $this->content = [];
366 | }
367 |
368 | $return = true;
369 | $this->delete = false;
370 | } elseif (! empty($this->update)) {
371 | $this->_update();
372 | $this->update = [];
373 | } else {
374 | $return = false;
375 | }
376 | $this->commit();
377 | return $this;
378 | }
379 |
380 | /**
381 | * Flushes indexes they won't be reused on next action
382 | */
383 | private function flush_indexes($flush_where = false)
384 | {
385 | $this->last_indexes = [];
386 | if ($flush_where) {
387 | $this->where = [];
388 | }
389 |
390 | if ($this->fp && is_resource($this->fp)) {
391 | fclose($this->fp);
392 | }
393 | }
394 |
395 | private function intersect_value_check($a, $b)
396 | {
397 | if ($b instanceof \stdClass) {
398 | if ($b->is_regex) {
399 | return ! preg_match($b->value, (string) $a, $_, $b->options);
400 | }
401 |
402 | return -1;
403 | }
404 |
405 | if ($a instanceof \stdClass) {
406 | if ($a->is_regex) {
407 | return ! preg_match($a->value, (string) $b, $_, $a->options);
408 | }
409 |
410 | return -1;
411 | }
412 |
413 | return strcasecmp((string) $a, (string) $b);
414 | }
415 |
416 | /**
417 | * Validates and fetch out the data for manipulation
418 | *
419 | * @return array $r Array of rows matching WHERE
420 | */
421 | private function where_result()
422 | {
423 | $this->flush_indexes();
424 |
425 | if ($this->merge == 'AND') {
426 | return $this->where_and_result();
427 | }
428 | // Filter array
429 | $r = array_filter($this->content, function ($row, $index) {
430 | $row = (array) $row; // Convert first stage to array if object
431 |
432 | // Check for rows intersecting with the where values.
433 | if (array_uintersect_uassoc($row, $this->where, [$this, 'intersect_value_check'], 'strcasecmp') /*array_intersect_assoc( $row, $this->where )*/) {
434 | $this->last_indexes[] = $index;
435 | return true;
436 | }
437 |
438 | return false;
439 | }, ARRAY_FILTER_USE_BOTH);
440 |
441 | // Make sure every object is turned to array here.
442 | return array_values(obj_to_array($r));
443 | }
444 |
445 | /**
446 | * Validates and fetch out the data for manipulation for AND
447 | *
448 | * @return array $r Array of fetched WHERE statement
449 | */
450 | private function where_and_result()
451 | {
452 | /*
453 | Validates the where statement values
454 | */
455 | $r = [];
456 |
457 | // Loop through the db rows. Ge the index and row
458 | foreach ($this->content as $index => $row) {
459 |
460 | // Make sure its array data type
461 | $row = (array) $row;
462 |
463 | //check if the row = where['col'=>'val', 'col2'=>'val2']
464 | if (! array_udiff_uassoc($this->where, $row, [$this, 'intersect_value_check'], 'strcasecmp')) {
465 | $r[] = $row;
466 | // Append also each row array key
467 | $this->last_indexes[] = $index;
468 | } else {
469 | continue;
470 | }
471 | }
472 | return $r;
473 | }
474 |
475 | public function to_xml($from, $to)
476 | {
477 | $this->from($from);
478 | if ($this->content) {
479 | $element = pathinfo($from, PATHINFO_FILENAME);
480 | $xml = '
481 |
482 | <' . $element . '>
483 | ';
484 |
485 | foreach ($this->content as $index => $value) {
486 | $xml .= '
487 | ';
488 | foreach ($value as $col => $val) {
489 | $xml .= sprintf('
490 | <%s>%s%s>', $col, $val, $col);
491 | }
492 | $xml .= '
493 |
494 | ';
495 | }
496 | $xml .= '' . $element . '>';
497 |
498 | $xml = trim($xml);
499 | file_put_contents($to, $xml);
500 | return true;
501 | }
502 | return false;
503 | }
504 |
505 | /**
506 | * Generates SQL from JSON
507 | *
508 | * @param string $from JSON file to get data from
509 | * @param string $to Filename to write SQL into
510 | * @param bool $create_table If to include create table in this export
511 | *
512 | * @return bool Returns true if file was created, else false
513 | */
514 | public function to_mysql(string $from, string $to, bool $create_table = true): bool
515 | {
516 | $this->from($from); // Reads the JSON file
517 | if ($this->content) {
518 | $table = pathinfo($to, PATHINFO_FILENAME); // Get filename to use as table
519 |
520 | $sql = "-- PHP-JSONDB JSON to MySQL Dump\n--\n\n";
521 | if ($create_table) {
522 | // Should create table, generate a CREATE TABLE statement using the column of the first row
523 | $first_row = (array) $this->content[0];
524 | $columns = array_map(function ($column) use ($first_row) {
525 | return sprintf("\t`%s` %s", $column, $this->_to_mysql_type(gettype($first_row[$column])));
526 | }, array_keys($first_row));
527 |
528 | $sql = sprintf("%s-- Table Structure for `%s`\n--\n\nCREATE TABLE `%s` \n(\n%s\n);\n", $sql, $table, $table, implode(",\n", $columns));
529 | }
530 |
531 | foreach ($this->content as $row) {
532 | $row = (array) $row;
533 | $values = array_map(function ($vv) {
534 | $vv = (is_array($vv) || is_object($vv) ? serialize($vv) : $vv);
535 | return sprintf("'%s'", addslashes((string) $vv));
536 | }, array_values($row));
537 |
538 | $cols = array_map(function ($col) {
539 | return sprintf('`%s`', $col);
540 | }, array_keys($row));
541 | $sql .= sprintf("INSERT INTO `%s` ( %s ) VALUES ( %s );\n", $table, implode(', ', $cols), implode(', ', $values));
542 | }
543 | file_put_contents($to, $sql);
544 | return true;
545 | }
546 | return false;
547 | }
548 |
549 | private function _to_mysql_type($type)
550 | {
551 | if ($type == 'bool') {
552 | $return = 'BOOLEAN';
553 | } elseif ($type == 'integer') {
554 | $return = 'INT';
555 | } elseif ($type == 'double') {
556 | $return = strtoupper($type);
557 | } else {
558 | $return = 'VARCHAR( 255 )';
559 | }
560 | return $return;
561 | }
562 |
563 | public function order_by($column, $order = self::ASC)
564 | {
565 | $this->order_by = [$column, $order];
566 | return $this;
567 | }
568 |
569 | private function _process_order_by($content)
570 | {
571 | if ($this->order_by && $content && in_array($this->order_by[0], array_keys((array) $content[0]))) {
572 | /*
573 | * Check if order by was specified
574 | * Check if there's actually a result of the query
575 | * Makes sure the column actually exists in the list of columns
576 | */
577 |
578 | list($sort_column, $order_by) = $this->order_by;
579 | $sort_keys = [];
580 | $sorted = [];
581 |
582 | foreach ($content as $index => $value) {
583 | $value = (array) $value;
584 | // Save the index and value so we can use them to sort
585 | $sort_keys[$index] = $value[$sort_column];
586 | }
587 |
588 | // Let's sort!
589 | if ($order_by == self::ASC) {
590 | asort($sort_keys);
591 | } elseif ($order_by == self::DESC) {
592 | arsort($sort_keys);
593 | }
594 |
595 | // We are done with sorting, lets use the sorted array indexes to pull back the original content and return new content
596 | foreach ($sort_keys as $index => $value) {
597 | $sorted[$index] = (array) $content[$index];
598 | }
599 |
600 | $content = $sorted;
601 | }
602 |
603 | return $content;
604 | }
605 |
606 | public function get()
607 | {
608 | if ($this->where != null) {
609 | $content = $this->where_result();
610 | } else {
611 | $content = $this->content;
612 | }
613 |
614 | if ($this->select && ! in_array('*', $this->select)) {
615 | $r = [];
616 | foreach ($content as $id => $row) {
617 | $row = (array) $row;
618 | foreach ($row as $key => $val) {
619 | if (in_array($key, $this->select)) {
620 | $r[$id][$key] = $val;
621 | } else {
622 | continue;
623 | }
624 | }
625 | }
626 | $content = $r;
627 | }
628 |
629 | // Finally, lets do sorting :)
630 | $content = $this->_process_order_by($content);
631 |
632 | $this->flush_indexes(true);
633 | return $content;
634 | }
635 | }
636 |
--------------------------------------------------------------------------------
/tests/JSONDBTest.php:
--------------------------------------------------------------------------------
1 | db = new JSONDB(__DIR__);
15 | }
16 |
17 | public function tearDown(): void
18 | {
19 | @unlink(__DIR__ . '/users.sql');
20 | }
21 |
22 | public function testInsert(): void
23 | {
24 | $names = ['James£', 'John£', 'Oji£', 'Okeke', 'Bola', 'Thomas', 'Ibrahim', 'Smile'];
25 | $states = ['Abia', 'Lagos', 'Benue', 'Kano', 'Kastina', 'Abuja', 'Imo', 'Ogun'];
26 | shuffle($names);
27 | shuffle($states);
28 |
29 | $state = current($states);
30 | $name = current($names);
31 | $age = mt_rand(20, 100);
32 | printf("Inserting: \n\nName\tAge\tState\n%s\t%d\t%s", $name, $age, $state);
33 |
34 | $indexes = $this->db->insert('users', [
35 | 'name' => $name,
36 | 'state' => $state,
37 | 'age' => $age,
38 | ]);
39 |
40 | $user = $this->db->select('*')
41 | ->from('users')
42 | ->where([
43 | 'name' => $name,
44 | 'state' => $state,
45 | 'age' => $age,
46 | ], 'AND')
47 | ->get();
48 |
49 | $this->db->insert('users', [
50 | 'name' => 'Dummy',
51 | 'state' => 'Lagos',
52 | 'age' => 12,
53 | ]);
54 |
55 | $this->assertEquals($name, $user[0]['name']);
56 | }
57 |
58 | public function testGet(): void
59 | {
60 | printf("\nCheck exist\n");
61 | $users = ($this->db->select('*')
62 | ->from('users')
63 | ->get());
64 | $this->assertNotEmpty($users);
65 | }
66 |
67 | public function testWhere(): void
68 | {
69 | $result = (
70 | $this->db->select('*')
71 | ->from('users')
72 | ->where([
73 | 'name' => 'Okeke',
74 | ])
75 | ->get()
76 | );
77 |
78 | // Probably has not inserted. Lets do it then
79 | if (! $result) {
80 | $this->db->insert('users', [
81 | 'name' => 'Okeke',
82 | 'age' => 21,
83 | 'state' => 'Enugu',
84 | ]);
85 |
86 | $result = (
87 | $this->db->select('*')
88 | ->from('users')
89 | ->where([
90 | 'name' => 'Okeke',
91 | ])
92 | ->get()
93 | );
94 | }
95 |
96 | $this->assertEquals('Okeke', $result[0]['name']);
97 | }
98 |
99 | public function testMultiWhere(): void
100 | {
101 | $this->db->insert('users', [
102 | 'name' => 'Jajo',
103 | 'age' => null,
104 | 'state' => 'Lagos',
105 | ]);
106 |
107 | $this->db->insert('users', [
108 | 'name' => 'Johnny',
109 | 'age' => 30,
110 | 'state' => 'Ogun',
111 | ]);
112 |
113 | $result = $this->db->select('*')->from('users')->where([
114 | 'age' => null,
115 | 'name' => 'Jajo',
116 | ])->get();
117 | $this->assertEquals('Jajo', $result[0]['name']);
118 | }
119 |
120 | public function testAND(): void
121 | {
122 | $this->db->insert('users', [
123 | 'name' => 'Jajo',
124 | 'age' => 50,
125 | 'state' => 'Lagos',
126 | ]);
127 |
128 | $this->db->insert('users', [
129 | 'name' => 'Johnny',
130 | 'age' => 50,
131 | 'state' => 'Ogun',
132 | ]);
133 |
134 | $result = $this->db->select('*')->from('users')->where([
135 | 'age' => 50,
136 | 'name' => 'Jajo',
137 | ], JSONDB::AND)->get();
138 |
139 | $this->assertEquals(1, count($result));
140 | $this->assertEquals('Jajo', $result[0]['name']);
141 | }
142 |
143 | public function testRegexAND(): void
144 | {
145 | $this->db->insert('users', [
146 | 'name' => 'Paulo',
147 | 'age' => 50,
148 | 'state' => 'Algeria',
149 | ]);
150 |
151 | $this->db->insert('users', [
152 | 'name' => 'Nina',
153 | 'age' => 50,
154 | 'state' => 'Nigeria',
155 | ]);
156 |
157 | $this->db->insert('users', [
158 | 'name' => 'Ogwo',
159 | 'age' => 49,
160 | 'state' => 'Nigeria',
161 | ]);
162 |
163 | $result = (
164 | $this->db->select('*')
165 | ->from('users')
166 | ->where([
167 | 'state' => JSONDB::regex('/ria/'),
168 | 'age' => JSONDB::regex('/5[0-9]/'),
169 | ], JSONDB::AND)
170 | ->get()
171 | );
172 |
173 | $this->assertEquals(2, count($result));
174 | $this->assertEquals('Paulo', $result[0]['name']);
175 | $this->assertEquals('Nina', $result[1]['name']);
176 | }
177 |
178 | public function testRegex(): void
179 | {
180 | $this->db->insert('users', [
181 | 'name' => 'Jajo',
182 | 'age' => 89,
183 | 'state' => 'Abia',
184 | ]);
185 |
186 | $this->db->insert('users', [
187 | 'name' => 'Mitchell',
188 | 'age' => 45,
189 | 'state' => 'Zamfara',
190 | ]);
191 |
192 | $result = ($this->db->select('*')
193 | ->from('users')
194 | ->where([
195 | 'state' => JSONDB::regex('/Zam/'),
196 | ])
197 | ->get());
198 |
199 | $this->assertEquals('Mitchell', $result[0]['name']);
200 | }
201 |
202 | public function testUpdate(): void
203 | {
204 | $this->db->update([
205 | 'name' => 'Jammy',
206 | 'state' => 'Sokoto',
207 | ])
208 | ->from('users')
209 | ->where([
210 | 'name' => 'Okeke',
211 | ])
212 | ->trigger();
213 |
214 | $this->db->update([
215 | 'state' => 'Rivers',
216 | ])
217 | ->from('users')
218 | ->where([
219 | 'name' => 'Dummy',
220 | ])
221 | ->trigger();
222 |
223 | $result = $this->db->select('*')
224 | ->from('users')
225 | ->where([
226 | 'name' => 'Jammy',
227 | ])
228 | ->get();
229 |
230 | $this->assertTrue($result[0]['state'] == 'Sokoto' && $result[0]['name'] == 'Jammy');
231 | }
232 |
233 | public function testSQLExport(): void
234 | {
235 | $this->db->to_mysql('users', 'tests/users.sql');
236 |
237 | $this->assertTrue(file_exists('tests/users.sql'));
238 | }
239 |
240 | public function testDelete(): void
241 | {
242 | $this->db->delete()
243 | ->from('users')
244 | ->where([
245 | 'name' => 'Jammy',
246 | ])
247 | ->trigger();
248 |
249 | $result = $this->db->select('*')
250 | ->from('users')
251 | ->where([
252 | 'name' => 'Jammy',
253 | ])
254 | ->get();
255 |
256 | $this->assertEmpty($result);
257 | }
258 |
259 | public function testDeleteAll(): void
260 | {
261 | /* I add a select action with where statement */
262 | $result_before = $this->db->select('*')
263 | ->from('users')
264 | ->where([
265 | 'state' => 'Rivers',
266 | ])
267 | ->get();
268 |
269 | /* Select action works fine */
270 | printf("\nCount of select action's result : %d", count($result_before));
271 | $this->assertTrue($result_before[0]['name'] == 'Dummy');
272 |
273 | /* Original test code by donjajo */
274 | $this->db->delete()
275 | ->from('users')
276 | ->trigger();
277 |
278 | $result = $this->db->select('*')
279 | ->from('users')
280 | ->get();
281 |
282 | /* But delete all action not working and assertion fail*/
283 | $this->assertEmpty($result);
284 | }
285 | }
286 |
--------------------------------------------------------------------------------
/tests/PerformanceTest.php:
--------------------------------------------------------------------------------
1 | jsondb = new JSONDB(__DIR__, JSON_UNESCAPED_UNICODE);
15 | }
16 |
17 | protected function tearDown(): void
18 | {
19 | unlink(__DIR__ . '/food.json');
20 | }
21 |
22 | public function testInsert(): void
23 | {
24 | $i = 0;
25 | while ($i < 5000) {
26 | $sum = 0;
27 | for ($j = 0; $j < 1000; $j++) {
28 | $start = hrtime(true);
29 | $this->jsondb->insert('food', [
30 | 'name' => 'Rice',
31 | 'class' => 'Carbohydrate',
32 | ]);
33 | $stop = hrtime(true);
34 | $sum += ($stop - $start) / 1000000;
35 | }
36 | $i += $j;
37 | fprintf(STDOUT, "\nTook average of %fms to insert 1000 records - BATCH %d", $sum, $i / 1000);
38 | fflush(STDOUT);
39 | }
40 |
41 | $foods = $this->jsondb->select('name')->from('food')->get();
42 | $this->assertCount(5000, $foods);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/WhereTest.php:
--------------------------------------------------------------------------------
1 | db = new JSONDB(__DIR__);
15 | }
16 |
17 | // Both 'setUp' and 'tearDown' function is called by phpunit per every test function before test function is called
18 | public function setUp(): void
19 | {
20 | $this->load_db();
21 |
22 | $names = ['hamster', 'chinchilla', 'dog', 'cat', 'rat', 'chamaeleon', 'turtle', 'chupacabra', 'catoblepas', 'catoblepas'];
23 | $kinds = ['rodentia', 'rodentia', 'canivora', 'carnivora', 'rodentia', 'squamata', 'testudines', null, null, 'game-character'];
24 |
25 | for ($i = 0; $i < count($names); $i++) {
26 | $this->db->insert('pets', [
27 | 'name' => $names[$i],
28 | 'kind' => $kinds[$i],
29 | 'age' => $i % 3,
30 | ]);
31 | }
32 | }
33 |
34 | public function tearDown(): void
35 | {
36 | $this->db->delete()
37 | ->from('pets')
38 | ->trigger();
39 | }
40 |
41 | public function testWhereOr()
42 | {
43 | $result = (
44 | $this->db->select('*')
45 | ->from('pets')
46 | ->where([
47 | 'kind' => 'rodentia',
48 | 'age' => 0,
49 | ])->get()
50 | );
51 | $this->assertCount(6, $result);
52 |
53 | $result = (
54 | $this->db->select('*')
55 | ->from('pets')
56 | ->where([
57 | 'kind' => 'squamata',
58 | 'age' => 2,
59 | ])->get()
60 | );
61 | $this->assertCount(3, $result);
62 | }
63 |
64 | public function testWhereNullOr()
65 | {
66 | $result = (
67 | $this->db->select('*')
68 | ->from('pets')
69 | ->where([
70 | 'kind' => null,
71 | ])->get()
72 | );
73 | $this->assertCount(2, $result);
74 | }
75 |
76 | public function testWhereNullAnd()
77 | {
78 | $result = (
79 | $this->db->select('*')
80 | ->from('pets')
81 | ->where([
82 | 'name' => 'catoblepas',
83 | 'kind' => null,
84 | ], 'AND')->get()
85 | );
86 | $this->assertCount(1, $result);
87 | $this->assertEquals('catoblepas', $result[0]['name']);
88 | $this->assertSame(null, $result[0]['kind']);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tests/pets.json:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/tests/users.json:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------