├── LICENSE ├── README.md ├── composer.json └── lib ├── DocumentStoreOne.php └── services ├── DocumentStoreOneCsv.php ├── DocumentStoreOneIgBinary.php ├── DocumentStoreOneJsonArray.php ├── DocumentStoreOneJsonObj.php ├── DocumentStoreOneMsgPack.php ├── DocumentStoreOneNone.php ├── DocumentStoreOnePHP.php ├── DocumentStoreOnePHPArray.php └── IDocumentStoreOneSrv.php /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DocumentStoreOne 2 | A document store for PHP that allows multiples concurrencies. It is a minimalist alternative to MongoDB or CouchDB without the overhead of installing a new service. 3 | 4 | It also works as a small footprint database. 5 | 6 | [![Packagist](https://img.shields.io/packagist/v/eftec/documentstoreone.svg)](https://packagist.org/packages/eftec/documentstoreone) 7 | [![Total Downloads](https://poser.pugx.org/eftec/documentstoreone/downloads)](https://packagist.org/packages/eftec/documentstoreone) 8 | [![License](https://img.shields.io/badge/license-LGPLV3-blue.svg)]() 9 | [![Maintenance](https://img.shields.io/maintenance/yes/2025.svg)]() 10 | [![composer](https://img.shields.io/badge/composer-%3E2.0-blue.svg)]() 11 | [![php](https://img.shields.io/badge/php-7.4-green.svg)]() 12 | [![php](https://img.shields.io/badge/php-8.4-green.svg)]() 13 | [![Doc](https://img.shields.io/badge/docs-62%25-green.svg)]() 14 | 15 | 16 | 17 | * [DocumentStoreOne](#documentstoreone) 18 | * [Key features](#key-features) 19 | * [Test](#test-) 20 | * [Concurrency test](#concurrency-test) 21 | * [Usage](#usage) 22 | * [Methods](#methods) 23 | * [Constructor($baseFolder,$collection,$strategy=DocumentStoreOne::DSO_AUTO,$server="",$serializeStrategy = false,$keyEncryption = '')](#constructorbasefoldercollectionstrategydocumentstoreonedso_autoserverserializestrategy--falsekeyencryption--) 24 | * [isCollection($collection)](#iscollectioncollection) 25 | * [collection($collection)](#collectioncollection) 26 | * [autoSerialize($value=true,$strategy='php')](#autoserializevaluetruestrategyphp-) 27 | * [createCollection($collection)](#createcollectioncollection-) 28 | * [insertOrUpdate($id,$document,[$tries=-1])](#insertorupdateiddocumenttries-1) 29 | * [insert($id,$document,[$tries=-1])](#insertiddocumenttries-1) 30 | * [update($id,$document,[$tries=-1])](#updateiddocumenttries-1) 31 | * [get($id,[$tries=-1],$default=false)](#getidtries-1defaultfalse) 32 | * [getFiltered($id,[$tries=-1],$default=false,$condition=[],$reindex=true)](#getfilteredidtries-1defaultfalseconditionreindextrue) 33 | * [public function appendValue($name,$addValue,$tries=-1)](#public-function-appendvaluenameaddvaluetries-1) 34 | * [getNextSequence($name="seq",$tries=-1,$init=1,$interval=1,$reserveAdditional=0)](#getnextsequencenameseqtries-1init1interval1reserveadditional0) 35 | * [getSequencePHP()](#getsequencephp) 36 | * [ifExist($id,[$tries=-1])](#ifexistidtries-1) 37 | * [delete($id,[$tries=-1])](#deleteidtries-1) 38 | * [select($mask="*")](#selectmask) 39 | * [copy($idorigin,$iddestination,[$tries=-1])](#copyidoriginiddestinationtries-1) 40 | * [rename($idorigin,$iddestination,[$tries=-1])](#renameidoriginiddestinationtries-1) 41 | * [fixCast (util class)](#fixcast-util-class) 42 | * [DocumentStoreOne Fields](#documentstoreone-fields) 43 | * [MapReduce](#mapreduce) 44 | * [Limits](#limits) 45 | * [Strategy of Serialization](#strategy-of-serialization) 46 | * [NONE](#none) 47 | * [PHP](#php) 48 | * [PHP_ARRAY](#php_array) 49 | * [JSON_ARRAY and JSON_OBJECT](#json_array-and-json_object) 50 | * [Control of Error](#control-of-error) 51 | * [Working with CSV](#working-with-csv) 52 | * [Version list](#version-list) 53 | * [Pending](#pending) 54 | 55 | 56 | ## Key features 57 | - Single key based. 58 | - Fast. However, it's not an alternative to a relational database. It's optimized to store a moderated number documents instead of millions of rows. 59 | - **Allows multiple concurrences by locking and unlocking a document**. If the document is locked then, it retries until the document is unlocked or fails after a number of retries. 60 | - One single class with no dependencies. 61 | - Automatic unlock document locked (by default, every 2 minutes if the file was left locked). 62 | - It could use **MapReduce** See [example](https://github.com/EFTEC/DocumentStoreOne/blob/master/examples/4_example_read_mapreduce.php) 63 | 64 | ## Test 65 | 66 | In average, an SMB generates 100 invoices per month. So, let's say that an SMB generates 12000 invoices per decade. 67 | 68 | Testing generating 12000 invoices with customer, details (around 1-5 lines per detail) and date on an i7/ssd/16gb/windows 64bits. 69 | 70 | * Store 12000 invoices 45.303 seconds (reserving a sequence range) 71 | * Store 12000 invoices 73.203 seconds (reading a sequence for every new invoice) 72 | * Store 12000 invoices 49.0286 seconds (reserving a sequence range and using igbinary) 73 | * Reading all invoices 60.2332 seconds. (only reading) 74 | * MapReduce all invoices per customers 64.0569 seconds. 75 | * MapReduce all invoices per customers 32.9869 seconds (igbinary) 76 | * Reading all invoices from a customer **0.3 seconds.** (including render the result, see image) 77 | * Adding a new invoice without recalculating all the MapReduce 0.011 seconds. 78 | 79 | ![mapreduce example](https://github.com/EFTEC/DocumentStoreOne/blob/master/doc/mapreduce.jpg "mapreduce on php") 80 | 81 | ## Concurrency test 82 | 83 | A test with 100 concurrent test (write and read), 10 times. 84 | 85 | | N° | Reads | (ms) | Reads | Error | 86 | |-----|-------|------|-------|-------| 87 | | 1 | 100 | 7471 | 100 | 0 | 88 | | 2 | 100 | 7751 | 100 | 0 | 89 | | 3 | 100 | 7490 | 100 | 0 | 90 | | 4 | 100 | 7480 | 100 | 0 | 91 | | 5 | 100 | 8199 | 100 | 0 | 92 | | 6 | 100 | 7451 | 100 | 0 | 93 | | 7 | 100 | 7476 | 100 | 0 | 94 | | 8 | 100 | 7244 | 100 | 0 | 95 | | 9 | 100 | 7573 | 100 | 0 | 96 | | 10 | 100 | 7818 | 100 | 0 | 97 | 98 | ## Usage 99 | 100 | ```php 101 | include "lib/DocumentStoreOne.php"; 102 | use eftec\DocumentStoreOne\DocumentStoreOne; 103 | try { 104 | $flatcon = new DocumentStoreOne("base", 'tmp'); 105 | // or you could use: 106 | // $flatcon = new DocumentStoreOne(__DIR__ . "/base", 'tmp'); 107 | } catch (Exception $e) { 108 | die("Unable to create document store. Please, check the folder"); 109 | } 110 | $flatcon->insertOrUpdate("somekey1",json_encode(array("a1"=>'hello',"a2"=>'world'))); // or you could use serialize/igbinary_serialize 111 | $doc=$flatcon->get("somekey1"); 112 | $listKeys=$flatcon->select(); 113 | $flatcon->delete("somekey1"); 114 | ``` 115 | 116 | ```php 117 | include "lib/DocumentStoreOne.php"; 118 | use eftec\DocumentStoreOne\DocumentStoreOne; 119 | $doc=new DocumentStoreOne("base","task",'folder'); 120 | //also: $doc=new DocumentStoreOne(__DIR__."/base","task",'folder'); 121 | $doc->serializeStrategy='php'; // it sets the strategy of serialization to php 122 | $doc->autoSerialize(true); // autoserialize 123 | 124 | $flatcon->insertOrUpdate("somekey1",array("a1"=>'hello',"a2"=>'world')); 125 | ``` 126 | 127 | ## Methods 128 | 129 | ### Constructor($baseFolder,$collection,$strategy=DocumentStoreOne::DSO_AUTO,$server="",$serializeStrategy = false,$keyEncryption = '') 130 | 131 | It creates the DocumentStoreOne instance. 132 | 133 | * **$baseFolder**: should be a folder 134 | * **$collection**: (a subfolder) is optional. 135 | * **$strategy**: It is the strategy used to determine if the file is in use or not. 136 | 137 | | strategy | type | server | benchmark | 138 | |------------|----------------------------------------------------------------------------------------------------------|----------------|----------------| 139 | | DSO_AUTO | It sets the best available strategy (default) | depends | - | 140 | | DSO_FOLDER | It uses a folder for lock/unlock a document | - | 0.3247 | 141 | | DSO_APCU | It uses APCU for lock/unlock a document | - | 0.1480 | 142 | | DSO_REDIS | It uses REDIS for lock/unlock a document | localhost:6379 | 2.5403 (worst) | 143 | | DSO_NONE | It uses nothing to lock/unlock a document. It is the fastest method but it is unsafe for multiples users | | 0 | 144 | 145 | * **$server**: It is used by REDIS. You can set the server used by the strategy. 146 | * **$serializeStrategy**: If false then it does not serialize the information. 147 | 148 | | strategy | type | 149 | |--------------------------|----------------------------------------------------------------------------------------------------------------------------------| 150 | | php | it serializes using serialize() function | 151 | | php_array | it serializes using include()/var_export()function. The result could be cached on OpCache because the result is a PHP code file. | 152 | | json_object | it is serialized using json (as object) | 153 | | json_array | it is serialized using json (as array) | 154 | | csv | it serializes using a csv file. | 155 | | igbinary | it serializes using a igbinary file. | 156 | | **none** (default value) | it is not serialized. Information must be serialized/de-serialized manually | 157 | 158 | Examples: 159 | 160 | ```php 161 | $flatcon = new DocumentStoreOne(__DIR__ . "/base"); // new instance, using the folder /base, without serialization and with the default data 162 | 163 | $flatcon = new DocumentStoreOne(__DIR__ . "/base", '','auto','','php_array'); // new instance and serializing using php_array 164 | ``` 165 | 166 | Benchmark how much time (in seconds) it takes to add 100 inserts. 167 | 168 | ```php 169 | use eftec\DocumentStoreOne\DocumentStoreOne; 170 | include "lib/DocumentStoreOne.php"; 171 | try { 172 | $flatcon = new DocumentStoreOne(__DIR__ . "/base", 'tmp'); 173 | } catch (Exception $e) { 174 | die("Unable to create document store.".$e->getMessage()); 175 | } 176 | ``` 177 | 178 | ```php 179 | use eftec\DocumentStoreOne\DocumentStoreOne; 180 | include "lib/DocumentStoreOne.php"; 181 | try { 182 | $flatcon = new DocumentStoreOne("/base", 'tmp',DocumentStoreOne::DSO_APCU); 183 | } catch (Exception $e) { 184 | die("Unable to create document store.".$e->getMessage()); 185 | } 186 | ``` 187 | 188 | ### isCollection($collection) 189 | 190 | Returns true if collection is valid (a sub-folder). 191 | ```php 192 | $ok=$flatcon->isCollection('tmp'); 193 | ``` 194 | ### collection($collection) 195 | 196 | It sets the current collection 197 | ```php 198 | $flatcon->collection('newcollection'); // it sets a collection. 199 | ``` 200 | This command could be nested. 201 | 202 | ```php 203 | $flatcon->collection('newcollection')->select(); // it sets and return a query 204 | ``` 205 | 206 | > Note, it doesn't validate if the collection is correct or exists. You must use **isCollection()** to verify if it's right. 207 | 208 | ### autoSerialize($value=true,$strategy='php') 209 | It sets if we want to auto serialize the information, and we set how it is serialized. You can also set using the constructor. 210 | 211 | | strategy | type | 212 | |----------------------------|----------------------------------------------------------------------------------------------------------------------------| 213 | | php | it serializes using serialize() function. | 214 | | php_array | it serializes using include()/var_export()function. The result could be cached on OpCache because the result is a php file | 215 | | json_object | it is serialized using json (as object) | 216 | | json_array | it is serialized using json (as array) | 217 | | csv | it serializes using a csv file. | 218 | | igbinary | it serializes using a igbinary file. | 219 | | **none** (default value) | it is not serialized. Information must be serialized/de-serialized manually | 220 | 221 | 222 | 223 | ### createCollection($collection) 224 | 225 | It creates a collection (a new folder inside the base folder). It returns false if the operation fails; otherwise it returns true 226 | 227 | ```php 228 | $flatcon->createCollection('newcollection'); 229 | $flatcon->createCollection('/folder1/folder2'); 230 | ``` 231 | 232 | ### insertOrUpdate($id,$document,[$tries=-1]) 233 | 234 | inserts a new document (string) in the **$id** indicated. If the document exists, then it's updated. 235 | **$tries** indicates the number of tries. The default value is -1 (default number of attempts). 236 | 237 | ```php 238 | // if we are not using auto serialization 239 | $doc=json_encode(["a1"=>'hello',"a2"=>'world']); 240 | $flatcon->insertOrUpdate("1",$doc); // it will create a document called 1.dson in the base folder. 241 | 242 | // if we are using auto serialization 243 | $flatcon->insertOrUpdate("1",["a1"=>'hello',"a2"=>'world']); 244 | ``` 245 | > If the document is locked then it retries until it is available or after a "nth" number of tries (by default it's 100 tries that equivalent to 10 seconds) 246 | 247 | > It's faster than insert or update. 248 | 249 | ### insert($id,$document,[$tries=-1]) 250 | 251 | Inserts a new document (string) in the **$id** indicated. If the document exists, then it returns false. 252 | **$tries** indicates the number of tries. The default value is -1 (default number of attempts). 253 | 254 | ```php 255 | // if we are not using auto serialization 256 | $doc=json_encode(array("a1"=>'hello',"a2"=>'world')); 257 | $flatcon->insert("1",$doc); 258 | 259 | // if we are using auto serialization 260 | $flatcon->insert("1",["a1"=>'hello',"a2"=>'world']); 261 | ``` 262 | 263 | > If the document is locked then it retries until it is available or after a "nth" number of tries (by default it's 100 tries that equivalent to 10 seconds) 264 | 265 | ### update($id,$document,[$tries=-1]) 266 | 267 | Update a document (string) in the **$id** indicated. If the document doesn't exist, then it returns false 268 | **$tries** indicates the number of tries. The default value is -1 (default number of attempts). 269 | 270 | ```php 271 | // if we are not using auto serialization 272 | $doc=json_encode(["a1"=>'hello',"a2"=>'world']); 273 | $flatcon->update("1",$doc); 274 | // if we are using auto serialization 275 | $flatcon->update("1",["a1"=>'hello',"a2"=>'world']); 276 | ``` 277 | 278 | > If the document is locked then it retries until it is available or after a "nth" number of tries (by default it's 100 tries that equivales to 10 seconds) 279 | 280 | 281 | ### get($id,[$tries=-1],$default=false) 282 | 283 | It reads the document **$id**. If the document doesn't exist, or it's unable to read it, then it returns false. 284 | **$tries** indicates the number of tries. The default value is -1 (default number of attempts). 285 | 286 | ```php 287 | $doc=$flatcon->get("1"); // the default value is false 288 | 289 | $doc=$flatcon->get("1",-1,'empty'); 290 | ``` 291 | 292 | > If the document is locked then it retries until it is available or after a "nth" number of tries (by default it's 100 tries that equivalent to 10 seconds) 293 | 294 | 295 | ### getFiltered($id,[$tries=-1],$default=false,$condition=[],$reindex=true) 296 | 297 | It reads the document **$id** filtered. If the document doesn't exist, or it's unable to read it, then it returns false. 298 | **$tries** indicates the number of tries. The default value is -1 (default number of attempts). 299 | 300 | ```php 301 | // data in rows [['id'=>1,'cat'=>'vip'],['id'=>2,'cat'=>'vip'],['id'=>3,'cat'=>'normal']]; 302 | $data=$this->getFiltered('rows',-1,false,['cat'=>'normal']); // [['id'=>3,'cat'=>'normal']] 303 | $data=$this->getFiltered('rows',-1,false,['type'=>'busy'],false); // [2=>['id'=>3,'cat'=>'normal']] 304 | ``` 305 | 306 | > If the document is locked then it retries until it is available or after a "nth" number of tries (by default it's 100 tries that equivalent to 10 seconds) 307 | 308 | ### public function appendValue($name,$addValue,$tries=-1) 309 | 310 | It adds a value to a document with name **$name**. The new value is added, so it avoids to create the whole document. It is useful, for example, for a log file. 311 | 312 | a) If the value doesn't exist, then it's created with $addValue. Otherwise, it will return true 313 | b) If the value exists, then **$addValue** is added, and it'll return true 314 | c) Otherwise, it will return false 315 | 316 | ```php 317 | $seq=$flatcon->appendValue("log",date('c')." new log"); 318 | ``` 319 | 320 | ### getNextSequence($name="seq",$tries=-1,$init=1,$interval=1,$reserveAdditional=0) 321 | 322 | It reads or generates a new sequence. 323 | 324 | a) If the sequence exists, then it's incremented by **$interval** and this value is returned. 325 | b) If the sequence doesn't exist, then it's created with **$init**, and this value is returned. 326 | c) If the library is unable to create a sequence, unable to lock or the sequence exists but, it's unable to read, then it returns false 327 | 328 | ```php 329 | $seq=$flatcon->getNextSequence(); 330 | ``` 331 | 332 | > You could peek a sequence with $id=get('genseq_') however it's not recommended. 333 | 334 | > If the sequence is corrupt then it's reset to $init 335 | 336 | > If you need to reserve a list of sequences, you could use **$reserveAdditional** 337 | 338 | ```php 339 | $seq=$flatcon->getNextSequence("seq",-1,1,1,100); // if $seq=1, then it's reserved up to the 101. The next value will be 102. 340 | ``` 341 | ### getSequencePHP() 342 | 343 | It returns a unique sequence (64bit integer) based on time, a random value and a serverId. 344 | 345 | > The chances of collision (a generation of the same value) is 1/4095 (per two operations executed every 0.0001 second). 346 | 347 | ```php 348 | $this->nodeId=1; // if it is not set then it uses a random value each time. 349 | $unique=$flatcon->getSequencePHP(); 350 | ``` 351 | 352 | 353 | ### ifExist($id,[$tries=-1]) 354 | 355 | It checks if the document **$id** exists. It returns true if the document exists. Otherwise, it returns false. 356 | **$tries** indicates the number of tries. The default value is -1 (default number of tries). 357 | >The validation only happens if the document is fully unlocked. 358 | 359 | ```php 360 | $found=$flatcon->ifExist("1"); 361 | ``` 362 | 363 | > If the document is locked then it retries until it is available or after a "nth" number of tries (by default it's 100 tries that equivales to 10 seconds) 364 | 365 | ### delete($id,[$tries=-1]) 366 | 367 | It deletes the document **$id**. If the document doesn't exist, or it's unable to delete, then it returns false. 368 | **$tries** indicates the number of tries. The default value is -1 (default number of tries). 369 | 370 | ```php 371 | $doc=$flatcon->delete("1"); 372 | ``` 373 | > If the document is locked then it retries until it is available or after a "nth" number of tries (by default it's 100 tries that equivales to 10 seconds) 374 | 375 | ### select($mask="*") 376 | 377 | It returns all the IDs stored on a collection. 378 | 379 | ```php 380 | $listKeys=$flatcon->select(); 381 | $listKeys=$flatcon->select("invoice_*"); 382 | ``` 383 | > It includes locked documents. 384 | 385 | ### copy($idorigin,$iddestination,[$tries=-1]) 386 | 387 | Copy the document **$idorigin** in **$iddestination** 388 | 389 | ```php 390 | $bool=$flatcon->copy(20,30); 391 | ``` 392 | 393 | > If the document destination exists then its replaced 394 | 395 | ### rename($idorigin,$iddestination,[$tries=-1]) 396 | 397 | Rename the document **$idorigin** as **$iddestination** 398 | 399 | ```php 400 | $bool=$flatcon->rename(20,30); 401 | ``` 402 | 403 | 404 | > If the document destination exists then the operation fails. 405 | 406 | ### fixCast (util class) 407 | 408 | It converts a stdclass to a specific class. 409 | 410 | ```php 411 | $inv=new Invoice(); 412 | $invTmp=$doc->get('someid'); //$invTmp is a stdClass(); 413 | DocumentStoreOne::fixCast($inv,$invTmp); 414 | ``` 415 | 416 | > It doesn't work with members that are array of objects. The array is kept as stdclass. 417 | 418 | ## DocumentStoreOne Fields 419 | 420 | The next fields are public, and they could be changed during runtime 421 | 422 | | field | Type | 423 | |------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------| 424 | | $database | string root folder of the database | 425 | | $collection | string Current collection (subfolder) of the database | 426 | | $maxLockTime=120 | int Maximium duration of the lock (in seconds). By default it's 2 minutes | 427 | | $defaultNumRetry=100 | int Default number of retries. By default it tries 100x0.1sec=10 seconds | 428 | | $intervalBetweenRetry=100000 | int Interval (in microseconds) between retries. 100000 means 0.1 seconds | 429 | | $docExt=".dson" | string Default extension (with dot) of the document | 430 | | $keyEncryption="" | string Indicates if the key is encrypted or not when it's stored (the file name). Empty means, no encryption. You could use md5,sha1,sha256,.. | 431 | 432 | Example: 433 | ```php 434 | $ds=new DocumentStoreOne(); 435 | $ds->maxLockTime=300; 436 | ``` 437 | 438 | ```php 439 | $ds=new DocumentStoreOne(); 440 | $ds->insert('1','hello'); // it stores the document 1.dson 441 | $ds->keyEncryption='SHA256'; 442 | $ds->insert('1','hello'); // it stores the document 6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b.dson 443 | ``` 444 | 445 | 446 | ## MapReduce 447 | 448 | It could be done manually. The system allows to store a pre-calculated value that could be easily accesses (instead of read all values). 449 | 450 | Let's say the next exercise, we have a list of purchases 451 | 452 | | id | customer | age | sex | productpurchase | amount | 453 | |-----|----------|-----|-----|-----------------|--------| 454 | | 14 | john | 33 | m | 33 | 3 | 455 | | 25 | anna | 22 | f | 32 | 1 | 456 | 457 | | productcode | unitprice | 458 | |-------------|-----------| 459 | | 32 | 23.3 | 460 | | 33 | 30 | 461 | 462 | 463 | John purchased 3 products with the code 33. The products 33 costs $23.3 per unit. 464 | 465 | Question, how much every customer paid?. 466 | 467 | > It's a simple exercise, it's more suitable for a relational database (select * from purchases inner join products). 468 | > However, if the document is long or complex to store in the database then it's here where a document store shines. 469 | 470 | ```php 471 | // 1) open the store 472 | $ds=new DocumentStoreOne('base','purchases'); // we open the document store and selected the collection purchase. 473 | $ds->autoSerialize(true,'auto'); 474 | // 2) reading all products 475 | // if the list of products holds in memory then, we could store the whole list in a single document (listproducts key) 476 | $products=$ds->collection('products')->get('listproducts'); 477 | // 3) we read the keys of every purchases. It could be slow and it should be a limited set (<100k rows) 478 | $purchases=$ds->collection('purchases')->select(); // they are keys such as 14,15... 479 | 480 | $customerXPurchase=[]; 481 | // 4) We read every purchase. It is also slow. Then we merge the result and obtained the final result 482 | foreach($purchases as $k) { 483 | $purchase=$ds->get($k); 484 | @$customerXPurchase[$purchase->customer]+=($purchase->amount * @$products[$purchase->productpurchase]); // we add the amount 485 | } 486 | // 5) Finally, we store the result. 487 | $ds->collection('total')->insertOrUpdate($customerXPurchase,'customerXPurchase'); // we store the result. 488 | ``` 489 | 490 | | customer | value | 491 | |----------|-------| 492 | | john | 69.9 | 493 | | anna | 30 | 494 | 495 | Since it's done on code then it's possible to create a hybrid system (relational database+store+memory cache) 496 | 497 | ## Limits 498 | - Keys should be of the type A-a,0-9. In windows, keys are not case-sensitive. 499 | - The limit of documents that a collection could hold is based on the document system used. NTFS allows 2 million 500 | of documents per collection. 501 | 502 | 503 | 504 | # Strategy of Serialization 505 | 506 | Let's say we want to serialize the next information: 507 | 508 | ```php 509 | $input=[['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']]; 510 | ``` 511 | 512 | ## NONE 513 | 514 | The values are not serialized, so it is not possible to serialize an object, array or other structure. It only works with strings. 515 | 516 | How values are stored 517 | 518 | ``` 519 | helloworld 520 | ``` 521 | 522 | How values are returned 523 | 524 | ```php 525 | "helloworld" 526 | ``` 527 | 528 | 529 | 530 | ## PHP 531 | 532 | The serialization of PHP is one of the faster way to serialize and de-serialize, and it always returns the same value with the same structure (classes, array, fields) 533 | 534 | However, the value stored could be long. 535 | 536 | How the values are stored: 537 | 538 | ``` 539 | a:2:{i:0;a:2:{s:2:"a1";i:1;s:2:"a2";s:1:"a";}i:1;a:2:{s:2:"a1";i:2;s:2:"a2";s:1:"b";}} 540 | ``` 541 | 542 | How the values are returned: 543 | 544 | ``` 545 | [['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']] 546 | ``` 547 | 548 | ## PHP_ARRAY 549 | 550 | This serialization generates a PHP code. This code is verbose however, it has some nice features: 551 | 552 | * It could be cached by PHP's OPcache. 553 | * It's fast to load. 554 | 555 | How the values are stored: 556 | 557 | ```php 558 | 561 | array ( 562 | 'a1' => 1, 563 | 'a2' => 'a', 564 | ), 565 | 1 => 566 | array ( 567 | 'a1' => 2, 568 | 'a2' => 'b', 569 | ), 570 | ); 571 | ``` 572 | 573 | How the values are returned: 574 | 575 | ``` 576 | [['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']] 577 | ``` 578 | 579 | ## JSON_ARRAY and JSON_OBJECT 580 | 581 | Both methods work with JSON for the serialization and de-serialization but the first on returns always an associative 582 | array while the other could return an object (stdClass) 583 | 584 | Pro: 585 | 586 | * JSON is fast (but not as fast a PHP's serialization) 587 | * JSON is compatible across different platforms. 588 | * JSON uses fewer space than PHP?s serialization. 589 | 590 | Cons: 591 | 592 | * It is a big slower than PHP's serialization 593 | * The result could vary, and it could return a different structure (objects are always returned as stdClass) 594 | 595 | How the values are stored: 596 | 597 | ``` 598 | [{"a1":1,"a2":"a"},{"a1":2,"a2":"b"}] 599 | ``` 600 | 601 | How the values are returned: 602 | 603 | ```php 604 | [['a1'=>1,'a2'=>'a'],['a1'=>2,'a2'=>'b']] // array 605 | [stdClass{'a1'=>1,'a2'=>'a'},stdClass{'a1'=>2,'a2'=>'b'}] // object 606 | ``` 607 | 608 | 609 | 610 | # Control of Error 611 | 612 | By default, this library throws errors when an error or exception happens. Some methods allow to avoid throwing errors but most of them could throw an error. 613 | 614 | The errors are try/catch catch-ables. 615 | 616 | ```php 617 | // throw an error: 618 | $this->throwable=true; // (default value is true) If false, then the errors are stored in $this->latestError 619 | try { 620 | $this->insert('id1','values'); 621 | } catch($ex) { 622 | var_dump($ex); 623 | } 624 | ``` 625 | 626 | ```php 627 | // not throw an error: 628 | $this->throwable=false; 629 | $this->insert('id1','values'); 630 | if($this->latestError) { 631 | var_dump($this->latestError); 632 | } 633 | $this->resetError(); 634 | 635 | ``` 636 | 637 | Or you could also use to avoid throwing an exception: 638 | 639 | ```php 640 | // not throw an error: 641 | $this->->noThrowOnError()->insert('id1','values'); 642 | // but you can still see the latest error: 643 | if($this->latestError) { 644 | var_dump($this->latestError); 645 | } 646 | ``` 647 | 648 | 649 | 650 | # Working with CSV 651 | 652 | You can work with CSV as follows: 653 | 654 | ```php 655 | $doc=new DocumentStoreOne(__DIR__ . "/base",'','none','','csv'); // set your strategy to csv. 656 | $doc->docExt='.csv'; // (optional), you can set the extension of the document 657 | $doc->csvPrefixColumn='col_'; // (optional), you can set the name of the columns (if the csv doesn't have columns) 658 | $doc->csvStyle(); // (optional) not needing, but you can use to set your own specifications of csv, for example tab-separated, etc. 659 | $doc->regionalStyle(); // (optional) not needing, but you can use to set your own regional settings. 660 | $values=[ 661 | ['name'=>'john1','age'=>22], 662 | ['name'=>'john2','age'=>22], 663 | ['name'=>'john3','age'=>22], 664 | ]; 665 | $doc->delete('csv1'); 666 | $doc->insert('csv1',$values); 667 | ``` 668 | 669 | 670 | 671 | # Version list 672 | * 1.28 2024-12-31 673 | * Updated to PHP 8.4 674 | * 1.27 2024-07-19 675 | * Fixed a problem with unlock (folder strategy), when the folder does not exist anymore. 676 | * 1.26 2024-02-13 677 | * composer.json ig-binary is suggested, not required 678 | * 1.25.1 2023-06-04 679 | * fixed a bug in the constructor. Now it generates the folders if they don't exist. 680 | * 1.25 2023-06-04 681 | * added DocumentStoreOne::isRelativePath() 682 | * now you can specify a relative path in the constructor 683 | * 1.24 2022-06-29 684 | * deleteCollection() deletes the collection including it's content. 685 | * 1.23 2022-03-20 686 | * **[new]** It allows to obtain an instance (if any) of DocumentStoreOne using the static method DocumentStoreOne::instance() 687 | * 1.22.1 2022-03-12 688 | * getTimeStamp() Fixed: returns a warning if the file does not exist. 689 | * 1.22 2022-03-12 690 | * added setTimeStamp() 691 | * 1.21 2022-02-07 692 | * compatibility with PHP 7.2 and higher. This library is not compatible with PHP 5.6 anymore, but you can use an old version of the library. 693 | * Tested the compatibility with PHP 8.1 694 | * **[added]** method noThrowOnError() 695 | - 1.20 2021-12-11 696 | - add igbinary 697 | - 1.19 2021-12-08 698 | * **[added]** more controls over the errors. 699 | - 1.18 2021-12-08 700 | * **[added]** csv as serialization strategy 701 | * Some optimizations 702 | * Memcache is removed. 703 | - 1.16.2 2020-09-20 704 | * getTimeStamp() throws an exception when the file doesn't exist. Now it returns false. 705 | - 1.16 2020-09-20 706 | * new method getTimeStamp() 707 | - 1.15 2020-09-13 708 | * method get() now unlocks a document correctly (using method php_array) 709 | * method appendValue() is more efficient with json_object,json_array, and it works with php_array. 710 | * method appendValue() now generates an array of values. 711 | - 1.14 2020-09-13 712 | * Fixed composer.json. However, the previous composer.json poisoned installations, so it removed all the previous 713 | version from packagist. 714 | * Maybe you should delete "composer.lock" and the folder vendor\efted\documentstoreone and runs composer update. 715 | > [RuntimeException] 716 | > Could not load package eftec/documentstoreone in repo.packagist.org: [UnexpectedValueException] Could not parse version constraint ^5.6.*: Invalid version string "^5.6.*" 717 | 718 | 719 | - 1.13 2020-07-12 720 | * method appendValue() now serializes information and works with most method but php_array. 721 | - 1.12 2020-04-18 722 | * method get() has a default value 723 | * method unlock() removed the argument $forced 724 | * new method getFiltered() 725 | - 1.11 2019-10-23 726 | * new method setObjectIndex() It sets the default index field for insertObject() and insertOrUpdateObject() 727 | * new method insertObject() 728 | * new method insertOrUpdateObject() 729 | * method select() now could return a list of indexes of a list of documents 730 | - 1.10 2019-08-30 Some cleaning. Added getSequencePHP() and field nodeId 731 | - 1.9 2019-02-10 Unlock now tries to unlock. Manuallock field is not used anymore. 732 | - 1.8 2018-02-03 field neverLock (for fast access a read only database) also phpunit 733 | - 1.7.3 2018-02-03 Updated composer.json 734 | - 1.7.1 2018-10-20 Removed an incorrect echo on lock() 735 | - 1.7 2018-10-20 Added key encryption (optional) 736 | - 1.6 2018-10-19 737 | - - Reduced the default time from 30 seconds to 10 seconds because usually PHP is configured to a timeout of 30 seconds. 738 | - - Method ifExist locks the resource and never releases. Now it releases as expected. 739 | - 1.5 2018-10-13 Maintenance update. Fixed the automatic strategy 740 | - 1.4 2018-08-26 function rename 741 | - 1.3 2018-08-15 Added strategy of lock. 742 | - 1.2 2018-08-12 Small fixes. 743 | - 1.1 2018-08-12 Changed schema with collection. 744 | - 1.0 2018-08-11 first version 745 | 746 | ## Pending 747 | 748 | - Transactional (allows to commit or rollback a multiple step transaction). It's in evaluation. 749 | - ~~Different strategy of lock (folder,redis and apcu)~~ 750 | - Msgpack and ~~igbinary~~ 751 | 752 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eftec/documentstoreone", 3 | "description": "A flat document store for PHP that allows multiples concurrencies.", 4 | "type": "library", 5 | "keywords": ["php-library","php","database","file","mapreduce","bigdata"], 6 | "homepage": "https://github.com/EFTEC/DocumentStoreOne", 7 | "license": "LGPL-3.0-only", 8 | "autoload": { 9 | "psr-4": { 10 | "eftec\\DocumentStoreOne\\": "lib/" 11 | } 12 | }, 13 | "autoload-dev": { 14 | "psr-4": { 15 | "eftec\\tests\\": "tests/" 16 | } 17 | }, 18 | "authors": [ 19 | { 20 | "name": "Jorge Castro Castillo", 21 | "email": "jcastro@eftec.cl" 22 | }], 23 | "suggest": { 24 | "ext-redis": "If you want to use redis for locks. It is not recommended becuase it's slow than folder", 25 | "ext-apcu": "If you want to use apcu for locks. It is recommended", 26 | "ext-igbinary": "Some examples could use it" 27 | 28 | }, 29 | "require": { 30 | "php": ">=7.4", 31 | "ext-json": "*" 32 | }, 33 | "require-dev": { 34 | "ext-redis": "*", 35 | "ext-apcu":"*", 36 | "phpunit/phpunit": "^8.5.13" 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /lib/DocumentStoreOne.php: -------------------------------------------------------------------------------- 1 | 80 | * It must be between 0..1023
81 | * If the value is -1, then it randomizes its value each call. 82 | */ 83 | public int $nodeId = 1; 84 | protected bool $autoSerialize = false; 85 | public bool $throwable = true; 86 | public ?string $latestError = ''; 87 | /** @var DocumentStoreOne|null */ 88 | protected static ?DocumentStoreOne $instance=null; 89 | /** 90 | * @var string= 'sequencephp' (snowfake generation),'nextsequence' (generated by a document) or the name of the 91 | * field. 92 | */ 93 | protected string $objectIndex; 94 | 95 | /** 96 | * DocumentStoreOne constructor.
97 | * **Example:** 98 | * ``` 99 | * $obj=new DocumentStoreOne(__DIR__."/base",'collectionFolder'); // using __DIR__ 100 | * $obj=new DocumentStoreOne("/path/p2",'collectionFolder'); // using absolute path 101 | * $obj=new DocumentStoreOne("c:\path\p2",'collectionFolder'); // using absolute path (Windows) 102 | * $obj=new DocumentStoreOne("path/p2",'collectionFolder'); // using relative path (relative to the first PHP) 103 | * ``` 104 | * 105 | * @param string $database root folder of the database 106 | * @param string $collection collection (subfolder) of the database. If the namr of the collection is 107 | * empty then it uses the root folder. 108 | * @param string $strategy =['auto','folder','apcu','redis','none'][$i] The strategy is 109 | * only used to lock/unlock purposes. 110 | * @param string $server Used for 'redis' (localhost:6379) 111 | * @param bool|string $serializeStrategy =['php','php_array','json_object','json_array','csv','igbinary','msgpack','none'][$i] 112 | * It sets the strategy of serialization
If false then the value 113 | * (inserted) is auto serialized
114 | * @param string $keyEncryption =['','md5','sha1','sha256','sha512'][$i] it uses to encrypt the name of 115 | * the keys 116 | * (filename) 117 | * 118 | * @throws Exception 119 | * @example $flatcon=new DocumentStoreOne(__DIR__."/base",'collectionFolder'); 120 | */ 121 | public function __construct( 122 | string $database, 123 | string $collection = '', 124 | string $strategy = 'auto', 125 | string $server = "", 126 | $serializeStrategy = false, 127 | string $keyEncryption = '' 128 | ) 129 | { 130 | if (self::isRelativePath($database)) { 131 | $database = dirname($_SERVER['SCRIPT_FILENAME']) . DIRECTORY_SEPARATOR . $database; 132 | } 133 | $this->database = $database; 134 | $this->collection = $collection; 135 | if ($serializeStrategy === false) { 136 | $this->autoSerialize = false; 137 | $this->srvSerialize = new DocumentStoreOneNone($this); 138 | } else if ($serializeStrategy === true) { 139 | // it is for old compatibility 140 | $this->srvSerialize = new DocumentStoreOneNone($this); 141 | $this->autoSerialize = true; 142 | } else { 143 | $this->autoSerialize(true, $serializeStrategy); 144 | } 145 | $this->keyEncryption = $keyEncryption; 146 | $this->setStrategy($strategy, $server); 147 | if (!is_dir($this->getPath())) { 148 | $backup = $this->collection; 149 | try { 150 | $this->createCollection("", false); // create base 151 | $this->createCollection($backup, false); // create collection and return previous configuration. 152 | } catch (Exception $ex) { 153 | $this->throwError("the folder is incorrect or I'm not unable to read it: " 154 | . $this->getPath() . 155 | '. ' . $ex->getMessage()); 156 | } 157 | } 158 | if (self::$instance === null) { 159 | self::$instance = $this; 160 | } 161 | } 162 | 163 | /** 164 | * It returns the first instance created DocumentStoreOne or throw an error if the instance is not set.
165 | * **Example:** 166 | * ``` 167 | * $doc=DocumentStoreOne::instance(); 168 | * ``` 169 | * @param bool $throwIfNull if true and the instance is not set, then it throws an exception
170 | * if false and the instance is not set, then it returns null 171 | * @return DocumentStoreOne|null 172 | */ 173 | public static function instance(bool $throwIfNull = true): ?DocumentStoreOne 174 | { 175 | if (self::$instance === null && $throwIfNull) { 176 | throw new RuntimeException('instance not created for DocumentStoreOne'); 177 | } 178 | return self::$instance; 179 | } 180 | 181 | /** 182 | * It is a utility function that returns true if the path supplied is absolute or relative
183 | * **Example:** 184 | * ``` 185 | * $isRel=DocumentStoreOne::isRelativePath('somepath/path'); 186 | * ``` 187 | * @param string $path 188 | * @return bool 189 | */ 190 | public static function isRelativePath(string $path): bool 191 | { 192 | $l = strlen($path); 193 | if ($l > 0 && $path[0] === '/') { // *nix absolute path 194 | return false; 195 | } 196 | if ($l > 2 && $path[1] === ':') { // windows absolute path 197 | return false; 198 | } 199 | return true; 200 | } 201 | 202 | /** 203 | * It returns true if it is using a tabular value
204 | * **Example:** 205 | * ``` 206 | * $isTabular=$this->isTabular(); 207 | * ``` 208 | * @return bool 209 | */ 210 | public function isTabular(): bool 211 | { 212 | return $this->tabular; 213 | } 214 | 215 | /** 216 | * It sets the strategy to lock and unlock the folders
217 | * **Example:** 218 | * ``` 219 | * $this->setStrategy('folder'); 220 | * $this->setStrategy('redis','127.0.0.1:6379'); 221 | * ``` 222 | * 223 | * @param string|int $strategy =['auto','folder','apcu','redis','none'][$i] 224 | * @param string $server 225 | * 226 | * @throws Exception 227 | * @noinspection ClassConstantCanBeUsedInspection 228 | */ 229 | public function setStrategy($strategy, string $server = ""): void 230 | { 231 | if ($strategy === self::DSO_AUTO) { 232 | if (function_exists('apcu_add')) { 233 | $this->strategy = self::DSO_APCU; 234 | } else { 235 | $this->strategy = self::DSO_FOLDER; 236 | } 237 | $strategy = $this->strategy; 238 | } else { 239 | $this->strategy = $strategy; 240 | } 241 | switch ($strategy) { 242 | case self::DSO_NONE: 243 | case self::DSO_FOLDER: 244 | break; 245 | case self::DSO_APCU: 246 | if (!function_exists("apcu_add")) { 247 | $this->throwError('APCU is not defined'); 248 | return; 249 | } 250 | break; 251 | case self::DSO_REDIS: 252 | /** @noinspection ClassConstantCanBeUsedInspection */ 253 | if (!class_exists("\Redis")) { 254 | $this->throwError('Redis is not defined'); 255 | return; 256 | } 257 | if (function_exists('cache')) { 258 | /** @noinspection PhpUndefinedFunctionInspection */ 259 | $this->redis = cache();// inject using the cache function (if any). 260 | } else { 261 | $this->redis = new Redis(); 262 | $host = explode(':', $server); 263 | $r = @$this->redis->pconnect($host[0], $host[1] ?? 6379, 30); // 30 seconds timeout 264 | if (!$r) { 265 | $this->throwError('Unable to open Redis, check the server and connections.'); 266 | return; 267 | } 268 | } 269 | break; 270 | default: 271 | $this->throwError("Strategy not defined"); 272 | } 273 | $this->resetChain(); 274 | } 275 | 276 | /** 277 | * It gets the current path.
278 | * **Example:** 279 | * ``` 280 | * $path=$this->getPath(); 281 | * ``` 282 | * 283 | * @return string 284 | */ 285 | protected function getPath(): string 286 | { 287 | return $this->database . "/" . $this->collection; 288 | } 289 | 290 | /** 291 | * Util function to fix the cast of an object using an empty object.
292 | * **Example:** 293 | * ``` 294 | * utilCache::fixCast($objectRightButEmpty,$objectBadCast); 295 | * ``` 296 | * 297 | * @param object|array $destination Object may be empty with the right cast. 298 | * @param object|array $source Object with the wrong cast. 299 | * 300 | * @return void 301 | */ 302 | public static function fixCast(&$destination, $source): void 303 | { 304 | if (is_array($source)) { 305 | $getClass = get_class($destination[0]); 306 | $array = []; 307 | foreach ($source as $sourceItem) { 308 | $obj = new $getClass(); 309 | self::fixCast($obj, $sourceItem); 310 | $array[] = $obj; 311 | } 312 | $destination = $array; 313 | } else { 314 | $sourceReflection = new ReflectionObject($source); 315 | $sourceProperties = $sourceReflection->getProperties(); 316 | foreach ($sourceProperties as $sourceProperty) { 317 | $name = $sourceProperty->getName(); 318 | if (is_object(@$destination->{$name})) { 319 | if (get_class(@$destination->{$name}) === "DateTime") { 320 | // source->name is a stdclass, not a DateTime, so we could read the value with the field date 321 | try { 322 | $destination->{$name} = new DateTime($source->$name->date); 323 | } catch (Exception $e) { 324 | $destination->{$name} = null; 325 | } 326 | } else { 327 | self::fixCast($destination->{$name}, $source->$name); 328 | } 329 | } else { 330 | $destination->{$name} = $source->$name; 331 | } 332 | } 333 | } 334 | } 335 | 336 | /** 337 | * Set if we need to lock/unlock every time we want to read/write a value 338 | * 339 | * @param bool $neverLock if its true then the register is never locked. It is fast but it's not concurrency-safe 340 | */ 341 | public function setNeverLock(bool $neverLock = true): void 342 | { 343 | $this->neverLock = $neverLock; 344 | } 345 | 346 | /** 347 | * It appends a value to an existing document. It creates an array of values if it doesn't exist.
348 | * **Example:** 349 | * ``` 350 | * $this->appendvalue('20',[1,2,3]); // document "20" = [[1,2,3]] 351 | * $this->appendvalue('20',[3,4,5]); // document "20" = [[1,2,3],[3,4,5]] 352 | * ``` 353 | * 354 | * @param string $id "ID" of the document. 355 | * @param mixed $addValue This value could be serialized. 356 | * @param int $tries number of tries. The default value is -1 (it uses the default value $defaultNumRetry) 357 | * 358 | * @return bool It returns false if it fails to lock the document or if it's unable to read the document. Otherwise 359 | * it returns true 360 | * @throws Exception 361 | */ 362 | public function appendValue(string $id, $addValue, int $tries = -1): bool 363 | { 364 | $this->currentId = $id; 365 | $file = $this->filename($id); 366 | if ($this->lock($file, $tries)) { 367 | try { 368 | $result = $this->srvSerialize->appendValue($file, $id, $addValue, $tries); 369 | $this->unlock($file, $tries); 370 | $this->resetChain(); 371 | return $result; 372 | } catch (Exception $ex) { 373 | $this->unlock($file, $tries); 374 | $this->throwError($ex); 375 | } 376 | } 377 | $this->throwError("unable to lock file [$this->currentId]"); 378 | return false; // unable to lock 379 | } 380 | 381 | /** 382 | * Used internally 383 | * @param $file 384 | * @param $id 385 | * @param $addValue 386 | * @param int $tries 387 | * @return bool|resource 388 | */ 389 | public function appendValueDecorator($file, $id, $addValue, int $tries = -1) 390 | { 391 | if ($this->ifExist($id, null)) { 392 | $fp = @fopen($file, 'rb+'); 393 | } else { 394 | $this->unlock($file); 395 | $this->resetChain(); 396 | return $this->insert($id, [$addValue], $tries); 397 | } 398 | if ($fp === false) { 399 | $this->unlock($file); 400 | $this->throwError(error_get_last()); 401 | return false; // file exists but I am unable to open it. 402 | } 403 | $this->resetChain(); 404 | return $fp; 405 | } 406 | 407 | /** 408 | * For internal use.
409 | * It is used when the service is unable to insert an information without modify the whole file
410 | * So it modifies the whole file 411 | * @param $filePath 412 | * @param $addValue 413 | * @return bool 414 | */ 415 | public function appendValueRaw($filePath, $addValue): bool 416 | { 417 | $fp = @fopen($filePath, 'ab'); 418 | if ($fp === false) { 419 | $this->unlock($filePath); 420 | $this->throwError(error_get_last()); 421 | return false; // file exists but I am unable to open it. 422 | } 423 | $addValue = $this->serialize($addValue); 424 | if ($this->tabular) { 425 | $addValue = $this->separatorAppend . $addValue; 426 | } 427 | $r = @fwrite($fp, $addValue); 428 | @fclose($fp); 429 | $this->unlock($filePath); 430 | if ($r === false) { 431 | $this->throwError(error_get_last()); 432 | } 433 | $this->resetChain(); 434 | return ($r !== false); 435 | } 436 | 437 | /** 438 | * Used internally. 439 | * @param $id 440 | * @param $addValue 441 | * @param int $tries 442 | * @return bool 443 | * @noinspection PhpUnusedParameterInspection 444 | * @throws RedisException 445 | */ 446 | public function appendValueRaw2($id, $addValue, int $tries = -1): bool 447 | { 448 | // it is called pre-locked, so we won't lock or unlock 449 | $content = $this->get($id, 0); 450 | if ($content === false) { 451 | $this->throwError('Append: unable to read file', true); 452 | return false; // unable to read 453 | } 454 | if (!is_array($content)) { 455 | $this->throwError('Append: content is not an array, unable to insert', true); 456 | return false; // not able to add 457 | } 458 | $content[] = $addValue; 459 | $this->resetChain(); 460 | return $this->update($id, $content, 0); // it is called pre-locked, so we won't lock or unlock 461 | } 462 | 463 | /** 464 | * Convert "ID" to a full filename. If keyencryption then the name is encrypted.
465 | * **Example:** 466 | * ``` 467 | * $fullfilename=$this->filename('doc20'); // "/folder/somefolder/doc20.dson" 468 | * ``` 469 | * 470 | * @param string $id 471 | * 472 | * @return string full filename 473 | */ 474 | public function filename(string $id): string 475 | { 476 | $file = $this->keyEncryption ? hash($this->keyEncryption, $id) : $id; 477 | return $this->getPath() . "/" . $file . $this->docExt; 478 | } 479 | 480 | /** 481 | * It locks a file depending on the current strategy. 482 | * 483 | * @param $filepath 484 | * @param int $maxRetry the number of times that it will retry to lock the file
> 485 | * -1 means it uses the default value.
486 | * 0 means it will not try to lock
487 | * @return bool returns false if it fails 488 | */ 489 | protected function lock($filepath, int $maxRetry = -1): bool 490 | { 491 | if ($this->neverLock || $maxRetry === 0) { 492 | return true; 493 | } 494 | $maxRetry = ($maxRetry === -1) ? $this->defaultNumRetry : $maxRetry; 495 | try { 496 | switch ($this->strategy) { 497 | case self::DSO_APCU: 498 | $try = 0; 499 | while (@apcu_add("documentstoreone." . $filepath, 1, $this->maxLockTime) === false && $try < $maxRetry) { 500 | $try++; 501 | usleep($this->intervalBetweenRetry); 502 | } 503 | return ($try < $maxRetry); 504 | case self::DSO_REDIS: 505 | $try = 0; 506 | while (@$this->redis->set("documentstoreone." . $filepath, 1, ['NX', 'EX' => $this->maxLockTime]) !== 507 | true 508 | && $try < $maxRetry) { 509 | $try++; 510 | usleep($this->intervalBetweenRetry); 511 | } 512 | return ($try < $maxRetry); 513 | case self::DSO_FOLDER: 514 | clearstatcache(); 515 | $lockname = $filepath . ".lock"; 516 | $life = @filectime($lockname); // $life=false is ok (the file doesn't exist) 517 | $try = 0; 518 | while (!@mkdir($lockname, 0755) && is_dir($lockname) && $try < $maxRetry) { 519 | $try++; 520 | if ($life && (time() - $life) > $this->maxLockTime) { 521 | rmdir($lockname); 522 | $life = false; 523 | } 524 | usleep($this->intervalBetweenRetry); 525 | } 526 | return ($try < $maxRetry); 527 | case self::DSO_NONE: 528 | return true; 529 | } 530 | } catch (Exception $ex) { 531 | } 532 | return false; 533 | } 534 | 535 | /** 536 | * Return if the document exists. It doesn't check until the document is fully unlocked unless $tries=null
537 | * **Example:** 538 | * ``` 539 | * $found=$this->ifExist('doc20'); 540 | * ``` 541 | * @param string $id "ID" of the document. 542 | * @param int|null $tries number of tries.
543 | * The default value is -1 (it uses the default value $defaultNumRetry)
544 | * If $tries=null then it never checks the lock. 545 | * 546 | * @return bool True if the information was read, otherwise false. 547 | */ 548 | public function ifExist(string $id, ?int $tries = -1): bool 549 | { 550 | $file = $this->filename($id); 551 | if ($tries !== null) { 552 | if ($this->lock($file, $tries)) { 553 | $exist = file_exists($file); 554 | $this->unlock($file); 555 | return $exist; 556 | } 557 | } else { 558 | return file_exists($file); 559 | } 560 | return false; 561 | } 562 | 563 | /** 564 | * Unlocks a document 565 | * 566 | * @param string $filePath full file path/key of the document to unlock. 567 | * @param int $tries 568 | * @return bool returns true if the unlocks works. Otherwise, it returns false. 569 | */ 570 | public function unlock(string $filePath, int $tries = -1): bool 571 | { 572 | if ($this->neverLock || $tries === 0) { 573 | return true; 574 | } 575 | try { 576 | switch ($this->strategy) { 577 | case self::DSO_APCU: 578 | return apcu_delete("documentstoreone." . $filePath); 579 | case self::DSO_REDIS: 580 | return ($this->redis->del("documentstoreone." . $filePath) > 0); 581 | case self::DSO_NONE: 582 | return true; 583 | } 584 | $unlockname = $filePath . ".lock"; 585 | $tries = $tries === -1 ? $this->defaultNumRetry : $tries; 586 | $try = 0; 587 | // retry to delete the unlockname folder. If fails then it tries it again. 588 | while (!@rmdir($unlockname) && $try < $tries) { 589 | $try++; 590 | if (!is_dir($unlockname)) { // is not directory or it was already deleted. 591 | break; 592 | } 593 | usleep($this->intervalBetweenRetry); 594 | } 595 | return ($try < $this->defaultNumRetry); 596 | } catch (Exception $ex) { 597 | return false; 598 | } 599 | } 600 | 601 | /** 602 | * Add a document.
603 | * **Example** 604 | * ``` 605 | * $ok=$this->insert("doc20",$values); 606 | * ``` 607 | * 608 | * @param string $id "ID" of the document. 609 | * @param string|array $document The document 610 | * @param int $tries number of tries. The default value is -1 (it uses the default value 611 | * $defaultNumRetry) 612 | * 613 | * @return bool True if the information was added, otherwise false 614 | */ 615 | public function insert(string $id, $document, int $tries = -1): bool 616 | { 617 | $file = $this->filename($id); 618 | $this->currentId = $id; 619 | if ($this->lock($file, $tries)) { 620 | if ((!is_array($document) || !isset($document[0])) && $this->tabular) { 621 | $write = false; 622 | $errorLast = 'Data is not tabular'; 623 | } else if (!file_exists($file)) { 624 | if ($this->autoSerialize) { 625 | $this->srvSerialize->insert($id, $document, $tries); 626 | // note: for the csv, it includes the header (if the csv uses headers) 627 | $write = @file_put_contents($file, $this->serialize($document), LOCK_EX); 628 | } else { 629 | $write = @file_put_contents($file, $document, LOCK_EX); 630 | } 631 | $errorLast = @error_get_last(); 632 | } else { 633 | $write = false; 634 | $errorLast = "File does exist [$this->currentId]. Unable to override file"; 635 | } 636 | if ($write === false) { 637 | @$this->unlock($file); 638 | $this->throwError($errorLast); 639 | return false; 640 | } 641 | $this->unlock($file); 642 | $this->resetChain(); 643 | return true; 644 | } 645 | $this->unlock($file); 646 | $this->throwError("Unable to lock file [$this->currentId]"); 647 | return false; 648 | } 649 | 650 | /** 651 | * @param mixed $document The document content to serialize. 652 | * @param bool $special used by serialize_php_array() and serializeCSV() 653 | * 654 | * @return string 655 | */ 656 | public function serialize($document, bool $special = false): string 657 | { 658 | return $this->srvSerialize->serialize($document, $special); 659 | } 660 | 661 | 662 | //region csv 663 | 664 | /** 665 | * Returns true if the input is a table (a 2-dimensional array) 666 | * @param mixed $table 667 | * @return bool 668 | */ 669 | public function isTable($table): bool 670 | { 671 | return isset($table[0]) && is_array($table[0]); 672 | } 673 | 674 | public function getType($input): string 675 | { 676 | // array 677 | if (is_array($input)) { 678 | return 'array'; 679 | } 680 | // int? 681 | if ($this->isInt($input)) { 682 | return 'int'; 683 | } 684 | // decimal value? 685 | if ($this->regionDecimal !== '.') { 686 | $inputD = str_replace($this->regionDecimal, '.', $input); 687 | } else { 688 | $inputD = $input; 689 | } 690 | if (is_numeric($inputD)) { 691 | return 'decimal'; 692 | } 693 | // datetime? 694 | $inputD = ($input instanceof DateTime) ? $input : DateTime::createFromFormat($this->regionDateTime, $input); 695 | if ($inputD instanceof DateTime) { 696 | return 'datetime'; 697 | } 698 | // date 699 | $inputD = DateTime::createFromFormat($this->regionDate, $input); 700 | if ($inputD instanceof DateTime) { 701 | return 'date'; 702 | } 703 | return is_string($input) ? 'string' : 'oobject'; 704 | } 705 | 706 | public function isInt($input): bool 707 | { 708 | if ($input === null || $input === '') { 709 | return false; 710 | } 711 | if (is_int($input)) { 712 | return true; 713 | } 714 | if (is_float($input) || is_object($input)) { 715 | return false; 716 | } 717 | if ($input[0] === '-') { 718 | return ctype_digit(substr($input, 1)); 719 | } 720 | return ctype_digit($input); 721 | } 722 | 723 | public function convertTypeBack($input, $type) 724 | { 725 | return $this->srvSerialize->convertTypeBack($input, $type); 726 | } 727 | 728 | public function convertType($input, $type) 729 | { 730 | return $this->srvSerialize->convertType($input, $type); 731 | } 732 | 733 | //endregion 734 | public static function serialize_php_array($document, $special = false): ?string 735 | { 736 | if ($special) { 737 | // for append 738 | return var_export($document, true); 739 | } 740 | return "objectIndex = $indexField; 751 | } 752 | 753 | /** 754 | * It inserts (or update) an associative array or an object into the document store
755 | * Example:
756 | * $obj=['id'=>1,'name'=>'john'];
757 | * $doc->insertOrUpdateObject($obj,'id');
758 | * 759 | * @param array|object $object The object (or associative array) to store 760 | * @param string|null $indexField =['sequencephp','nextsequence'][$i]
761 | * if null then it uses the default index field defined by setObjectIndex()
762 | * if sequencephp then it uses snowflake for generate a new sequence
763 | * if nextsequence then it uses a document sequence
764 | * 765 | * @return string the index value 766 | * @throws Exception 767 | * @throws Exception 768 | * @see DocumentStoreOne::setObjectIndex 769 | */ 770 | public function insertOrUpdateObject($object, ?string $indexField = null) 771 | { 772 | return $this->insertObject($object, $indexField, 'insertorupdate'); 773 | } 774 | 775 | /** 776 | * It inserts an associative array or an object into the document store 777 | * 778 | * @param array|object $object The object to store 779 | * @param string|null $indexField =['sequencephp','nextsequence'][$i]
780 | * if null then it uses the default index field defined by setObjectIndex()
781 | * if sequencephp then it uses snowflake for generate a new sequence
782 | * if nextsequence then it uses a document sequence
783 | * if the value is tabular, then it uses the first row to find the id
784 | * 785 | * @param string $operation =['insert','insertorupdate'][$i] 786 | * 787 | * @return string the index value 788 | * @throws Exception 789 | * @see DocumentStoreOne::setObjectIndex 790 | */ 791 | public function insertObject($object, ?string $indexField = null, string $operation = 'insert') 792 | { 793 | if ($indexField === null) { 794 | $indexField = $this->objectIndex; 795 | } 796 | switch ($indexField) { 797 | case 'sequencephp': 798 | $idx = $this->getSequencePHP(); 799 | break; 800 | case 'nextsequence': 801 | $idx = $this->getNextSequence(); 802 | break; 803 | default: 804 | $idx = $indexField; 805 | if ($this->tabular && isset($object[0])) { 806 | $idx = is_object($object[0]) ? $object[0]->{$idx} : $object[0][$idx]; 807 | } else { 808 | $idx = is_object($object) ? $object->{$idx} : $object[$idx]; 809 | } 810 | } 811 | try { 812 | switch ($operation) { 813 | case 'insert': 814 | $this->insert($idx, $object); 815 | break; 816 | case 'insertorupdate': 817 | $this->insertOrUpdate($idx, $object); 818 | break; 819 | default: 820 | trigger_error('insertObject: operation not defined'); 821 | } 822 | $this->resetChain(); 823 | return $idx; 824 | } catch (Exception $ex) { 825 | $this->throwError($ex->getMessage()); 826 | return false; 827 | } 828 | } 829 | 830 | public function throwError($msg, $append = false): void 831 | { 832 | if (is_array($msg)) { 833 | $msg = "{$msg['message']} file:{$msg['file']}[{$msg['line']}] type:{$msg['type']}"; 834 | } 835 | if ($this->throwable) { 836 | $this->resetChain(); 837 | throw @new RuntimeException($msg); 838 | } 839 | $this->resetChain(); 840 | if ($append) { 841 | $this->latestError .= $msg; 842 | } else { 843 | $this->latestError = $msg; 844 | } 845 | } 846 | 847 | public function lastError(): string 848 | { 849 | return $this->latestError; 850 | } 851 | 852 | public function resetError(): void 853 | { 854 | $this->latestError = ''; 855 | } 856 | 857 | protected function resetChain(): void 858 | { 859 | $this->throwable = true; 860 | } 861 | 862 | /** 863 | *

This function returns a unique sequence

864 | * It ensures a collision free number only if we don't do more than one operation 865 | * per 0.0001 second However,it also adds a pseudo random number (0-4095) 866 | * so the chances of collision is 1/4095 (per two operations executed every 0.0001 second).
867 | * It is based on Twitter's Snowflake number. 868 | * 869 | * @return string (it returns a 64bit integer). 870 | * @throws Exception 871 | */ 872 | public function getSequencePHP(): string 873 | { 874 | $ms = microtime(true); // we use this number as a random number generator (we use the decimals) 875 | //$ms=1000; 876 | $timestamp = round($ms * 1000); 877 | $rand = (int)(fmod($ms, 1) * 1000000) % 4096; // 4096= 2^12 It is the millionth of seconds 878 | if ($this->nodeId === -1) { 879 | $number = random_int(0, 1023); // a 10bit number. 880 | $calc = (($timestamp - 1459440000000) << 22) + ($number << 12) + $rand; 881 | } else { 882 | $calc = (($timestamp - 1459440000000) << 22) + ($this->nodeId << 12) + $rand; 883 | } 884 | usleep(1); 885 | return '' . $calc; 886 | } 887 | 888 | /** 889 | * It gets the next sequence. If the sequence doesn't exist, it generates a new one with 1.
890 | * You could peek a sequence with get('genseq_*name*')
891 | * If the sequence is corrupt then it's resetted.
892 | * 893 | * @param string $name Name of the sequence. 894 | * @param int $tries number of tries. It uses the default value defined in $defaultNumRetry (-1) 895 | * @param int $init The initial value of the sequence (if it's created) 896 | * @param int $interval The interval between each sequence. It could be negative. 897 | * @param int $reserveAdditional Reserve an additional number of sequence. It's useful when you want to 898 | * generates many sequences at once. 899 | * 900 | * @return bool|int It returns false if it fails to lock the sequence or if it's unable to read thr sequence. 901 | * Otherwise, it returns the sequence 902 | */ 903 | public function getNextSequence(string $name = "seq", int $tries = -1, int $init = 1, int $interval = 1, int $reserveAdditional = 0) 904 | { 905 | $id = "genseq_" . $name; 906 | $file = $this->filename($id) . ".seq"; 907 | if ($this->lock($file, $tries)) { 908 | if (file_exists($file)) { 909 | $read = @file_get_contents($file); 910 | if ($read === false) { 911 | $this->unlock($file); 912 | $this->throwError(error_get_last()); 913 | return false; // file exists but I am unable to read it. 914 | } 915 | $read = (is_numeric($read)) ? ($read + $interval) 916 | : $init; // if the value stored is numeric, then we add one, otherwise, it starts with 1 917 | } else { 918 | $read = $init; 919 | } 920 | $write = @file_put_contents($file, $read + $reserveAdditional, LOCK_EX); 921 | $this->unlock($file); 922 | if ($write === false) { 923 | $this->throwError(error_get_last()); 924 | } 925 | $this->resetChain(); 926 | return ($write === false) ? false : $read; 927 | } 928 | $this->resetChain(); 929 | return false; // unable to lock 930 | } 931 | 932 | /** 933 | * Add or update a whole document. 934 | * 935 | * @param string $id "ID" of the document. 936 | * @param string|array $document The document 937 | * @param int $tries number of tries. The default value is -1, it uses the default value 938 | * $defaultNumRetry. 939 | * 940 | * @return bool True if the information was added, otherwise false 941 | */ 942 | public function insertOrUpdate(string $id, $document, int $tries = -1): bool 943 | { 944 | $file = $this->filename($id); 945 | $this->currentId = $id; 946 | if ($this->lock($file, $tries)) { 947 | if ($this->autoSerialize) { 948 | $this->srvSerialize->insert($id, $document, $tries); 949 | $write = @file_put_contents($file, $this->serialize($document), LOCK_EX); 950 | } else { 951 | $write = @file_put_contents($file, $document, LOCK_EX); 952 | } 953 | $this->unlock($file); 954 | if ($write === false) { 955 | $this->throwError(error_get_last()); 956 | } 957 | $this->resetChain(); 958 | return ($write !== false); 959 | } 960 | $this->throwError("Unable to lock file [$this->currentId]"); 961 | return false; 962 | } 963 | 964 | /** 965 | * Update a whole document 966 | * 967 | * @param string $id "ID" of the document. 968 | * @param string|array $document The document 969 | * @param int $tries number of tries. The default value is -1 (it uses the default value 970 | * $defaultNumRetry) 971 | * 972 | * @return bool True if the information was added, otherwise false 973 | */ 974 | public function update(string $id, $document, int $tries = -1): bool 975 | { 976 | $file = $this->filename($id); 977 | $this->currentId = $id; 978 | if ($this->lock($file, $tries)) { 979 | if (file_exists($file)) { 980 | if ($this->autoSerialize) { 981 | $write = @file_put_contents($file, $this->serialize($document), LOCK_EX); 982 | } else { 983 | $write = @file_put_contents($file, $document, LOCK_EX); 984 | } 985 | } else { 986 | $write = false; 987 | } 988 | $this->unlock($file, $tries); 989 | if ($write === false) { 990 | $this->throwError(error_get_last()); 991 | } 992 | $this->resetChain(); 993 | return ($write !== false); 994 | } 995 | $this->throwError("Unable to lock file [$this->currentId]"); 996 | return false; 997 | } 998 | 999 | /** 1000 | * Set the current collection. It also could create the collection. 1001 | * 1002 | * @param string $collection 1003 | * @param bool $createIfNotExist if true then it checks if the collection (folder) exists, if not then it's 1004 | * created 1005 | * 1006 | * @return DocumentStoreOne 1007 | */ 1008 | public function collection(string $collection, bool $createIfNotExist = false): DocumentStoreOne 1009 | { 1010 | $this->collection = $collection; 1011 | if ($createIfNotExist && !$this->isCollection($collection)) { 1012 | $this->createCollection($collection); 1013 | } 1014 | return $this; 1015 | } 1016 | 1017 | /** 1018 | * Check a collection 1019 | * 1020 | * @param string $collection 1021 | * 1022 | * @return bool It returns false if it's not a collection (a valid folder) 1023 | */ 1024 | public function isCollection(string $collection): bool 1025 | { 1026 | $this->collection = $collection; 1027 | return is_dir($this->getPath()); 1028 | } 1029 | 1030 | /** 1031 | * Creates a collection 1032 | * 1033 | * @param string $collection 1034 | * @param bool $throwifFail 1035 | * @return bool true if the operation is right, false if it fails. 1036 | */ 1037 | public function createCollection(string $collection, bool $throwifFail = true): bool 1038 | { 1039 | $oldCollection = $this->collection; 1040 | $this->collection = $collection; 1041 | $r = @mkdir($this->getPath(), 0755); 1042 | if ($r === false && $throwifFail) { 1043 | $this->throwError('error create collection :' . @error_get_last()); 1044 | } 1045 | $this->collection = $oldCollection; 1046 | $this->resetChain(); 1047 | return $r; 1048 | } 1049 | 1050 | /** 1051 | * It deletes a collection inside a database,including its content. 1052 | * 1053 | * @param string $collection 1054 | * @param boolean $throwOnError 1055 | * @param bool $includeContent if true (default), then it deletes the content. 1056 | * @return void 1057 | */ 1058 | public function deleteCollection(string $collection, bool $throwOnError = false, bool $includeContent = true): void 1059 | { 1060 | $oldCollection = $this->collection; 1061 | $this->collection = $collection; 1062 | try { 1063 | $r = false; 1064 | if ($includeContent) { 1065 | array_map('unlink', glob($this->getPath() . "/*.*")); 1066 | } 1067 | $r = rmdir($this->getPath()); 1068 | } catch (Exception $ex) { 1069 | if ($throwOnError) { 1070 | $this->throwError($ex->getMessage()); 1071 | } 1072 | } 1073 | if ($r === false && $throwOnError) { 1074 | $this->throwError('error create collection :' . @error_get_last()); 1075 | } 1076 | $this->collection = $oldCollection; 1077 | $this->resetChain(); 1078 | } 1079 | 1080 | /** 1081 | * It sets if we want to auto serialize the information, and we set how it is serialized 1082 | * php = it serializes using serialize() function 1083 | * php_array = it serializes using include()/var_export() function. The result could be cached using OpCache 1084 | * json_object = it is serialized using json (as object) 1085 | * json_array = it is serialized using json (as array) 1086 | * none = it is not serialized. Information must be serialized/de-serialized manually 1087 | * php_array = it is serialized as a php_array 1088 | * 1089 | * @param bool $value 1090 | * @param string $strategy =['auto','php','php_array','json_object','json_array','csv','igbinary','msgpack','none'][$i] 1091 | * @param bool|null $tabular indicates if the value is tabular (it has rows and columns) or not.
1092 | * Note: if null then it takes the default value (csv is tabular, while others 1093 | * don't
1094 | */ 1095 | public function autoSerialize(bool $value = true, string $strategy = 'auto', ?bool $tabular = null): void 1096 | { 1097 | $this->autoSerialize = $value; 1098 | if ($value === false) { 1099 | $strategy = 'none'; 1100 | } 1101 | $this->serializeStrategy = $strategy; 1102 | if ($this->serializeStrategy === 'auto') { 1103 | if (function_exists('igbinary_serialize')) { 1104 | $this->serializeStrategy = 'igbinary'; 1105 | } elseif (function_exists('msgpack_pack')) { 1106 | $this->serializeStrategy = 'msgpack'; 1107 | } else { 1108 | $this->serializeStrategy = 'php'; 1109 | } 1110 | } 1111 | switch ($this->serializeStrategy) { 1112 | case 'csv': 1113 | $this->srvSerialize = new DocumentStoreOneCsv($this); 1114 | break; 1115 | case 'igbinary': 1116 | $this->srvSerialize = new DocumentStoreOneIgBinary($this); 1117 | break; 1118 | case 'msgpack': 1119 | $this->srvSerialize = new DocumentStoreOneMsgPack($this); 1120 | break; 1121 | case 'php': 1122 | $this->srvSerialize = new DocumentStoreOnePHP($this); 1123 | break; 1124 | case 'php_array': 1125 | $this->srvSerialize = new DocumentStoreOnePHPArray($this); 1126 | break; 1127 | case 'json_object': 1128 | $this->srvSerialize = new DocumentStoreOneJsonObj($this); 1129 | break; 1130 | case 'json_array': 1131 | $this->srvSerialize = new DocumentStoreOneJsonArray($this); 1132 | break; 1133 | default: 1134 | $this->srvSerialize = new DocumentStoreOneNone($this); 1135 | break; 1136 | } 1137 | if ($tabular === null) { 1138 | $this->tabular = $this->srvSerialize->defaultTabular(); 1139 | } 1140 | } 1141 | 1142 | /** 1143 | * It sets the style of the csv. 1144 | * 1145 | * @param string $separator For example ",", "\t", etc. 1146 | * @param string $enclosure The enclosure of a string value. 1147 | * @param string $lineEnd 1148 | * @param bool $header If the csv contains or not a header 1149 | * @param string $prefixColumn If the csv or the data does not contain name, then it uses this value to create a 1150 | * column name
1151 | * **Example:**
1152 | * 'col': columns 'col0','col1',etc.
1153 | * '': columns 0,1 1154 | * 1155 | * @return void 1156 | */ 1157 | public function csvStyle(string $separator = ',', string $enclosure = '"', string $lineEnd = "\n" 1158 | , bool $header = true, string $prefixColumn = ''): void 1159 | { 1160 | if ($this->srvSerialize instanceof DocumentStoreOneCsv) { 1161 | $this->srvSerialize->csvSeparator = $separator; 1162 | $this->srvSerialize->csvText = $enclosure; 1163 | $this->srvSerialize->csvLineEnd = $lineEnd; 1164 | $this->srvSerialize->csvHeader = $header; 1165 | $this->srvSerialize->csvPrefixColumn = $prefixColumn; 1166 | } 1167 | } 1168 | 1169 | /** 1170 | * It sets the regional style 1171 | * 1172 | * @param string $regionalDecimal 1173 | * @param string $regionalDate 1174 | * @param string $regionalDateTime 1175 | * @return void 1176 | */ 1177 | public function regionalStyle(string $regionalDecimal = '.', string $regionalDate = 'Y-m-d' 1178 | , string $regionalDateTime = 'Y-m-d H:i:s'): void 1179 | { 1180 | $this->regionDecimal = $regionalDecimal; 1181 | $this->regionDate = $regionalDate; 1182 | $this->regionDateTime = $regionalDateTime; 1183 | } 1184 | 1185 | /** 1186 | * List all the Ids in a collection (or returns the documents if $returnOnlyIndex is false) 1187 | * 1188 | * @param string $mask see http://php.net/manual/en/function.glob.php 1189 | * 1190 | * @param bool $returnOnlyIndex 1191 | * If false then it returns each document. 1192 | * If returns (default) then it returns indexes. 1193 | * 1194 | * @return array 1195 | * @throws RedisException 1196 | */ 1197 | public function select(string $mask = "*", bool $returnOnlyIndex = true): array 1198 | { 1199 | $list = glob($this->database . "/" . $this->collection . "/" . $mask . $this->docExt); 1200 | foreach ($list as $key => $fileId) { 1201 | $list[$key] = basename($fileId, $this->docExt); 1202 | } 1203 | if ($returnOnlyIndex) { 1204 | return $list; 1205 | } 1206 | $listDoc = []; 1207 | foreach ($list as $fileId) { 1208 | $listDoc[] = $this->get($fileId); 1209 | } 1210 | return $listDoc; 1211 | } 1212 | 1213 | /** 1214 | * Get a document from the datastore
1215 | * **Example:**
1216 | * ``` 1217 | * $data=$this->get('rows',-1,false); // it returns a value or false 1218 | * $data=$this->get('rows',-1,"not null"); // it returns a value or "not null" if not found 1219 | * ``` 1220 | * 1221 | * @param string $id "ID" of the document. 1222 | * @param int $tries number of tries.
1223 | * The default value is -1 (it uses the default value $defaultNumRetry)
1224 | * 0 means it will not try to lock
1225 | * @param mixed $default Default value (if the value is not found) 1226 | * 1227 | * @return mixed The object if the information was read, otherwise false (or default value). 1228 | * @throws RedisException 1229 | * @throws RedisException 1230 | */ 1231 | public function get(string $id, int $tries = -1, $default = false) 1232 | { 1233 | $file = $this->filename($id); 1234 | $this->currentId = $id; 1235 | if ($this->lock($file, $tries)) { 1236 | if ($this->serializeStrategy === 'php_array') { 1237 | $json = @include $file; 1238 | $this->unlock($file); 1239 | } else { 1240 | $json = @file_get_contents($file); 1241 | $this->unlock($file, $tries); 1242 | if ($json !== false) { 1243 | if (strpos($json, $this->separatorAppend) === false) { 1244 | if ($this->autoSerialize) { 1245 | $json = $this->deserialize($json); 1246 | } 1247 | } else { 1248 | $arr = explode($this->separatorAppend, $json); 1249 | if (count($arr) > 0 && $arr[0] === '') { 1250 | unset($arr[0]); 1251 | } 1252 | $json = []; 1253 | foreach ($arr as $item) { 1254 | $json[] = $this->deserialize($item); 1255 | } 1256 | } 1257 | } 1258 | } 1259 | if ($json === false) { 1260 | $this->throwError(error_get_last()); 1261 | } 1262 | $this->resetChain(); 1263 | return ($json === false) ? $default : $json; 1264 | } 1265 | $this->resetChain(); 1266 | return $default; 1267 | } 1268 | 1269 | /** 1270 | * Set if you don't want to throw an exception when error.
1271 | * This value is reset every time the value is read or an exception is throw.
1272 | * However, the error message lastError() is still stored.
1273 | * **Example:**
1274 | * ``` 1275 | * $this->noThrowOnError()->get('id'); // it will not throw an exception if "ID" document does not exist. 1276 | * ``` 1277 | * @param bool $throw if false (default), then it doesn't throw an exception on error. 1278 | * @return $this 1279 | */ 1280 | public function noThrowOnError(bool $throw = false): DocumentStoreOne 1281 | { 1282 | $this->throwable = $throw; 1283 | return $this; 1284 | } 1285 | 1286 | protected function deserialize($document) 1287 | { 1288 | return $this->srvSerialize->deserialize($document); 1289 | } 1290 | 1291 | /** 1292 | * It gets the timestamp of a document or false in case of error.
1293 | * **Example** 1294 | * ``` 1295 | * $timeStamp=$this->getTimeStamp("doc20"); 1296 | * ``` 1297 | * 1298 | * @param string $id "ID" of the document. 1299 | * @param bool $returnAsAbsoluteTime if true then it returns the age (how many seconds are elapsed)
1300 | * if false then it returns the regular timestamp of the time. 1301 | * 1302 | * @return false|int 1303 | */ 1304 | public function getTimeStamp(string $id, bool $returnAsAbsoluteTime = false) 1305 | { 1306 | $this->currentId = $id; 1307 | $file = $this->filename($id); 1308 | try { 1309 | $rt = @filemtime($file); 1310 | } catch (Exception $ex) { 1311 | $rt = false; 1312 | } 1313 | if ($returnAsAbsoluteTime) { 1314 | if ($rt === false) { 1315 | return false; 1316 | } 1317 | return time() - $rt; 1318 | } 1319 | return $rt; 1320 | } 1321 | 1322 | /** 1323 | * It sets the modification time of a document
1324 | * **Example** 1325 | * ``` 1326 | * $timeStamp=$this->setTimeStamp("doc20",12323232); // set the absolute timestemp 1327 | * $timeStamp=$this->setTimeStamp("doc20",100,false); // set the relative timestamp (relative current time) 1328 | * ``` 1329 | * @param string $id "ID" of the document 1330 | * @param int $ttl the interval (in seconds). 1331 | * @param bool $setTTLAsAbsoluteTime if true then it sets the age ($ttl+time())
1332 | * if false then it sets the regular timestamp of the time. 1333 | * @return bool 1334 | */ 1335 | public function setTimeStamp(string $id, int $ttl, bool $setTTLAsAbsoluteTime = true): bool 1336 | { 1337 | $time = $setTTLAsAbsoluteTime ? $ttl : time() + $ttl; 1338 | try { 1339 | return @touch($this->filename($id), $time); 1340 | } catch (Exception $ex) { 1341 | return false; 1342 | } 1343 | } 1344 | 1345 | /** 1346 | * It gets a values from the datastore filtered by a condition
1347 | * **Example:** 1348 | * ``` 1349 | * $data=$this->getFiltered('rows',-1,false,['type'=>'busy']); // it returns values [0=>...,1=>...] 1350 | * $data=$this->getFiltered('rows',-1,false,['type'=>'busy'],false); // it returns values [2=>...,4=>..] 1351 | * ``` 1352 | * 1353 | * @param string $id "ID" of the document. 1354 | * @param int $tries number of tries. The default value is -1 (it uses the default value $defaultNumRetry) 1355 | * @param mixed $default default value (if the value is not found) 1356 | * @param array $condition An associative array with the conditions. 1357 | * @param bool $reindex If true then the result is reindexed (starting from zero). 1358 | * 1359 | * @return array 1360 | * @noinspection TypeUnsafeComparisonInspection 1361 | * @throws RedisException 1362 | */ 1363 | public function getFiltered(string $id, int $tries = -1, $default = false, array $condition = [], bool $reindex = true): array 1364 | { 1365 | $rows = $this->get($id, $tries, $default); 1366 | $result = []; 1367 | foreach ($rows as $k => $v) { 1368 | $fail = false; 1369 | foreach ($condition as $k2 => $v2) { 1370 | if (is_object($v)) { 1371 | if (!isset($v->{$k2}) || $v->{$k2} != $v2) { 1372 | $fail = true; 1373 | break; 1374 | } 1375 | } elseif (!isset($v[$k2]) || $v[$k2] != $v2) { 1376 | $fail = true; 1377 | break; 1378 | } 1379 | } 1380 | if (!$fail) { 1381 | if ($reindex) { 1382 | $result[] = $v; 1383 | } else { 1384 | $result[$k] = $v; 1385 | } 1386 | } 1387 | } 1388 | return $result; 1389 | } 1390 | 1391 | /** 1392 | * Delete a document.
1393 | * **Example:** 1394 | * ``` 1395 | * $isDeleted=$this->delete('doc20'); 1396 | * ``` 1397 | * 1398 | * @param string $id "ID" of the document 1399 | * @param int $tries number of tries. The default value is -1 (it uses the default value $defaultNumRetry) 1400 | * 1401 | * @return bool if it's unable to unlock or the document doesn't exist. 1402 | */ 1403 | public function delete(string $id, int $tries = -1, $throwOnError = false): bool 1404 | { 1405 | $file = $this->filename($id); 1406 | if ($this->lock($file, $tries)) { 1407 | $r = @unlink($file); 1408 | $this->unlock($file); 1409 | if ($r === false && $throwOnError) { 1410 | $this->throwError(error_get_last()); 1411 | } 1412 | $this->resetChain(); 1413 | return $r; 1414 | } 1415 | $this->resetChain(); 1416 | return false; 1417 | } 1418 | 1419 | /** 1420 | * Copy a document. If the destination exists then it's replaced.
1421 | * **Example:** 1422 | * ``` 1423 | * $isCopied=$this->copy('doc20','doc20copy'); 1424 | * ``` 1425 | * @param string $idOrigin 1426 | * @param string $idDestination 1427 | * @param int $tries number of tries. The default value is -1 (it uses the default value $defaultNumRetry) 1428 | * 1429 | * @return bool true if the operation is correct, otherwise it returns false (unable to lock / unable to copy) 1430 | */ 1431 | public function copy(string $idOrigin, string $idDestination, int $tries = -1): bool 1432 | { 1433 | $fileOrigin = $this->filename($idOrigin); 1434 | $fileDestination = $this->filename($idDestination); 1435 | if ($this->lock($fileOrigin, $tries)) { 1436 | if ($this->lock($fileDestination, $tries)) { 1437 | $r = @copy($fileOrigin, $fileDestination); 1438 | $this->unlock($fileOrigin); 1439 | $this->unlock($fileDestination); 1440 | if ($r === false) { 1441 | $this->throwError(error_get_last()); 1442 | } 1443 | $this->resetChain(); 1444 | return $r; 1445 | } 1446 | $this->unlock($fileOrigin); 1447 | $this->throwError("Unable to lock file [$idDestination]"); 1448 | return false; 1449 | } 1450 | $this->throwError("Unable to lock file [$idOrigin]"); 1451 | return false; 1452 | } 1453 | 1454 | /** 1455 | * Rename a document. If the destination exists then it's not renamed
1456 | * **Example:** 1457 | * ``` 1458 | * $isRenamed=$this->rename('doc20','doc20newName'); 1459 | * ``` 1460 | * 1461 | * @param string $idOrigin 1462 | * @param string $idDestination 1463 | * @param int $tries number of tries. The default value is -1 (it uses the default value $defaultNumRetry) 1464 | * 1465 | * @return bool true if the operation is correct, otherwise it returns false 1466 | * (unable to lock / unable to rename) 1467 | */ 1468 | public function rename(string $idOrigin, string $idDestination, int $tries = -1): bool 1469 | { 1470 | $fileOrigin = $this->filename($idOrigin); 1471 | $fileDestination = $this->filename($idDestination); 1472 | if ($this->lock($fileOrigin, $tries)) { 1473 | if ($this->lock($fileDestination, $tries)) { 1474 | $r = @rename($fileOrigin, $fileDestination); 1475 | $this->unlock($fileOrigin); 1476 | $this->unlock($fileDestination); 1477 | if ($r === false) { 1478 | $this->throwError(error_get_last()); 1479 | } 1480 | $this->resetChain(); 1481 | return $r; 1482 | } 1483 | $this->unlock($fileOrigin); 1484 | $this->resetChain(); 1485 | return false; 1486 | } 1487 | $this->resetChain(); 1488 | return false; 1489 | } 1490 | } 1491 | 1492 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOneCsv.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 37 | } 38 | public function defaultTabular():bool { 39 | return true; 40 | } 41 | public function appendValue($filePath, $id, $addValue, $tries = -1) 42 | { 43 | $fp=$this->parent->appendValueDecorator($filePath,$id,$addValue,$tries); 44 | if(!is_resource($fp)) { 45 | return $fp; 46 | } 47 | fseek($fp, 0, SEEK_END); 48 | $addValue = $this->parent->serialize($addValue, true); // true no add header 49 | $r = @fwrite($fp, $addValue); 50 | @fclose($fp); 51 | $this->parent->unlock($filePath); 52 | if($r===false) { 53 | $this->parent->throwError(error_get_last()); 54 | } 55 | return ($r !== false); 56 | } 57 | public function insert($id, $document, $tries = -1) { 58 | $this->getHeaderCSV($document); 59 | $this->determineTypes($document[0]); 60 | } 61 | public function serialize($document, $special = false) { 62 | return $this->serializeCSV($document, $special); 63 | } 64 | 65 | public function serializeCSV($table, $noheader = false) 66 | { 67 | $result = ''; 68 | if (!$noheader && $this->csvHeader) { 69 | // had header 70 | $line = []; 71 | foreach ($this->parent->schemas[$this->parent->currentId] as $colname => $type) { 72 | $line[] = $this->parent->convertTypeBack($colname, 'string'); 73 | } 74 | $result = implode($this->csvSeparator, $line) . $this->csvLineEnd; 75 | } 76 | $line = []; 77 | if (!$this->parent->isTable($table)) { 78 | // it just a row, so we convert into a row. 79 | $table = [$table]; 80 | } 81 | foreach ($table as $kr => $row) { 82 | foreach ($row as $kcol => $v) { 83 | $line[$kr][$kcol] = $this->parent->convertTypeBack($v, $this->parent->schemas[$this->parent->currentId][$kcol]); 84 | } 85 | $result .= implode($this->csvSeparator, $line[$kr]) . $this->csvLineEnd; 86 | } 87 | return $result; 88 | } 89 | 90 | 91 | /** 92 | * @param array $row is an associative array with the values to determine 93 | * @return void 94 | * @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection 95 | */ 96 | public function determineTypes(&$row) 97 | { 98 | $cols = isset($this->parent->schemas[$this->parent->currentId]) ? $this->parent->schemas[$this->parent->currentId] : null; 99 | // $this->parent->schemas[$this->parent->currentId] = []; 100 | if ($cols === null) { 101 | // there is no columns defined, so we define columns using this row 102 | foreach ($row as $kcol => $item) { 103 | if (is_numeric($kcol)) { 104 | $kcol = $this->csvPrefixColumn . $kcol; 105 | } 106 | $this->parent->schemas[$this->parent->currentId][$kcol] = $this->parent->getType($item); 107 | } 108 | } else { 109 | $c = -1; 110 | foreach ($cols as $kcol => $v) { 111 | if (isset($row[0])) { 112 | $c++; 113 | $this->parent->schemas[$this->parent->currentId][$kcol] 114 | = $this->parent->getType($row[$c]); 115 | } else { 116 | $this->parent->schemas[$this->parent->currentId][$kcol] 117 | = $this->parent->getType($row[$kcol]); 118 | } 119 | } 120 | } 121 | } 122 | 123 | 124 | public function convertTypeBack($input, $type) { 125 | switch ($type) { 126 | case 'int': 127 | return $input; 128 | case 'decimal': 129 | if ($this->parent->regionDecimal !== '.') { 130 | $inputD = str_replace('.', $this->parent->regionDecimal, $input); 131 | } else { 132 | $inputD = $input; 133 | } 134 | return $inputD; 135 | case 'date': 136 | return $this->csvText . $input->format($this->parent->regionDate) . $this->csvText; 137 | case 'string': 138 | if($this->csvText && strpos($input,$this->csvText)) { 139 | return $this->csvText .str_replace($this->csvText,$this->csvEscape.$this->csvText,$input) . $this->csvText; 140 | } 141 | return $this->csvText .$input . $this->csvText; 142 | case 'datetime': 143 | return $this->csvText . $input->format($this->parent->regionDateTime) . $this->csvText; 144 | } 145 | return $input; 146 | } 147 | 148 | public function convertType($input, $type) 149 | { 150 | switch ($type) { 151 | case 'int': 152 | return (int)$input; 153 | case 'decimal': 154 | if ($this->parent->regionDecimal !== '.') { 155 | $inputD = str_replace($this->parent->regionDecimal, '.', $input); 156 | } else { 157 | $inputD = $input; 158 | } 159 | return (float)$inputD; 160 | case 'string': 161 | if(!$this->csvEscape || strpos($input,$this->csvEscape)===false) { 162 | return (string)$input; 163 | } 164 | return (string)str_replace($this->csvEscape,'',$input); 165 | case 'date': 166 | return DateTime::createFromFormat($this->parent->regionDate, $input); 167 | case 'datetime': 168 | return DateTime::createFromFormat($this->parent->regionDateTime, $input); 169 | } 170 | return $input; 171 | } 172 | 173 | 174 | public function insertOrUpdate($id, $document, $tries = -1) 175 | { 176 | 177 | } 178 | public function deserialize($document) 179 | { 180 | return $this->unserializeCSV($document); 181 | } 182 | 183 | /** @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection */ 184 | public function unserializeCSV(&$document) 185 | { 186 | $lines = explode($this->csvLineEnd, $document); 187 | $result = []; 188 | $numLines = count($lines); 189 | if ($numLines > 0) { 190 | if ($this->csvHeader) { 191 | // it has header 192 | $header = $this->splitLine($lines[0], false); 193 | foreach ($header as $namecol) { 194 | $this->parent->schemas[$this->parent->currentId][$namecol] = 'string'; 195 | } 196 | $first = 1; 197 | } else { 198 | // no header 199 | $this->parent->schemas[$this->parent->currentId] = null; 200 | $first = 0; 201 | } 202 | if (isset($lines[$first])) { 203 | // asume types 204 | $firstLine = $this->splitLine($lines[$first], true); 205 | //$numcols=count($firstLine); 206 | $this->determineTypes($firstLine); 207 | for ($i = $first; $i < $numLines; $i++) { 208 | $tmp = $this->splitLine($lines[$i], true); 209 | foreach ($tmp as $namecol => $item) { 210 | if (!isset($this->parent->schemas[$this->parent->currentId][$namecol])) { 211 | $this->parent->throwError('incorrect column found in csv line ' . $i); 212 | return null; 213 | } 214 | $tmp[$namecol] = $this->parent->convertType($item, $this->parent->schemas[$this->parent->currentId][$namecol]); 215 | } 216 | if (count($tmp) > 0) { 217 | // we avoid inserting an empty line 218 | $result[] = $tmp; 219 | } 220 | } 221 | } 222 | } 223 | return $result; 224 | } 225 | private function splitLine($lineTxt, $useColumn) 226 | { 227 | if ($lineTxt === null || $lineTxt === '') { 228 | return []; 229 | } 230 | $arr = str_getcsv($lineTxt, $this->csvSeparator, $this->csvText,$this->csvEscape); 231 | $result = []; 232 | if ($useColumn === true) { 233 | $colnames = isset($this->parent->schemas[$this->parent->currentId]) ? array_keys($this->parent->schemas[$this->parent->currentId]) : []; 234 | foreach ($arr as $numCol => $v) { 235 | if (!isset($colnames[$numCol])) { 236 | $colnames[$numCol] = $this->csvPrefixColumn . $numCol; 237 | $this->parent->schemas[$this->parent->currentId][$colnames[$numCol]] = $this->parent->getType($v); // column is missing so we create a column name 238 | } 239 | $result[$colnames[$numCol]] = trim($v); 240 | 241 | } 242 | } else { 243 | foreach ($arr as $k => $v) { 244 | $result[$k] = trim($v); 245 | } 246 | } 247 | return $result; 248 | } 249 | /** 250 | * It gets the header of a csv only if the id of the document doesn't have it. 251 | * 252 | * @param array $table 253 | * @return void 254 | */ 255 | public function getHeaderCSV($table) 256 | { 257 | if (!isset($this->parent->schemas[$this->parent->currentId])) { 258 | if (!$this->parent->isTable($table)) { 259 | // it just a row, so we convert into a row. 260 | $table = [$table]; 261 | } 262 | $this->parent->schemas[$this->parent->currentId] = []; 263 | foreach ($table[0] as $kcol => $v) { 264 | if (is_numeric($kcol)) { 265 | $kcol = $this->csvPrefixColumn . $kcol; 266 | } 267 | $this->parent->schemas[$this->parent->currentId][$kcol] = 'string'; // default value is string 268 | } 269 | } 270 | } 271 | 272 | 273 | 274 | 275 | } 276 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOneIgBinary.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 25 | } 26 | public function defaultTabular():bool { 27 | return false; 28 | } 29 | 30 | public function appendValue($filePath, $id, $addValue, $tries = -1) 31 | { 32 | return $this->parent->appendValueRaw2($id,$addValue,$tries); 33 | } 34 | public function insert($id, $document, $tries = -1) { 35 | } 36 | public function serialize($document, $special = false) { 37 | return igbinary_serialize($document); 38 | } 39 | public function convertTypeBack($input, $type) { 40 | switch ($type) { 41 | case 'decimal': 42 | case 'string': 43 | case 'int': 44 | return $input; 45 | case 'date': 46 | return $input->format($this->parent->regionDate); 47 | case 'datetime': 48 | return $input->format($this->parent->regionDateTime); 49 | } 50 | return $input; 51 | } 52 | public function convertType($input, $type) { 53 | return $input; 54 | } 55 | public function insertOrUpdate($id, $document, $tries = -1) 56 | { 57 | 58 | } 59 | public function deserialize($document) 60 | { 61 | return igbinary_unserialize($document); 62 | } 63 | 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOneJsonArray.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 25 | } 26 | public function defaultTabular():bool { 27 | return false; 28 | } 29 | 30 | public function appendValue($filePath, $id, $addValue, $tries = -1) 31 | { 32 | $fp=$this->parent->appendValueDecorator($filePath,$id,$addValue,$tries); 33 | if(!is_resource($fp)) { 34 | return $fp; 35 | } 36 | fseek($fp, -1, SEEK_END); 37 | $addValue = $this->parent->serialize($addValue, true); 38 | $r = @fwrite($fp, ',' . $addValue . ']'); 39 | @fclose($fp); 40 | $this->parent->unlock($filePath); 41 | if($r===false) { 42 | $this->parent->throwError(error_get_last()); 43 | } 44 | return ($r !== false); 45 | } 46 | public function insert($id, $document, $tries = -1) { 47 | } 48 | public function serialize($document, $special = false) { 49 | return json_encode($document); 50 | } 51 | public function convertTypeBack($input, $type) { 52 | switch ($type) { 53 | case 'decimal': 54 | case 'string': 55 | case 'int': 56 | return $input; 57 | case 'date': 58 | return $input->format($this->parent->regionDate); 59 | case 'datetime': 60 | return $input->format($this->parent->regionDateTime); 61 | } 62 | return $input; 63 | } 64 | public function convertType($input, $type) { 65 | return $input; 66 | } 67 | public function insertOrUpdate($id, $document, $tries = -1) 68 | { 69 | 70 | } 71 | public function deserialize($document) 72 | { 73 | return json_decode($document, true); 74 | } 75 | 76 | 77 | 78 | } 79 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOneJsonObj.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 26 | } 27 | public function defaultTabular():bool { 28 | return false; 29 | } 30 | 31 | public function appendValue($filePath, $id, $addValue, $tries = -1) 32 | { 33 | $fp=$this->parent->appendValueDecorator($filePath,$id,$addValue,$tries); 34 | if(!is_resource($fp)) { 35 | return $fp; 36 | } 37 | 38 | fseek($fp, -1, SEEK_END); 39 | $addValue = $this->parent->serialize($addValue, true); 40 | $r = @fwrite($fp, ',' . $addValue . ']'); 41 | 42 | @fclose($fp); 43 | $this->parent->unlock($filePath); 44 | if($r===false) { 45 | $this->parent->throwError(error_get_last()); 46 | } 47 | return ($r !== false); 48 | } 49 | public function insert($id, $document, $tries = -1) { 50 | } 51 | public function serialize($document, $special = false) { 52 | return json_encode($document); 53 | } 54 | public function convertTypeBack($input, $type) { 55 | switch ($type) { 56 | case 'decimal': 57 | case 'string': 58 | case 'int': 59 | return $input; 60 | case 'date': 61 | return $input->format($this->parent->regionDate); 62 | case 'datetime': 63 | return $input->format($this->parent->regionDateTime); 64 | } 65 | return $input; 66 | } 67 | public function convertType($input, $type) { 68 | return $input; 69 | } 70 | 71 | public function insertOrUpdate($id, $document, $tries = -1) 72 | { 73 | 74 | } 75 | public function deserialize($document) 76 | { 77 | return json_decode($document, false); 78 | } 79 | 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOneMsgPack.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 25 | } 26 | public function defaultTabular():bool { 27 | return false; 28 | } 29 | 30 | public function appendValue($filePath, $id, $addValue, $tries = -1) 31 | { 32 | return $this->parent->appendValueRaw2($id,$addValue,$tries); 33 | } 34 | public function insert($id, $document, $tries = -1) { 35 | } 36 | public function serialize($document, $special = false) { 37 | return msgpack_pack($document); 38 | } 39 | public function convertTypeBack($input, $type) { 40 | switch ($type) { 41 | case 'decimal': 42 | case 'string': 43 | case 'int': 44 | return $input; 45 | case 'date': 46 | return $input->format($this->parent->regionDate); 47 | case 'datetime': 48 | return $input->format($this->parent->regionDateTime); 49 | } 50 | return $input; 51 | } 52 | public function convertType($input, $type) { 53 | return $input; 54 | } 55 | public function insertOrUpdate($id, $document, $tries = -1) 56 | { 57 | 58 | } 59 | public function deserialize($document) 60 | { 61 | return msgpack_unpack($document); 62 | } 63 | 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOneNone.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 26 | } 27 | public function defaultTabular():bool { 28 | return false; 29 | } 30 | 31 | public function appendValue($filePath, $id, $addValue, $tries = -1) 32 | { 33 | return $this->parent->appendValueRaw($filePath, $addValue); 34 | } 35 | public function insert($id, $document, $tries = -1) { 36 | } 37 | public function serialize($document, $special = false) { 38 | return $document; 39 | } 40 | public function convertTypeBack($input, $type) { 41 | return $input; 42 | } 43 | public function convertType($input, $type) { 44 | return $input; 45 | } 46 | 47 | public function insertOrUpdate($id, $document, $tries = -1) 48 | { 49 | 50 | } 51 | public function deserialize($document) 52 | { 53 | return $document; 54 | } 55 | 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOnePHP.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 26 | } 27 | public function defaultTabular():bool { 28 | return false; 29 | } 30 | 31 | public function appendValue($filePath, $id, $addValue, $tries = -1) 32 | { 33 | return $this->parent->appendValueRaw($filePath, $addValue); 34 | } 35 | public function insert($id, $document, $tries = -1) { 36 | } 37 | public function serialize($document, $special = false) { 38 | return serialize($document); 39 | } 40 | public function convertTypeBack($input, $type) { 41 | switch ($type) { 42 | case 'decimal': 43 | case 'string': 44 | case 'int': 45 | return $input; 46 | case 'date': 47 | return $input->format($this->parent->regionDate); 48 | case 'datetime': 49 | return $input->format($this->parent->regionDateTime); 50 | } 51 | return $input; 52 | } 53 | public function convertType($input, $type) { 54 | return $input; 55 | } 56 | 57 | public function insertOrUpdate($id, $document, $tries = -1) 58 | { 59 | 60 | } 61 | public function deserialize($document) 62 | { 63 | 64 | return unserialize($document); 65 | } 66 | 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /lib/services/DocumentStoreOnePHPArray.php: -------------------------------------------------------------------------------- 1 | parent = $parent; 26 | } 27 | public function defaultTabular():bool { 28 | return false; 29 | } 30 | 31 | public function appendValue($filePath, $id, $addValue, $tries = -1) 32 | { 33 | $fp=$this->parent->appendValueDecorator($filePath,$id,$addValue,$tries); 34 | if(!is_resource($fp)) { 35 | return $fp; 36 | } 37 | 38 | fseek($fp, -4, SEEK_END); 39 | $addValue = $this->parent->serialize($addValue, true); 40 | $r = @fwrite($fp, ',' . $addValue . ");\n "); //note: ");\n " it must be exactly 4 characters. 41 | 42 | @fclose($fp); 43 | $this->parent->unlock($filePath); 44 | if($r===false) { 45 | $this->parent->throwError(error_get_last()); 46 | } 47 | return ($r !== false); 48 | } 49 | public function insert($id, $document, $tries = -1) { 50 | } 51 | public function serialize($document, $special = false) { 52 | return DocumentStoreOne::serialize_php_array($document, $special); 53 | } 54 | public function convertTypeBack($input, $type) { 55 | switch ($type) { 56 | case 'decimal': 57 | case 'string': 58 | case 'int': 59 | return $input; 60 | case 'date': 61 | return $input->format($this->parent->regionDate); 62 | case 'datetime': 63 | return $input->format($this->parent->regionDateTime); 64 | } 65 | return $input; 66 | } 67 | public function convertType($input, $type) { 68 | return $input; 69 | } 70 | 71 | public function insertOrUpdate($id, $document, $tries = -1) 72 | { 73 | 74 | } 75 | public function deserialize($document) 76 | { 77 | //php_array should be included. 78 | return $document; 79 | } 80 | 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /lib/services/IDocumentStoreOneSrv.php: -------------------------------------------------------------------------------- 1 |