├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── benchmarks ├── bootstrap.php ├── inserts.php └── reads.php ├── cli-demo.gif ├── composer.json ├── flatbase ├── flatbase.json ├── phpunit.xml ├── src ├── Collection.php ├── Console │ ├── Commands │ │ ├── AbstractCommand.php │ │ ├── DeleteCommand.php │ │ ├── InsertCommand.php │ │ ├── ReadCommand.php │ │ └── UpdateCommand.php │ └── Dumper.php ├── Exception │ ├── Exception.php │ └── InvalidArgumentException.php ├── Flatbase.php ├── Handler │ ├── DeleteQueryHandler.php │ ├── InsertQueryHandler.php │ ├── QueryHandler.php │ ├── ReadQueryHandler.php │ └── UpdateQueryHandler.php ├── Query │ ├── DeleteQuery.php │ ├── InsertQuery.php │ ├── Query.php │ ├── ReadQuery.php │ └── UpdateQuery.php └── Storage │ ├── Filesystem.php │ └── Storage.php └── tests ├── CollectionTest.php ├── Console └── Commands │ ├── InsertCommandTest.php │ └── ReadCommandTest.php ├── DeleteTest.php ├── FlatbaseTest.php ├── FlatbaseTestCase.php ├── FluentInterfaceTest.php ├── InsertTest.php ├── ReadTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | tests/storage/ 4 | benchmarks/storage/ 5 | humbuglog.txt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 7.0 4 | - 5.6 5 | - hhvm 6 | install: 7 | - composer install --no-interaction --prefer-source 8 | - mkdir tests/storage -m 777 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flatbase 2 | 3 | Flatbase is a flat file database written in PHP which aims to be: 4 | 5 | - Lightweight 6 | - Very easy to install, with minimal/no configuration 7 | - Simple intuitive API 8 | - Suitable for small data sets, low-load applications, and testing/prototyping 9 | 10 | ## Example Usage 11 | 12 | ```php 13 | insert()->in('users') 19 | ->set(['name' => 'Adam', 'height' => "6'4"]) 20 | ->execute(); 21 | 22 | $flatbase->read()->in('users') 23 | ->where('name', '=', 'Adam') 24 | ->first(); 25 | // (array) ['name' => 'Adam', 'height' => "6'4"] 26 | 27 | ``` 28 | 29 | ## Installation 30 | 31 | composer require flatbase/flatbase 32 | 33 | ## Usage 34 | 35 | ### Reading 36 | 37 | Fetch all the records from a collection: 38 | 39 | ```php 40 | $flatbase->read()->in('users')->get(); // Flatbase\Collection 41 | ``` 42 | 43 | Reading only data matching a certain criteria: 44 | 45 | ```php 46 | $flatbase->read()->in('users')->where('id', '==', '5')->get(); 47 | ``` 48 | 49 | We support all the comparison operators you'd expect: 50 | 51 | - `=` 52 | - `!=` 53 | - `==` 54 | - `!==` 55 | - `<` 56 | - `>` 57 | 58 | You can chain as many `where()` conditions as you like: 59 | 60 | ```php 61 | $flatbase->read() 62 | ->in('users') 63 | ->where('age', '<', 40) 64 | ->where('age', '>', 20) 65 | ->where('country', '==', 'UK') 66 | ->get(); 67 | ``` 68 | 69 | Limit the returned records: 70 | 71 | ```php 72 | $flatbase->read()->in('users')->limit(10)->get(); // Get the first 10 records 73 | $flatbase->read()->in('users')->skip(5)->limit(10)->get(); // Skip the first 5, then return the next 10 74 | $flatbase->read()->in('users')->first(); // Get the first record 75 | ``` 76 | 77 | Sort the records: 78 | 79 | ```php 80 | $flatbase->read()->in('users')->sort('age')->get(); // Sort by age in ascending order 81 | $flatbase->read()->in('users')->sortDesc('age')->get(); // Sort by age in descending order 82 | ``` 83 | 84 | Just get a count of records: 85 | 86 | ```php 87 | $flatbase->read()->in('users')->count(); 88 | ``` 89 | 90 | ### Deleting 91 | 92 | Delete all records in a collection: 93 | 94 | ```php 95 | $flatbase->delete()->in('users')->execute(); 96 | ``` 97 | 98 | Or just some records: 99 | 100 | ```php 101 | $flatbase->delete()->in('users')->where('id', '==', 5)->execute(); 102 | ``` 103 | 104 | ### Inserting 105 | 106 | ```php 107 | $flatbase->insert()->in('users')->set([ 108 | 'name' => 'Adam', 109 | 'country' => 'UK', 110 | 'language' => 'English' 111 | ])->execute(); 112 | ``` 113 | 114 | ### Updating 115 | 116 | Update all records in a collection: 117 | 118 | ```php 119 | $flatbase->update()->in('users')->set(['country' => 'IE',])->execute(); 120 | ``` 121 | 122 | Or just some records: 123 | 124 | ```php 125 | $flatbase->update() 126 | ->in('users') 127 | ->set(['country' => 'IE',]) 128 | ->where('name', '==', 'Adam') 129 | ->execute(); 130 | ``` 131 | 132 | 133 | ## SQL Cheat Sheet 134 | 135 | SQL Statement | Flatbase Query 136 | --- | --- 137 | `SELECT * FROM posts` | `$flatbase->read()->in('posts')->get();` 138 | `SELECT * FROM posts LIMIT 0,1` | `$flatbase->read()->in('posts')->first();` 139 | `SELECT * FROM posts WHERE id = 5` | `$flatbase->read()->in('posts')->where('id', '==', 5)->get();` 140 | `SELECT * FROM posts WHERE views > 500` | `$flatbase->read()->in('posts')->where('views', '>', 500)->get();` 141 | `SELECT * FROM posts WHERE views > 50 AND id = 5` | `$flatbase->read()->in('posts')->where('views', '>', 50)->where('id', '==', '5')->get();` 142 | `UPDATE posts SET title = 'Foo' WHERE content = 'bar'` | `$flatbase->update()->in('posts')->set(['title' => 'var'])->where('content', '==', 'bar')->execute();` 143 | `DELETE FROM posts WHERE id = 2` | `$flatbase->delete()->in('posts')->where('id', '==', 2)->execute();` 144 | `INSERT INTO posts SET title='Foo', content='Bar'` | `$flatbase->insert()->in('posts')->set(['title' => 'Foo', 'content' => 'Bar')->execute();` 145 | 146 | ## Command Line Interface 147 | 148 | Flatbase includes a command line interface `flatbase` for quick manipulation of data outside of your application. 149 | 150 | ```bash 151 | php vendor/bin/flatbase read users 152 | ``` 153 | 154 | ### Installation 155 | 156 | To use the CLI, you must define the path to your storage directory. This can either be done with a `flatbase.json` file in the directory you call flatbase from (usually your application root): 157 | 158 | ```json 159 | { 160 | "path": "some/path/to/storage" 161 | } 162 | ``` 163 | 164 | Alternatively, simply include the `--path` option when issuing commands. Eg: 165 | 166 | ```bash 167 | php vendor/bin/flatbase read users --path="some/path/to/storage" 168 | ``` 169 | 170 | 171 | ### Demo 172 | 173 | 174 | ### Usage 175 | 176 | ```bash 177 | # Get all records 178 | php flatbase read users 179 | 180 | # Get the first record in a collection 181 | php flatbase read users --first 182 | 183 | # Count the records in a collection 184 | php flatbase read users --count 185 | 186 | # Get users matching some where clauses 187 | php flatbase read users --where "name,==,Adam" --where "age,<,30" 188 | 189 | # Update some record(s) 190 | php flatbase update users --where "age,<,18" --where "age,>,12" ageGroup=teenager 191 | 192 | # Insert a new record 193 | php flatbase insert users name=Adam age=25 country=UK 194 | 195 | # Delete some record(s) 196 | php flatbase delete users --where "name,==,Adam" 197 | ``` 198 | 199 | For more info on the CLI, use one of the `help` commands 200 | 201 | ```bash 202 | php flatbase help 203 | php flatbase read --help 204 | php flatbase update --help 205 | php flatbase insert --help 206 | php flatbase delete --help 207 | ``` 208 | 209 | ## Why use a flat file database? 210 | 211 | What are some of the advantages of a flat file database? 212 | 213 | #### It's really easy to get started 214 | Just add `flatbase/flatbase` to your `composer.json` and you're rolling. No need for any other services running. 215 | 216 | #### It's schema-less 217 | You don't have to worry about defining a schema, writing migration scripts, or any of that other boring stuff. Just instantiate `Flatbase` and start giving it data. This is particularly useful when developing/prototyping new features. 218 | 219 | #### Store plain old PHP objects 220 | Data is stored in a native PHP serialized array using [PHPSerializer](https://github.com/adamnicholson/php-serializer). This means that you can store plain old PHP objects straight to the database: 221 | 222 | ```php 223 | $flatbase->insert()->in('users')->set([ 224 | 'id' => 1, 225 | 'name' => 'Adam', 226 | 'added' => new DateTime() 227 | ])->execute(); 228 | 229 | $record = $flatbase->read()->in('users')->where('id', '==', 1)->first(); 230 | var_dump($record['added']); // DateTime 231 | ``` 232 | 233 | It also means that you can, at any point, easily unserialize() your data without having to go through Flatbase if you wish. 234 | > Note: Although serializing is possible, be careful when using this in production. Remember that if you serialize an object, and then, later on, delete or move the class it was an instance of, you won't be able to un-serialize it. Storing scalar data is always a safer alternative. 235 | 236 | #### It isn't actually that slow 237 | 238 | Ok, that's a bit of a baiting title. Some operations are remarkably quick considering this is a flat file database. On a mediocre Ubuntu desktop development environment, it can process around 50,000 "inserts" in 1 second. No, that is still nowhere near a database like MySQL or Mongo, but it's a hell of a lot more than most people need. 239 | 240 | Reading data out is certainly a lot slower, and although there are lots of places we can optimise, ultimately you'd need to accept this is never going to be a high-performance solution for persistence. 241 | 242 | ## Author 243 | 244 | Adam Nicholson - adamnicholson10@gmail.com 245 | 246 | ## Contributing 247 | 248 | Contributions are welcome, and they can be made via GitHub issues or pull requests. 249 | 250 | ## License 251 | 252 | Flatbase is licensed under the MIT License - see the `LICENSE.txt` file for details 253 | -------------------------------------------------------------------------------- /benchmarks/bootstrap.php: -------------------------------------------------------------------------------- 1 | start(); 14 | 15 | $flatbase = new Flatbase(new Filesystem(__DIR__.'/storage')); 16 | 17 | $limit = 70000; 18 | for ($i = 0; $i <= $limit; $i++) { 19 | $insertQuery = new \Flatbase\Query\InsertQuery(); 20 | $insertQuery->setCollection('test-insert-collection'); 21 | $insertQuery->setValues(['id' => $i]); 22 | $flatbase->execute($insertQuery); 23 | } 24 | 25 | // Stop the clock 26 | $bench->end(); 27 | 28 | // Post the results 29 | echo number_format($limit).' inserts completed'.PHP_EOL; 30 | echo 'Execution time: '.$bench->getTime().PHP_EOL; 31 | echo 'Memory peak: '.$bench->getMemoryPeak().PHP_EOL; -------------------------------------------------------------------------------- /benchmarks/reads.php: -------------------------------------------------------------------------------- 1 | $i, 'time' => microtime()]; 14 | } 15 | file_put_contents(__DIR__.'/storage/test-read-collection', serialize($data)); 16 | 17 | // Start the clock 18 | $bench = new Ubench; 19 | $bench->start(); 20 | 21 | echo "Starting reads".PHP_EOL; 22 | $flatbase = new Flatbase(new Filesystem(__DIR__.'/storage')); 23 | 24 | $limit = 50; 25 | for ($i = 0; $i <= $limit; $i++) { 26 | $flatbase->read()->in('test-read-collection')->get(); 27 | } 28 | 29 | // Stop the clock 30 | $bench->end(); 31 | 32 | // Post results 33 | echo number_format($limit).' reads of a database with '.number_format($databaseSize).' records completed'.PHP_EOL; 34 | echo 'Execution time: '.$bench->getTime().PHP_EOL; 35 | echo 'Memory peak: '.$bench->getMemoryPeak().PHP_EOL; -------------------------------------------------------------------------------- /cli-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamnicholson/flatbase/8bfb373429d3359b064b39e226015e48f1c1243f/cli-demo.gif -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flatbase/flatbase", 3 | "description": "Standalone queryable flat file database", 4 | "authors": [ 5 | { 6 | "name": "Adam Nicholson", 7 | "email": "adamnicholson10@gmail.com" 8 | } 9 | ], 10 | "bin": ["flatbase"], 11 | "require": { 12 | "php": ">=5.6.0", 13 | "php-serializer/php-serializer": "0.1.*" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Flatbase\\": ["src/", "tests/"] 18 | } 19 | }, 20 | "require-dev": { 21 | "devster/ubench": "~1.1", 22 | "phpunit/phpunit": "^5", 23 | "symfony/console": "~2.6", 24 | "symfony/var-dumper": "~2.6" 25 | }, 26 | "suggest": { 27 | "symfony/console": "Needed for console support using vendor/bin/flatbase", 28 | "symfony/var-dumper": "Needed for console support using vendor/bin/flatbase" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /flatbase: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new \Flatbase\Console\Commands\ReadCommand( 27 | new \Symfony\Component\VarDumper\Cloner\VarCloner(), 28 | new \Flatbase\Console\Dumper() 29 | )); 30 | $console->add(new \Flatbase\Console\Commands\InsertCommand()); 31 | $console->add(new \Flatbase\Console\Commands\DeleteCommand()); 32 | $console->add(new \Flatbase\Console\Commands\UpdateCommand()); 33 | $console->run(); -------------------------------------------------------------------------------- /flatbase.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "tests/storage" 3 | } 4 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | offsetExists(0) ? $this->offsetGet(0) : null; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Console/Commands/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | addOption( 36 | 'path', 37 | null, 38 | InputOption::VALUE_OPTIONAL, 39 | 'Path of the database storage dir' 40 | ) 41 | ->addOption( 42 | 'config', 43 | null, 44 | InputOption::VALUE_OPTIONAL, 45 | 'Path to a flatbase.json configuration file' 46 | ) 47 | ; 48 | } 49 | 50 | /** 51 | * Get the Flatbase storage path from the --path console option 52 | * 53 | * @return string 54 | */ 55 | protected function getStoragePath() 56 | { 57 | if ($override = $this->input->getOption('path')) { 58 | return $override; 59 | } 60 | $config = $this->getConfig(); 61 | 62 | return getcwd().'/'.$config->path; 63 | } 64 | 65 | /** 66 | * Get the config data 67 | * 68 | * @return \stdClass 69 | */ 70 | protected function getConfig() 71 | { 72 | $configPath = $this->input->getOption('config') ?: (getcwd().'/flatbase.json'); 73 | 74 | $defaults = new \stdClass(); 75 | $defaults->path = null; 76 | 77 | if (file_exists($configPath)) { 78 | foreach (json_decode(file_get_contents($configPath)) as $property => $value) { 79 | $defaults->{$property} = $value; 80 | } 81 | } 82 | 83 | return $defaults; 84 | } 85 | 86 | /** 87 | * Get a Flatbase object for a given storage path. 88 | * 89 | * @return Flatbase 90 | */ 91 | protected function getFlatbase($storagePath) 92 | { 93 | $factory = $this->factory; 94 | 95 | return $factory ? $factory($storagePath) : new Flatbase(new Filesystem($storagePath)); 96 | } 97 | 98 | /** 99 | * Testing method 100 | * 101 | * @param callable $factory 102 | */ 103 | public function setFlatbaseFactory(callable $factory) 104 | { 105 | $this->factory = $factory; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Console/Commands/DeleteCommand.php: -------------------------------------------------------------------------------- 1 | input = $input; 19 | $this->output = $output; 20 | 21 | $this->buildQuery($input)->execute(); 22 | 23 | // Write out the count 24 | $output->writeln('Delete query executed'); 25 | } 26 | 27 | /** 28 | * @param InputInterface $input 29 | * @return \Flatbase\Query\DeleteQuery 30 | */ 31 | protected function buildQuery(InputInterface $input) 32 | { 33 | $flatbase = $this->getFlatbase($this->getStoragePath()); 34 | $query = $flatbase->delete()->in($input->getArgument('collection')); 35 | 36 | foreach ($this->input->getOption('where') as $where) { 37 | $splode = explode(',', $where); 38 | 39 | if (count($splode) !== 3) { 40 | throw new InvalidArgumentException('Each --where must be passed a string in the format "{key},{operator},{value}. Eg. --where "name,==,Adam"'); 41 | } 42 | 43 | list($l, $op, $r) = $splode; 44 | 45 | $query->where($l, $op, $r); 46 | } 47 | 48 | return $query; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | protected function configure() 55 | { 56 | $this 57 | ->setName('delete') 58 | ->setDescription('Delete into a collection') 59 | ->addArgument( 60 | 'collection', 61 | InputArgument::REQUIRED, 62 | 'Name of the collection to delete from' 63 | ) 64 | ->addOption( 65 | 'where', 66 | null, 67 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 68 | 'Add a "where" statement. Must include 3 comma-separated parts. Eg. "name,==,Adam"', 69 | [] 70 | ) 71 | ; 72 | 73 | parent::configure(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Console/Commands/InsertCommand.php: -------------------------------------------------------------------------------- 1 | input = $input; 18 | $this->output = $output; 19 | 20 | $this->buildQuery($input)->execute(); 21 | 22 | // Write out the count 23 | $output->writeln('Insert query executed'); 24 | } 25 | 26 | /** 27 | * @param InputInterface $input 28 | * @return \Flatbase\Query\InsertQuery 29 | */ 30 | protected function buildQuery(InputInterface $input) 31 | { 32 | $flatbase = $this->getFlatbase($this->getStoragePath()); 33 | $query = $flatbase->insert()->in($input->getArgument('collection')); 34 | 35 | $values = []; 36 | foreach ($input->getArgument('set') as $value) { 37 | $splode = explode('=', $value); 38 | if (count($splode) !== 2) { 39 | throw new InvalidArgumentException('Each value set must be passed a string with a single key-value pair formatted as "key=value". Eg. "flatbase insert users name=Adam created=Monday age=25"'); 40 | } 41 | $values[$splode[0]] = $splode[1]; 42 | } 43 | $query->setValues($values); 44 | 45 | return $query; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function configure() 52 | { 53 | $this 54 | ->setName('insert') 55 | ->setDescription('Insert into a collection') 56 | ->addArgument( 57 | 'collection', 58 | InputArgument::REQUIRED, 59 | 'Name of the collection to insert into' 60 | ) 61 | ->addArgument( 62 | 'set', 63 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 64 | 'Set a value for the new record. Must include a key value pair separated by "=" (eg. "name=Adam")' 65 | ) 66 | ; 67 | 68 | parent::configure(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Console/Commands/ReadCommand.php: -------------------------------------------------------------------------------- 1 | cloner = $cloner; 29 | $this->dumper = $dumper; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | $this->input = $input; 38 | $this->output = $output; 39 | 40 | $this->dumper->setOutputInterface($output); 41 | 42 | // Fetch the records 43 | $records = $this->buildQuery($input)->get(); 44 | 45 | // Write out the count 46 | $output->writeln('Found '.$records->count().' records'); 47 | 48 | if ($input->getOption('count')) { 49 | return; 50 | } 51 | 52 | foreach ($records as $record) { 53 | $this->dumper->dump($this->cloner->cloneVar($record)); 54 | } 55 | } 56 | 57 | /** 58 | * @param InputInterface $input 59 | * @return \Flatbase\Query\ReadQuery 60 | */ 61 | protected function buildQuery(InputInterface $input) 62 | { 63 | $flatbase = $this->getFlatbase($this->getStoragePath()); 64 | $query = $flatbase->read()->in($input->getArgument('collection')); 65 | 66 | foreach ($this->input->getOption('where') as $where) { 67 | $splode = explode(',', $where); 68 | 69 | if (count($splode) !== 3) { 70 | throw new InvalidArgumentException('Each --where must be passed a string in the format "{key},{operator},{value}. Eg. --where "name,==,Adam"'); 71 | } 72 | 73 | list($l, $op, $r) = $splode; 74 | 75 | $query->where($l, $op, $r); 76 | } 77 | 78 | if ($limit = $this->input->getOption('limit')) { 79 | $query->limit($limit); 80 | } 81 | 82 | if ($offset = $this->input->getOption('skip')) { 83 | $query->skip($offset); 84 | } 85 | 86 | if ($offset = $this->input->getOption('sort')) { 87 | $query->sort($offset); 88 | } 89 | 90 | if ($offset = $this->input->getOption('sortDesc')) { 91 | $query->sortDesc($offset); 92 | } 93 | 94 | if ($this->input->getOption('first')) { 95 | $query->limit(1); 96 | } 97 | 98 | return $query; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | protected function configure() 105 | { 106 | $this 107 | ->setName('read') 108 | ->setDescription('Read from a collection') 109 | ->addArgument( 110 | 'collection', 111 | InputArgument::REQUIRED, 112 | 'Name of the collection to read from' 113 | ) 114 | ->addOption( 115 | 'where', 116 | null, 117 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 118 | 'Add a "where" statement. Must include 3 comma-separated parts. Eg. "name,==,Adam"', 119 | [] 120 | ) 121 | ->addOption( 122 | 'limit', 123 | null, 124 | InputOption::VALUE_OPTIONAL, 125 | 'Limit the number of results' 126 | ) 127 | ->addOption( 128 | 'skip', 129 | null, 130 | InputOption::VALUE_OPTIONAL, 131 | 'Skip the first x number of results' 132 | ) 133 | ->addOption( 134 | 'sort', 135 | null, 136 | InputOption::VALUE_OPTIONAL, 137 | 'Sort the results by a field in ascending order' 138 | ) 139 | ->addOption( 140 | 'sortDesc', 141 | null, 142 | InputOption::VALUE_OPTIONAL, 143 | 'Sort the results by a field in descending order' 144 | ) 145 | ->addOption( 146 | 'count', 147 | null, 148 | InputOption::VALUE_NONE, 149 | 'Only get the record count' 150 | ) 151 | ->addOption( 152 | 'first', 153 | null, 154 | InputOption::VALUE_NONE, 155 | 'Only get the first record' 156 | ) 157 | ; 158 | 159 | parent::configure(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Console/Commands/UpdateCommand.php: -------------------------------------------------------------------------------- 1 | input = $input; 19 | $this->output = $output; 20 | 21 | $this->buildQuery($input)->execute(); 22 | 23 | // Write out the count 24 | $output->writeln('Update query executed'); 25 | } 26 | 27 | /** 28 | * @param InputInterface $input 29 | * @return \Flatbase\Query\UpdateQuery 30 | */ 31 | protected function buildQuery(InputInterface $input) 32 | { 33 | $flatbase = $this->getFlatbase($this->getStoragePath()); 34 | $query = $flatbase->update()->in($input->getArgument('collection')); 35 | 36 | // Parse new values to set/update 37 | $values = []; 38 | foreach ($input->getArgument('set') as $value) { 39 | $splode = explode('=', $value); 40 | if (count($splode) !== 2) { 41 | throw new \InvalidArgumentException('Each value set must be passed a string with a single key-value pair formatted as "key=value". Eg. "flatbase update users name=Adam created=Monday age=25"'); 42 | } 43 | $values[$splode[0]] = $splode[1]; 44 | } 45 | $query->setValues($values); 46 | 47 | // Parse "where" clauses 48 | foreach ($this->input->getOption('where') as $where) { 49 | $splode = explode(',', $where); 50 | 51 | if (count($splode) !== 3) { 52 | throw new InvalidArgumentException('Each --where must be passed a string in the format "{key},{operator},{value}. Eg. --where "name,==,Adam"'); 53 | } 54 | 55 | list($l, $op, $r) = $splode; 56 | 57 | $query->where($l, $op, $r); 58 | } 59 | 60 | return $query; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | protected function configure() 67 | { 68 | $this 69 | ->setName('update') 70 | ->setDescription('Update into a collection') 71 | ->addArgument( 72 | 'collection', 73 | InputArgument::REQUIRED, 74 | 'Name of the collection to update into' 75 | ) 76 | ->addArgument( 77 | 'set', 78 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 79 | 'Set a value to update. Must include a key value pair separated by "=" (eg. "name=Adam")' 80 | ) 81 | ->addOption( 82 | 'where', 83 | null, 84 | InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 85 | 'Add a "where" statement. Must include 3 comma-separated parts. Eg. "name,==,Adam"', 86 | [] 87 | ) 88 | ; 89 | 90 | parent::configure(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Console/Dumper.php: -------------------------------------------------------------------------------- 1 | outputInterface = $outputInterface; 18 | } 19 | 20 | /** 21 | * Generic line dumper callback. 22 | * 23 | * @param string $line The line to write. 24 | * @param int $depth The recursive depth in the dumped structure. 25 | */ 26 | protected function echoLine($line, $depth, $indentPad) 27 | { 28 | if (-1 !== $depth) { 29 | $this->outputInterface->writeln(str_repeat($indentPad, $depth).$line); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 27 | } 28 | 29 | /** 30 | * Execute a query 31 | * 32 | * @param Query $query 33 | * @return Collection|void 34 | * @throws \Exception 35 | */ 36 | public function execute(Query $query) 37 | { 38 | $handler = $this->resolveHandler($query); 39 | 40 | return $handler->handle($query); 41 | } 42 | 43 | /** 44 | * Create a new insert query 45 | * 46 | * @return InsertQuery 47 | */ 48 | public function insert() 49 | { 50 | return new InsertQuery($this); 51 | } 52 | 53 | /** 54 | * Create a new update query 55 | * 56 | * @return UpdateQuery 57 | */ 58 | public function update() 59 | { 60 | return new UpdateQuery($this); 61 | } 62 | 63 | /** 64 | * Create a new read query 65 | * 66 | * @return ReadQuery 67 | */ 68 | public function read() 69 | { 70 | return new ReadQuery($this); 71 | } 72 | 73 | /** 74 | * Create a new delete query 75 | * 76 | * @return DeleteQuery 77 | */ 78 | public function delete() 79 | { 80 | return new DeleteQuery($this); 81 | } 82 | 83 | /** 84 | * Find the appropriate handler for a given Query 85 | * 86 | * @param Query $query 87 | * @return DeleteQueryHandler|InsertQueryHandler|ReadQueryHandler 88 | * @throws \Exception 89 | */ 90 | protected function resolveHandler(Query $query) 91 | { 92 | if ($query instanceof ReadQuery) { 93 | return new ReadQueryHandler($this); 94 | } 95 | 96 | if ($query instanceof InsertQuery) { 97 | return new InsertQueryHandler($this); 98 | } 99 | 100 | if ($query instanceof DeleteQuery) { 101 | return new DeleteQueryHandler($this); 102 | } 103 | 104 | if ($query instanceof UpdateQuery) { 105 | return new UpdateQueryHandler($this); 106 | } 107 | 108 | throw new Exception('Could not resolve handler for query'); 109 | } 110 | 111 | /** 112 | * @return \Flatbase\Storage\Storage 113 | */ 114 | public function getStorage() 115 | { 116 | return $this->storage; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Handler/DeleteQueryHandler.php: -------------------------------------------------------------------------------- 1 | validateQuery($query); 13 | 14 | $stream = $this->getIterator($query->getCollection()); 15 | 16 | foreach ($stream as $record) { 17 | if ($this->recordMatchesQuery($record, $query)) { 18 | $stream->remove(); 19 | } 20 | } 21 | } 22 | 23 | protected function validateQuery(Query $query) 24 | { 25 | parent::validateQuery($query); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Handler/InsertQueryHandler.php: -------------------------------------------------------------------------------- 1 | validateQuery($query); 14 | 15 | $stream = $this->getIterator($query->getCollection()); 16 | 17 | $stream->append($query->getValues()); 18 | } 19 | 20 | protected function validateQuery(Query $query) 21 | { 22 | parent::validateQuery($query); 23 | 24 | if (!$query->getValues()) { 25 | throw new Exception('No values given to insert'); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Handler/QueryHandler.php: -------------------------------------------------------------------------------- 1 | flatbase = $flatbase; 20 | } 21 | 22 | /** 23 | * Check if a single record should be matched for a given Query's conditions ("where" clauses) 24 | * 25 | * @param $record 26 | * @param Query $query 27 | * @return bool 28 | * @throws \Exception 29 | */ 30 | protected function recordMatchesQuery($record, Query $query) 31 | { 32 | $results = []; 33 | 34 | foreach ($query->getConditions() as $condition) { 35 | $results[] = $this->assertCondition($record, $condition); 36 | } 37 | 38 | $failed = in_array(false, $results); 39 | 40 | return !$failed; 41 | } 42 | 43 | /** 44 | * Check if a single record should be matched for a single Query condition 45 | * 46 | * @param $record 47 | * @param $condition 48 | * @return bool 49 | * @throws \Exception 50 | */ 51 | protected function assertCondition($record, $condition) 52 | { 53 | $left = $condition[0]; 54 | $op = $condition[1]; 55 | $right = $condition[2]; 56 | 57 | switch ($op) { 58 | case '=': 59 | $value = $this->getRecordField($record, $left); 60 | return $value == $right; 61 | 62 | case '==': 63 | $value = $this->getRecordField($record, $left); 64 | return $value === $right; 65 | 66 | case '!=': 67 | $value = $this->getRecordField($record, $left); 68 | return $value != $right; 69 | 70 | case '!==': 71 | $value = $this->getRecordField($record, $left); 72 | return $value !== $right; 73 | 74 | case '<': 75 | $value = $this->getRecordField($record, $left); 76 | return $value < $right; 77 | 78 | case '>': 79 | $value = $this->getRecordField($record, $left); 80 | return $value > $right; 81 | 82 | default: 83 | throw new Exception('Operator ['.$op.'] is not supported'); 84 | } 85 | } 86 | 87 | /** 88 | * Get the value of a field on a record. Defaults to null if not set 89 | * 90 | * @param $record 91 | * @param $field 92 | * @return null 93 | */ 94 | protected function getRecordField($record, $field) 95 | { 96 | return isset($record[$field]) ? $record[$field] : null; 97 | } 98 | 99 | /** 100 | * Validate a Query for execution 101 | * 102 | * @param Query $query 103 | * @throws \Exception 104 | */ 105 | protected function validateQuery(Query $query) 106 | { 107 | if (!$query->getCollection()) { 108 | throw new Exception('No colleciton set'); 109 | } 110 | } 111 | 112 | /** 113 | * Get a SerializedArray instance for the collection 114 | * 115 | * @param string $collection 116 | * @return SerializedArray 117 | */ 118 | public function getIterator($collection) 119 | { 120 | return new SerializedArray($this->flatbase->getStorage()->getFileObject($collection)); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Handler/ReadQueryHandler.php: -------------------------------------------------------------------------------- 1 | validateQuery($query); 20 | 21 | // Get all the records matching the given query conditions 22 | $records = $this->getAllRecordsMatchingQueryConditions($query); 23 | 24 | // Sort them 25 | $this->sortRecords($records, $query); 26 | 27 | // Limit them 28 | $this->limitRecords($records, $query); 29 | 30 | // Return them as a Collection 31 | return new Collection($records); 32 | } 33 | 34 | /** 35 | * Get all the records in a collection matching the Query conditions 36 | * 37 | * @param ReadQuery $query 38 | * @return array 39 | */ 40 | protected function getAllRecordsMatchingQueryConditions(ReadQuery $query) 41 | { 42 | $records = []; 43 | foreach ($this->getIterator($query->getCollection()) as $record) { 44 | $records[] = $record; 45 | } 46 | 47 | if (!$query->getConditions()) { 48 | return $records; 49 | } 50 | 51 | foreach ($records as $key => $record) { 52 | if (!$this->recordMatchesQuery($record, $query)) { 53 | unset($records[$key]); 54 | } 55 | } 56 | 57 | return $records; 58 | } 59 | 60 | /** 61 | * Sort an array of records as per by a ReadQuery getSortBy() 62 | * 63 | * @param $results 64 | * @param ReadQuery $query 65 | */ 66 | protected function sortRecords(&$results, ReadQuery $query) 67 | { 68 | if (list($sortField, $sortDirection) = $query->getSortBy()) { 69 | usort($results, function($a, $b) use ($sortField, $sortDirection) { 70 | 71 | $leftValue = $this->getRecordField($a, $sortField); 72 | $rightValue = $this->getRecordField($b, $sortField); 73 | 74 | if ($sortDirection == 'DESC') { 75 | return strcmp((string) $rightValue, (string) $leftValue); 76 | } else { 77 | return strcmp((string) $leftValue, (string) $rightValue); 78 | } 79 | }); 80 | } 81 | } 82 | 83 | /** 84 | * Limit an array of records as per a ReadQuery getLimit() and getOffset() options 85 | * @param $records 86 | * @param ReadQuery $query 87 | */ 88 | protected function limitRecords(&$records, ReadQuery $query) 89 | { 90 | $records = array_slice($records, $query->getOffset(), $query->getLimit()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Handler/UpdateQueryHandler.php: -------------------------------------------------------------------------------- 1 | validateQuery($query); 12 | 13 | $stream = $this->getIterator($query->getCollection()); 14 | 15 | $toUpdate = []; 16 | 17 | foreach ($stream as $record) { 18 | if ($this->recordMatchesQuery($record, $query)) { 19 | $stream->remove(); 20 | $toUpdate[] = $record; 21 | } 22 | } 23 | 24 | foreach ($toUpdate as $record) { 25 | foreach ($query->getValues() as $key => $value) { 26 | $record[$key] = $value; 27 | } 28 | $stream->append($record); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Query/DeleteQuery.php: -------------------------------------------------------------------------------- 1 | setFlatbase($flatbase); 20 | } 21 | } 22 | 23 | /** 24 | * Set the collection this query is querying 25 | * 26 | * @param $collection 27 | * @return $this 28 | */ 29 | public function setCollection($collection) 30 | { 31 | $this->collection = $collection; 32 | return $this; 33 | } 34 | 35 | /** 36 | * Get the collection this query is querying 37 | * 38 | * @return string 39 | */ 40 | public function getCollection() 41 | { 42 | return $this->collection; 43 | } 44 | 45 | /** 46 | * Add a condition to this query 47 | * 48 | * @param $recordField 49 | * @param $operator 50 | * @param $value 51 | * @return $this 52 | */ 53 | public function addCondition($recordField, $operator, $value) 54 | { 55 | $this->conditions[] = [ 56 | $recordField, 57 | $operator, 58 | $value 59 | ]; 60 | return $this; 61 | } 62 | 63 | /** 64 | * Get all the conditions associated with this query 65 | * 66 | * @return array 67 | */ 68 | public function getConditions() 69 | { 70 | return $this->conditions; 71 | } 72 | 73 | /** 74 | * @return mixed 75 | */ 76 | public function getValues() 77 | { 78 | return $this->values; 79 | } 80 | 81 | /** 82 | * @param mixed $values 83 | * @return $this 84 | */ 85 | public function setValues($values) 86 | { 87 | $this->values = $values; 88 | return $this; 89 | } 90 | 91 | public function set($data, $valueIfDataIsKey = null) 92 | { 93 | if (is_array($data) && $valueIfDataIsKey === null) { 94 | $this->values = array_merge($this->values, $data); 95 | return $this; 96 | } 97 | 98 | if ($valueIfDataIsKey !== null) { 99 | $this->values[$data] = $valueIfDataIsKey;# 100 | return $this; 101 | } 102 | 103 | throw new InvalidArgumentException('Argument 1 to Query::set() must be an array if the second argument is not given'); 104 | } 105 | 106 | /** 107 | * Execute this query 108 | * 109 | * @return \Flatbase\Collection|void 110 | * @throws \Exception 111 | */ 112 | public function execute() 113 | { 114 | if (!$this->flatbase) { 115 | throw new Exception('Query::execute() can only be called when the query was 116 | created by Flatbase, eg. Flatbase::read()'); 117 | } 118 | 119 | return $this->flatbase->execute($this); 120 | } 121 | 122 | /** 123 | * @param Flatbase $flatbase 124 | */ 125 | public function setFlatbase(Flatbase $flatbase) 126 | { 127 | $this->flatbase = $flatbase; 128 | } 129 | 130 | /** 131 | * Alias of setCollection() 132 | * 133 | * @param $collection 134 | * @return $this 135 | */ 136 | public function in($collection) 137 | { 138 | return $this->setCollection($collection); 139 | } 140 | 141 | /** 142 | * Alias of addCollection() 143 | * 144 | * @param $recordField 145 | * @param $operator 146 | * @param $value 147 | * @return $this 148 | */ 149 | public function where($recordField, $operator, $value) 150 | { 151 | return $this->addCondition($recordField, $operator, $value); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Query/ReadQuery.php: -------------------------------------------------------------------------------- 1 | offset = $offset; 20 | return $this; 21 | } 22 | 23 | /** 24 | * Get the offset 25 | * @return int 26 | */ 27 | public function getOffset() 28 | { 29 | return $this->offset; 30 | } 31 | 32 | /** 33 | * Set the maximum number of records to return 34 | * @param $limit 35 | * @return $this 36 | */ 37 | public function setLimit($limit) 38 | { 39 | $this->limit = $limit; 40 | return $this; 41 | } 42 | 43 | /** 44 | * Get the limit 45 | * @return integer|null 46 | */ 47 | public function getLimit() 48 | { 49 | return $this->limit; 50 | } 51 | 52 | /** 53 | * Set the field the records should be ordered by 54 | * 55 | * @param $field 56 | * @param string $direction 57 | * @return $this 58 | */ 59 | public function setSortBy($field, $direction = 'ASC') 60 | { 61 | $this->sort = [$field, $direction]; 62 | return $this; 63 | } 64 | 65 | /** 66 | * Get the sortBy 67 | * 68 | * @return null 69 | */ 70 | public function getSortBy() 71 | { 72 | return $this->sort; 73 | } 74 | 75 | /** 76 | * Alias of setLimit() 77 | * 78 | * @param $limit 79 | * @return ReadQuery 80 | */ 81 | public function limit($limit) 82 | { 83 | return $this->setLimit($limit); 84 | } 85 | 86 | /** 87 | * Alias of setOffset() 88 | * 89 | * @param $offset 90 | * @return ReadQuery 91 | */ 92 | public function skip($offset) 93 | { 94 | return $this->setOffset($offset); 95 | } 96 | 97 | /** 98 | * Alias of setSortBy() in default direction 99 | * 100 | * @param $field 101 | * @return ReadQuery 102 | */ 103 | public function sort($field) 104 | { 105 | return $this->setSortBy($field); 106 | } 107 | 108 | /** 109 | * Alias of setSortBy in DESC order 110 | * 111 | * @param $field 112 | * @return ReadQuery 113 | */ 114 | public function sortDesc($field) 115 | { 116 | return $this->setSortBy($field, 'DESC'); 117 | } 118 | 119 | /** 120 | * Alias of execute() 121 | * 122 | * @return \Flatbase\Collection 123 | * @throws \Exception 124 | */ 125 | public function get() 126 | { 127 | return $this->execute(); 128 | } 129 | 130 | /** 131 | * Execute the query and return the first element 132 | * 133 | * @return mixed|null 134 | */ 135 | public function first() 136 | { 137 | foreach ($this->get() as $item) { 138 | return $item; 139 | } 140 | return null; 141 | } 142 | 143 | /** 144 | * Count the records matching the query conditions 145 | * 146 | * @return int 147 | */ 148 | public function count() 149 | { 150 | return $this->get()->count(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Query/UpdateQuery.php: -------------------------------------------------------------------------------- 1 | storageDir = $storageDir; 15 | } 16 | 17 | /** 18 | * @param $collection 19 | * @return \SplFileObject 20 | */ 21 | public function getFileObject($collection) 22 | { 23 | $file = $this->getFilename($collection); 24 | 25 | return new \SplFileObject($file, 'r+'); 26 | } 27 | 28 | /** 29 | * Get the corresponding file path for a given collection 30 | * 31 | * @param $collection 32 | * @return string 33 | */ 34 | protected function getFilename($collection) 35 | { 36 | $file = rtrim($this->storageDir, '/').'/'.$collection; 37 | 38 | if (!file_exists($file)) { 39 | file_put_contents($file, serialize([])); 40 | chmod($file, 0777); 41 | } 42 | 43 | return $file; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Storage/Storage.php: -------------------------------------------------------------------------------- 1 | assertTrue(new Collection() instanceof Collection); 10 | } 11 | 12 | public function testArrayAccess() 13 | { 14 | $items = ['one', 'two', 'three']; 15 | $collection = new Collection($items); 16 | $this->assertEquals($items[0], $collection[0]); 17 | $this->assertEquals($items[1], $collection[1]); 18 | $this->assertEquals($items[2], $collection[2]); 19 | $this->assertEquals(count($collection), 3); 20 | } 21 | 22 | public function testFirst() 23 | { 24 | $items = ['one', 'two', 'three']; 25 | $collection = new Collection($items); 26 | $this->assertEquals($collection->first(), 'one'); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Console/Commands/InsertCommandTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($command instanceof Command); 17 | } 18 | 19 | public function testRequiredArguments() 20 | { 21 | $this->runReadCommandTest(['collection' => 'users', 'set' => ['name=Adam']], function(InsertQuery $query) { 22 | $this->assertEquals($query->getCollection(), 'users'); 23 | $this->assertEquals($query->getValues(), ['name' => 'Adam']); 24 | return true; 25 | }); 26 | } 27 | 28 | public function testSetAcceptsMupltipleArguments() 29 | { 30 | $this->runReadCommandTest(['collection' => 'users', 'set' => ['name=Adam', 'dob=1990-08-08', 'from=UK']], function(InsertQuery $query) { 31 | $this->assertEquals($query->getCollection(), 'users'); 32 | $this->assertEquals($query->getValues(), [ 33 | 'name' => 'Adam', 34 | 'dob' => '1990-08-08', 35 | 'from' => 'UK' 36 | ]); 37 | return true; 38 | }); 39 | } 40 | 41 | protected function runReadCommandTest(array $input, callable $executeArgumentExpectation) 42 | { 43 | // Create the command 44 | $command = new InsertCommand(); 45 | 46 | // Attach a Flatbase double to the command 47 | $flatbase = $this->prophesize('Flatbase\Flatbase'); 48 | $command->setFlatbaseFactory(function($storageDir) use ($flatbase) { 49 | return $flatbase->reveal(); 50 | }); 51 | 52 | // Expect read() to be called and return a ReadQuery. Making sure the ReadQuery uses our Flatbase double 53 | $flatbase->insert()->shouldBeCalled()->willReturn($query = new InsertQuery()); 54 | $query->setFlatbase($flatbase->reveal()); 55 | 56 | // Define what the query argument should look like when Flatbase::execute() is called 57 | $expectedQuery = Argument::that($executeArgumentExpectation); 58 | $flatbase->execute($expectedQuery)->shouldBeCalled(); 59 | 60 | // Run it 61 | $input = new ArrayInput($input); 62 | $output = $this->prophesize('Symfony\Component\Console\Output\OutputInterface'); 63 | $command->run($input, $output->reveal()); 64 | } 65 | } -------------------------------------------------------------------------------- /tests/Console/Commands/ReadCommandTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($command instanceof Command); 20 | } 21 | 22 | public function testReadUsesCorrectCollection() 23 | { 24 | $this->runReadCommandTest(['collection' => 'users'], function(ReadQuery $query) { 25 | $this->assertEquals($query->getCollection(), 'users'); 26 | return true; 27 | }); 28 | } 29 | 30 | public function testReadWheres() 31 | { 32 | $this->runReadCommandTest(['collection' => 'users', '--where' => ['foo,==,bar']], function(ReadQuery $query) { 33 | $this->assertEquals($query->getCollection(), 'users'); 34 | $wheres = $query->getConditions(); 35 | $this->assertEquals(count($wheres), 1); 36 | $this->assertEquals($wheres[0], ['foo', '==', 'bar']); 37 | return true; 38 | }); 39 | } 40 | 41 | public function testLimit() 42 | { 43 | $this->runReadCommandTest(['collection' => 'users', '--limit' => '5'], function(ReadQuery $query) { 44 | $this->assertEquals($query->getLimit(), 5); 45 | return true; 46 | }); 47 | } 48 | 49 | public function testOffset() 50 | { 51 | $this->runReadCommandTest(['collection' => 'users', '--skip' => '2'], function(ReadQuery $query) { 52 | $this->assertEquals($query->getOffset(), 2); 53 | return true; 54 | }); 55 | } 56 | 57 | public function testSort() 58 | { 59 | $this->runReadCommandTest(['collection' => 'users', '--sort' => 'age'], function(ReadQuery $query) { 60 | $this->assertEquals($query->getSortBy(), ['age', 'ASC']); 61 | return true; 62 | }); 63 | } 64 | 65 | public function testSortDesc() 66 | { 67 | $this->runReadCommandTest(['collection' => 'users', '--sortDesc' => 'age'], function(ReadQuery $query) { 68 | $this->assertEquals($query->getSortBy(), ['age', 'DESC']); 69 | return true; 70 | }); 71 | } 72 | 73 | public function testFirst() 74 | { 75 | $this->runReadCommandTest(['collection' => 'users', '--first' => true], function(ReadQuery $query) { 76 | $this->assertEquals($query->getLimit(), 1); 77 | return true; 78 | }); 79 | } 80 | 81 | protected function runReadCommandTest(array $input, callable $executeArgumentExpectation) 82 | { 83 | // Create the command 84 | $command = new ReadCommand(new VarCloner(), new Dumper()); 85 | 86 | // Attach a Flatbase double to the command 87 | $flatbase = $this->prophesize('Flatbase\Flatbase'); 88 | $command->setFlatbaseFactory(function($storageDir) use ($flatbase) { 89 | return $flatbase->reveal(); 90 | }); 91 | 92 | // Expect read() to be called and return a ReadQuery. Making sure the ReadQuery uses our Flatbase double 93 | $flatbase->read()->shouldBeCalled()->willReturn($query = new ReadQuery()); 94 | $query->setFlatbase($flatbase->reveal()); 95 | 96 | // Define what the query argument should look like when Flatbase::execute() is called 97 | $expectedQuery = Argument::that($executeArgumentExpectation); 98 | $flatbase->execute($expectedQuery)->shouldBeCalled()->willReturn(new Collection()); 99 | 100 | // Run it 101 | $input = new ArrayInput($input); 102 | $output = $this->prophesize('Symfony\Component\Console\Output\OutputInterface'); 103 | $command->run($input, $output->reveal()); 104 | } 105 | } -------------------------------------------------------------------------------- /tests/DeleteTest.php: -------------------------------------------------------------------------------- 1 | getFlatbaseWithSampleData(); 13 | 14 | $flatbase->delete()->in('users')->execute(); 15 | 16 | $count = $flatbase->read()->in('users')->count(); 17 | 18 | $this->assertEquals($count, 0); 19 | } 20 | 21 | public function testDeleteWithSingleEqualsCondition() 22 | { 23 | $flatbase = $this->getFlatbaseWithSampleData(); 24 | 25 | $countPreDelete = $flatbase->read()->in('users')->count(); 26 | 27 | // Delete one thing 28 | $flatbase->delete()->in('users')->where('age', '=', 24)->execute(); 29 | 30 | // Re-count them. Should be $countPreDelete minus one 31 | $countPostDelete = $flatbase->read()->in('users')->count(); 32 | 33 | $this->assertEquals($countPostDelete, $countPreDelete-1); 34 | } 35 | } -------------------------------------------------------------------------------- /tests/FlatbaseTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($flatbase instanceof Flatbase); 18 | } 19 | 20 | public function testReadQueryReturnsData() 21 | { 22 | $flatbase = $this->getFlatbase(); 23 | $query = new ReadQuery(); 24 | $query->setCollection('users'); 25 | $collection = $flatbase->execute($query); 26 | $this->assertTrue($collection instanceof Collection); 27 | } 28 | 29 | public function testInsertQueryDoesNotThrowErrors() 30 | { 31 | $flatbase = $this->getFlatbase(); 32 | $query = new InsertQuery(); 33 | $query->setCollection('users'); 34 | $query->setValues([ 35 | 'firstname' => 'Adam', 36 | 'lastname' => 'Nicholson' 37 | ]); 38 | $flatbase->execute($query); 39 | } 40 | 41 | public function testInsertIncreasesReadRecordCountByOne() 42 | { 43 | $flatbase = $this->getFlatbase(); 44 | // Read the current count 45 | $query = new ReadQuery(); 46 | $query->setCollection('users'); 47 | $countPreInsert = $flatbase->execute($query)->count(); 48 | // Insert something 49 | $query = new InsertQuery(); 50 | $query->setCollection('users'); 51 | $query->setValues([ 52 | 'firstname' => 'Adam', 53 | 'lastname' => 'Nicholson' 54 | ]); 55 | $flatbase->execute($query); 56 | // Read the new count 57 | $query = new ReadQuery(); 58 | $query->setCollection('users'); 59 | $countPostInsert = $flatbase->execute($query)->count(); 60 | $this->assertEquals($countPostInsert, $countPreInsert+1); 61 | } 62 | 63 | public function testDeleteDecrementsCountToZeroWithNoConditions() 64 | { 65 | $flatbase = $this->getFlatbase(); 66 | // Insert something 67 | $query = new InsertQuery(); 68 | $query->setCollection('users'); 69 | $query->setValues([ 70 | 'firstname' => 'Adam', 71 | 'lastname' => 'Nicholson' 72 | ]); 73 | $flatbase->execute($query); 74 | // Delete everything 75 | $query = new DeleteQuery(); 76 | $query->setCollection('users'); 77 | $flatbase->execute($query); 78 | // Read the new count 79 | $query = new ReadQuery(); 80 | $query->setCollection('users'); 81 | $countDeleteInsert = $flatbase->execute($query)->count(); 82 | $this->assertEquals($countDeleteInsert, 0); 83 | } 84 | 85 | public function testUpdateChangesValueAndDoesNotAffectCollectionCount() 86 | { 87 | $flatbase = $this->getFlatbaseWithSampleData(); 88 | $countMatchingUsersBeforeUpdate = $flatbase->execute($flatbase->read()->in('users')->where('age', '=', 26))->count(); 89 | $countAllUsersBeforeUpdate = $flatbase->execute($flatbase->read()->in('users'))->count(); 90 | $query = $flatbase->update()->in('users')->where('age', '=', 26)->setValues([ 91 | 'age' => 35 92 | ]); 93 | $flatbase->execute($query); 94 | $this->assertEquals($flatbase->execute($flatbase->read()->in('users'))->count(), $countAllUsersBeforeUpdate); 95 | $this->assertEquals($flatbase->execute($flatbase->read()->in('users')->where('age', '=', 35))->count(), $countMatchingUsersBeforeUpdate); 96 | 97 | } 98 | 99 | public function testQueryBuilderConstructors() 100 | { 101 | $flatbase = $this->getFlatbase(); 102 | $this->assertTrue($flatbase->insert() instanceof InsertQuery); 103 | $this->assertTrue($flatbase->update() instanceof UpdateQuery); 104 | $this->assertTrue($flatbase->read() instanceof ReadQuery); 105 | $this->assertTrue($flatbase->delete() instanceof DeleteQuery); 106 | } 107 | 108 | public function testQueriesCanSelfExecute() 109 | { 110 | $flatbase = $this->getFlatbaseWithSampleData(); 111 | $this->assertTrue($flatbase->read()->in('users')->execute() instanceof Collection); 112 | } 113 | } -------------------------------------------------------------------------------- /tests/FlatbaseTestCase.php: -------------------------------------------------------------------------------- 1 | storageDir = __DIR__ . '/storage'; 16 | parent::setup(); 17 | } 18 | 19 | protected function getFlatbase() 20 | { 21 | $storage = new Filesystem($this->storageDir); 22 | $flatbase = new Flatbase($storage); 23 | return $flatbase; 24 | } 25 | 26 | protected function getFlatbaseWithSampleData() 27 | { 28 | $flatbase = $this->getFlatbase(); 29 | 30 | // Empty it 31 | @unlink(__DIR__ . '/storage/users'); 32 | 33 | $data = [ 34 | [ 35 | 'name' => 'Adam', 36 | 'age' => 23, 37 | 'height' => "6'3", 38 | 'company' => 'Foo Inc', 39 | 'weight' => 200 40 | ], 41 | [ 42 | 'name' => 'Adam', 43 | 'age' => 24, 44 | 'height' => "6'4", 45 | 'company' => 'Foo Inc', 46 | 'weight' => 180 47 | ], 48 | [ 49 | 'name' => 'Adam', 50 | 'age' => 25, 51 | 'height' => "6'5", 52 | 'company' => 'Bar Inc', 53 | 'weight' => 210 54 | ], 55 | [ 56 | 'name' => 'Michael', 57 | 'age' => 26, 58 | 'height' => "6'6", 59 | 'company' => 'Foo Inc', 60 | 'weight' => 170 61 | ], 62 | ]; 63 | 64 | foreach ($data as $record) { 65 | $flatbase->insert() 66 | ->in('users') 67 | ->setValues($record) 68 | ->execute(); 69 | } 70 | 71 | return $flatbase; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/FluentInterfaceTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($query->in('foo'), $query); 13 | $this->assertEquals($query->setCollection('foo'), $query); 14 | $this->assertEquals($query->addCondition('foo', '=', 'bar'), $query); 15 | $this->assertEquals($query->where('foo', '=', 'bar'), $query); 16 | } 17 | } -------------------------------------------------------------------------------- /tests/InsertTest.php: -------------------------------------------------------------------------------- 1 | getFlatbase(); 12 | $collection = 'test'; 13 | $db = $this->storageDir . '/' . $collection; 14 | $flatbase->insert()->in($collection)->setValues(['foo' => 'bar'])->execute(); 15 | $dbContents = file_get_contents($db); 16 | if (!@unserialize($dbContents)) { 17 | $this->fail('Insert corrupted the database'); 18 | } 19 | } 20 | 21 | public function testSetSetsValuesWhenArrayPassed() 22 | { 23 | $query = new InsertQuery(); 24 | 25 | $query->in('foo')->set($data = [ 26 | 'foo' => 'bar', 27 | 'baz' => 'buz' 28 | ]); 29 | 30 | $this->assertEquals($query->getValues(), $data); 31 | } 32 | 33 | public function testSetAppendsValuesWhenArrayPassed() 34 | { 35 | $query = new InsertQuery(); 36 | 37 | $query->in('foo')->set('bla', 'blo')->set($data = [ 38 | 'foo' => 'bar', 39 | 'baz' => 'buz' 40 | ]); 41 | 42 | $this->assertEquals($query->getValues(), [ 43 | 'bla' => 'blo', 44 | 'foo' => 'bar', 45 | 'baz' => 'buz' 46 | ]); 47 | } 48 | 49 | public function testSetSetsValuesWhenKeyValuePairPassed() 50 | { 51 | $query = new InsertQuery(); 52 | 53 | $query->in('foo')->set('foo', 'bar')->set('baz', 'buz'); 54 | 55 | $this->assertEquals($query->getValues(), [ 56 | 'foo' => 'bar', 57 | 'baz' => 'buz' 58 | ]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/ReadTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($this->getFlatbaseWithSampleData()->read()->in('users')->count(), 4); 14 | } 15 | 16 | public function testReadWithSingleEqualsCondition() 17 | { 18 | $flatbase = $this->getFlatbaseWithSampleData(); 19 | $this->assertEquals($flatbase->read()->in('users')->where('age', '=', 24)->count(), 1); 20 | $this->assertEquals($flatbase->read()->in('users')->where('name', '=', 'Adam')->count(), 3); 21 | $this->assertEquals($flatbase->read()->in('users')->where('height', '=', "6'4")->count(), 1); 22 | } 23 | 24 | public function testReadWithMultipleEqualsConditions() 25 | { 26 | $flatbase = $this->getFlatbaseWithSampleData(); 27 | $query = new ReadQuery(); 28 | $query->setCollection('users'); 29 | $query->addCondition('name', '=', 'Adam'); 30 | $query->addCondition('company', '=', 'Foo Inc'); 31 | $collection = $flatbase->execute($query); 32 | $this->assertEquals($collection->count(), 2); 33 | } 34 | 35 | public function testReadWithLessThanCondition() 36 | { 37 | $flatbase = $this->getFlatbaseWithSampleData(); 38 | $query = new ReadQuery(); 39 | $query->setCollection('users'); 40 | $query->addCondition('age', '<', 25); 41 | $collection = $flatbase->execute($query); 42 | $this->assertEquals($collection->count(), 2); 43 | } 44 | 45 | public function testReadWithMoreThanCondition() 46 | { 47 | $flatbase = $this->getFlatbaseWithSampleData(); 48 | $query = new ReadQuery(); 49 | $query->setCollection('users'); 50 | $query->addCondition('age', '>', 23); 51 | $collection = $flatbase->execute($query); 52 | $this->assertEquals($collection->count(), 3); 53 | } 54 | 55 | public function testReadWithNotEqualToCondition() 56 | { 57 | $flatbase = $this->getFlatbaseWithSampleData(); 58 | $query = new ReadQuery(); 59 | $query->setCollection('users'); 60 | $query->addCondition('name', '!=', 'Michael'); 61 | $collection = $flatbase->execute($query); 62 | $this->assertEquals($collection->count(), 3); 63 | } 64 | 65 | public function testReadWithStrictEqualToCondition() 66 | { 67 | $flatbase = $this->getFlatbaseWithSampleData(); 68 | $query = new ReadQuery(); 69 | $query->setCollection('users'); 70 | $query->addCondition('age', '==', '24'); 71 | $collection = $flatbase->execute($query); 72 | $this->assertEquals($collection->count(), 0); 73 | $query = new ReadQuery(); 74 | $query->setCollection('users'); 75 | $query->addCondition('age', '==', 24); 76 | $collection = $flatbase->execute($query); 77 | $this->assertEquals($collection->count(), 1); 78 | $count = $flatbase->read()->in('users') 79 | ->where('age', '==', 24) 80 | ->where('name', '==', 'Adam') 81 | ->get()->count(); 82 | $this->assertEquals($count, 1); 83 | } 84 | 85 | public function testReadWithStrictNotEqualToCondition() 86 | { 87 | $flatbase = $this->getFlatbaseWithSampleData(); 88 | $query = new ReadQuery(); 89 | $query->setCollection('users'); 90 | $query->addCondition('age', '!==', 24); 91 | $collection = $flatbase->execute($query); 92 | $this->assertEquals($collection->count(), 3); 93 | $query = new ReadQuery(); 94 | $query->setCollection('users'); 95 | $query->addCondition('age', '!==', '24'); 96 | $collection = $flatbase->execute($query); 97 | $this->assertEquals($collection->count(), 4); 98 | } 99 | 100 | public function testFluentQueryBuildingAliases() 101 | { 102 | $flatbase = $this->getFlatbaseWithSampleData(); 103 | $query = $flatbase->read()->in('users')->where('name', '=', 'Adam'); 104 | $users = $flatbase->execute($query); 105 | $this->assertEquals($users->count(), 3); 106 | } 107 | 108 | public function testSelfExecutionWithCount() 109 | { 110 | $flatbase = $this->getFlatbaseWithSampleData(); 111 | $this->assertEquals($flatbase->read()->in('users')->count(), 4); 112 | } 113 | 114 | public function testSelfExecutionWithGet() 115 | { 116 | $flatbase = $this->getFlatbaseWithSampleData(); 117 | $this->assertTrue($flatbase->read()->in('users')->get() instanceof Collection); 118 | } 119 | 120 | public function testFluentAliases() 121 | { 122 | $flatbase = $this->getFlatbaseWithSampleData(); 123 | $query = $flatbase->read()->in('users')->where('name', '=', 'Adam'); 124 | $users = $flatbase->execute($query); 125 | $this->assertEquals($users->count(), 3); 126 | } 127 | public function testSelfExecutionWithFirstReturnsFirstItem() 128 | { 129 | $flatbase = $this->getFlatbaseWithSampleData(); 130 | $item1 = $flatbase->read()->in('users')->get()->first(); 131 | $this->assertEquals($flatbase->read()->in('users')->first(), $item1); 132 | } 133 | 134 | public function testSelfExecutionWithFirstReturnsNullIfCollectionEmpty() 135 | { 136 | $flatbase = $this->getFlatbase(); 137 | $flatbase->delete()->in('users')->execute(); 138 | $this->assertEquals($flatbase->read()->in('users')->first(), null); 139 | } 140 | 141 | public function testLimit() 142 | { 143 | $flatbase = $this->getFlatbaseWithSampleData(); 144 | 145 | $users = $flatbase->read()->in('users')->where('name', '==', 'Adam')->setLimit(2)->execute(); 146 | $this->assertEquals($users->count(), 2); 147 | 148 | $users = $flatbase->read()->in('users')->setLimit(2)->execute(); 149 | $this->assertEquals($users->count(), 2); 150 | } 151 | 152 | public function testOffset() 153 | { 154 | $flatbase = $this->getFlatbaseWithSampleData(); 155 | 156 | $user = $flatbase->read()->in('users')->where('name', '==', 'Adam')->setOffset(1)->first(); 157 | $this->assertEquals($user['age'], 24); 158 | 159 | $user = $flatbase->read()->in('users')->where('name', '==', 'Adam')->setOffset(2)->first(); 160 | $this->assertEquals($user['age'], 25); 161 | 162 | $users = $flatbase->read()->in('users')->where('name', '==', 'Adam')->setOffset(1)->get(); 163 | $this->assertEquals($users->count(), 2); 164 | 165 | $users = $flatbase->read()->in('users')->setOffset(2)->get(); 166 | $this->assertEquals($users->count(), 2); 167 | } 168 | 169 | public function testOffsetAndLimit() 170 | { 171 | $flatbase = $this->getFlatbaseWithSampleData(); 172 | 173 | $users = $flatbase->read()->in('users')->setOffset(1)->setLimit(2)->get(); 174 | $this->assertEquals($users->first()['age'], 24); 175 | $this->assertEquals($users->count(), 2); 176 | } 177 | 178 | public function testSortOrder() 179 | { 180 | $flatbase = $this->getFlatbaseWithSampleData(); 181 | $users = $flatbase->read()->in('users')->sort('weight')->get(); 182 | $this->assertEquals($users[0]['weight'], 170); 183 | $this->assertEquals($users[1]['weight'], 180); 184 | $this->assertEquals($users[2]['weight'], 200); 185 | $this->assertEquals($users[3]['weight'], 210); 186 | } 187 | 188 | public function testSortOrderDesc() 189 | { 190 | $flatbase = $this->getFlatbaseWithSampleData(); 191 | $users = $flatbase->read()->in('users')->sortDesc('weight')->get(); 192 | $this->assertEquals($users[0]['weight'], 210); 193 | $this->assertEquals($users[1]['weight'], 200); 194 | $this->assertEquals($users[2]['weight'], 180); 195 | $this->assertEquals($users[3]['weight'], 170); 196 | } 197 | 198 | public function testSortWithLimit() 199 | { 200 | $flatbase = $this->getFlatbaseWithSampleData(); 201 | $users = $flatbase->read()->in('users')->sortDesc('weight')->limit(2)->get(); 202 | $this->assertEquals($users[0]['weight'], 210); 203 | $this->assertEquals($users[1]['weight'], 200); 204 | $this->assertEquals($users->count(), 2); 205 | } 206 | 207 | public function testSortWhenFieldDoesNotAlwaysExist() 208 | { 209 | $flatbase = $this->getFlatbaseWithSampleData(); 210 | $flatbase->update()->in('users')->where('age', '==', 24)->setValues(['foo' => 'bar'])->execute(); 211 | $users = $flatbase->read()->in('users')->sort('foo')->get(); 212 | $this->assertEquals($users[3]['foo'], 'bar'); 213 | } 214 | 215 | public function testSortDescWhenFieldDoesNotAlwaysExist() 216 | { 217 | $flatbase = $this->getFlatbaseWithSampleData(); 218 | $flatbase->update()->in('users')->where('age', '==', 24)->setValues(['foo' => 'bar'])->execute(); 219 | $users = $flatbase->read()->in('users')->sortDesc('foo')->get(); 220 | $this->assertEquals($users[0]['foo'], 'bar'); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |