PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / 3rdparty / SleekDB / Store.php
backup / src / JetBackup / 3rdparty / SleekDB Last commit date
Classes 4 months ago Exceptions 1 year ago .htaccess 1 year ago Cache.php 4 months ago Query.php 1 year ago QueryBuilder.php 1 year ago SleekDB.php 1 year ago Store.php 4 months ago autoload.php 1 year ago index.html 1 year ago web.config 1 year ago
Store.php
911 lines
1 <?php
2
3 namespace SleekDB;
4
5 use Exception;
6 use SleekDB\Classes\IoHelper;
7 use SleekDB\Classes\NestedHelper;
8 use SleekDB\Exceptions\InvalidArgumentException;
9 use SleekDB\Exceptions\IdNotAllowedException;
10 use SleekDB\Exceptions\InvalidConfigurationException;
11 use SleekDB\Exceptions\IOException;
12 use SleekDB\Exceptions\JsonException;
13
14 class Store
15 {
16
17 protected $root = __DIR__;
18
19 protected $storeName = "";
20 protected $storePath = "";
21
22 protected $databasePath = "";
23
24 protected $useCache = true;
25 protected $folderPermissions = 0777;
26 protected $defaultCacheLifetime;
27 protected $primaryKey = "_id";
28 protected $timeout = 120;
29 protected $searchOptions = [
30 "minLength" => 2,
31 "scoreKey" => "searchScore",
32 "mode" => "or",
33 "algorithm" => Query::SEARCH_ALGORITHM["hits"]
34 ];
35
36 const dataDirectory = "data" . DIRECTORY_SEPARATOR;
37
38 /**
39 * Store constructor.
40 *
41 * @param string $storeName
42 * @param string $databasePath
43 * @param array $configuration
44 *
45 * @throws InvalidArgumentException
46 * @throws IOException
47 * @throws InvalidConfigurationException
48 */
49 public function __construct(string $storeName, string $databasePath, array $configuration = [])
50 {
51 $storeName = trim($storeName);
52 if (empty($storeName)) {
53 throw new InvalidArgumentException('store name can not be empty');
54 }
55 $this->storeName = $storeName;
56
57 $databasePath = trim($databasePath);
58 if (empty($databasePath)) {
59 throw new InvalidArgumentException('data directory can not be empty');
60 }
61
62 IoHelper::normalizeDirectory($databasePath);
63 $this->databasePath = $databasePath;
64
65 $this->setConfiguration($configuration);
66
67 // boot store
68 $this->createDatabasePath();
69 $this->createStore();
70 }
71
72 /**
73 * Change the destination of the store object.
74 *
75 * @param string $storeName
76 * @param string|null $databasePath If empty, previous database path will be used.
77 * @param array $configuration
78 *
79 * @return Store
80 * @throws IOException
81 * @throws InvalidArgumentException
82 * @throws InvalidConfigurationException
83 */
84 public function changeStore(string $storeName, ?string $databasePath = null, array $configuration = []): Store
85 {
86 if(empty($databasePath)){
87 $databasePath = $this->getDatabasePath();
88 }
89 $this->__construct($storeName, $databasePath, $configuration);
90 return $this;
91 }
92
93 /**
94 * @return string
95 */
96 public function getStoreName(): string
97 {
98 return $this->storeName;
99 }
100
101 /**
102 * @return string
103 */
104 public function getDatabasePath(): string
105 {
106 return $this->databasePath;
107 }
108
109 /**
110 * @return QueryBuilder
111 */
112 public function createQueryBuilder(): QueryBuilder
113 {
114 return new QueryBuilder($this);
115 }
116
117 /**
118 * Insert a new document to the store.
119 * It is stored as a plaintext JSON document.
120 * @param array $data
121 * @return array inserted document
122 * @throws IOException
123 * @throws IdNotAllowedException
124 * @throws InvalidArgumentException
125 * @throws JsonException
126 */
127 public function insert(array $data): array
128 {
129 // Handle invalid data
130 if (empty($data)) {
131 throw new InvalidArgumentException('No data found to insert in the store');
132 }
133
134 $data = $this->writeNewDocumentToStore($data);
135
136 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
137
138 return $data;
139 }
140
141 /**
142 * Insert multiple documents to the store.
143 * They are stored as plaintext JSON documents.
144 * @param array $data
145 * @return array inserted documents
146 * @throws IOException
147 * @throws IdNotAllowedException
148 * @throws InvalidArgumentException
149 * @throws JsonException
150 */
151 public function insertMany(array $data): array
152 {
153 // Handle invalid data
154 if (empty($data)) {
155 throw new InvalidArgumentException('No data found to insert in the store');
156 }
157
158 // All results.
159 $results = [];
160 foreach ($data as $document) {
161 $results[] = $this->writeNewDocumentToStore($document);
162 }
163
164 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
165 return $results;
166 }
167
168
169 /**
170 * Delete store with all its data and cache.
171 * @return bool
172 * @throws IOException
173 */
174 public function deleteStore(): bool
175 {
176 $storePath = $this->getStorePath();
177 return IoHelper::deleteFolder($storePath);
178 }
179
180
181 /**
182 * Return the last created store object ID.
183 * @return int
184 * @throws IOException
185 */
186 public function getLastInsertedId(): int
187 {
188 $counterPath = $this->getStorePath() . '_cnt.sdb';
189
190 return (int) IoHelper::getFileContentRaw($counterPath);
191 }
192
193 /**
194 * @return string
195 */
196 public function getStorePath(): string
197 {
198 return $this->storePath;
199 }
200
201 /**
202 * Retrieve all documents.
203 *
204 * @param array|null $orderBy array($fieldName => $order). $order can be "asc" or "desc"
205 * @param int|null $limit the amount of data record to limit
206 * @param int|null $offset the amount of data record to skip
207 *
208 * @return array
209 * @throws IOException
210 * @throws InvalidArgumentException
211 */
212 public function findAll(?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
213 {
214 $qb = $this->createQueryBuilder();
215 if(!is_null($orderBy)){
216 $qb->orderBy($orderBy);
217 }
218 if(!is_null($limit)){
219 $qb->limit($limit);
220 }
221 if(!is_null($offset)){
222 $qb->skip($offset);
223 }
224 return $qb->getQuery()->fetch();
225 }
226
227 /**
228 * Retrieve one document by its primary key. Very fast because it finds the document by its file path.
229 * @param int|string $id
230 * @return array|null
231 * @throws InvalidArgumentException
232 */
233 public function findById($id){
234
235 $id = $this->checkAndStripId($id);
236
237 $filePath = $this->getDataPath() . "$id.json";
238
239 try{
240 $content = IoHelper::getFileContent($filePath);
241 } catch (Exception $exception){
242 return null;
243 }
244
245 return @json_decode($content, true);
246 }
247
248 /**
249 * Retrieve one or multiple documents.
250 *
251 * @param array $criteria
252 * @param array|null $orderBy
253 * @param int|null $limit
254 * @param int|null $offset
255 *
256 * @return array
257 * @throws IOException
258 * @throws InvalidArgumentException
259 */
260 public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
261 {
262 $qb = $this->createQueryBuilder();
263
264 $qb->where($criteria);
265
266 if($orderBy !== null) {
267 $qb->orderBy($orderBy);
268 }
269 if($limit !== null) {
270 $qb->limit($limit);
271 }
272 if($offset !== null) {
273 $qb->skip($offset);
274 }
275
276 return $qb->getQuery()->fetch();
277 }
278
279 /**
280 * Retrieve one document.
281 * @param array $criteria
282 * @return array|null single document or NULL if no document can be found
283 * @throws IOException
284 * @throws InvalidArgumentException
285 */
286 public function findOneBy(array $criteria)
287 {
288 $qb = $this->createQueryBuilder();
289
290 $qb->where($criteria);
291
292 $result = $qb->getQuery()->first();
293
294 return (!empty($result))? $result : null;
295
296 }
297
298 /**
299 * Update or insert one document.
300 * @param array $data
301 * @param bool $autoGenerateIdOnInsert
302 * @return array updated / inserted document
303 * @throws IOException
304 * @throws InvalidArgumentException
305 * @throws JsonException
306 */
307 public function updateOrInsert(array $data, bool $autoGenerateIdOnInsert = true): array
308 {
309 $primaryKey = $this->getPrimaryKey();
310
311 if(empty($data)) {
312 throw new InvalidArgumentException("No document to update or insert.");
313 }
314
315 // // we can use this check to determine if multiple documents are given
316 // // because documents have to have at least the primary key.
317 // if(array_keys($data) !== range(0, (count($data) - 1))){
318 // $data = [ $data ];
319 // }
320
321 if(!array_key_exists($primaryKey, $data)) {
322 // $documentString = var_export($document, true);
323 // throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\". Got data: $documentString");
324 $data[$primaryKey] = $this->increaseCounterAndGetNextId();
325 } else {
326 $data[$primaryKey] = $this->checkAndStripId($data[$primaryKey]);
327 if($autoGenerateIdOnInsert && $this->findById($data[$primaryKey]) === null){
328 $data[$primaryKey] = $this->increaseCounterAndGetNextId();
329 }
330 }
331
332 // One document to update or insert
333
334 // save to access file with primary key value because we secured it above
335 $storePath = $this->getDataPath() . "$data[$primaryKey].json";
336 $jsonData = json_encode($data, JSON_INVALID_UTF8_SUBSTITUTE);
337 if ($jsonData === false) {
338 throw new JsonException("Failed to encode document data to JSON: " . json_last_error_msg());
339 }
340 IoHelper::writeContentToFile($storePath, $jsonData);
341
342 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
343
344 return $data;
345 }
346
347 /**
348 * Update or insert multiple documents.
349 * @param array $data
350 * @param bool $autoGenerateIdOnInsert
351 * @return array updated / inserted documents
352 * @throws IOException
353 * @throws InvalidArgumentException
354 * @throws JsonException
355 */
356 public function updateOrInsertMany(array $data, bool $autoGenerateIdOnInsert = true): array
357 {
358 $primaryKey = $this->getPrimaryKey();
359
360 if(empty($data)) {
361 throw new InvalidArgumentException("No documents to update or insert.");
362 }
363
364 // // we can use this check to determine if multiple documents are given
365 // // because documents have to have at least the primary key.
366 // if(array_keys($data) !== range(0, (count($data) - 1))){
367 // $data = [ $data ];
368 // }
369
370 // Check if all documents have the primary key before updating or inserting any
371 foreach ($data as $key => $document){
372 if(!is_array($document)) {
373 throw new InvalidArgumentException('Documents have to be an arrays.');
374 }
375 if(!array_key_exists($primaryKey, $document)) {
376 // $documentString = var_export($document, true);
377 // throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\". Got data: $documentString");
378 $document[$primaryKey] = $this->increaseCounterAndGetNextId();
379 } else {
380 $document[$primaryKey] = $this->checkAndStripId($document[$primaryKey]);
381 if($autoGenerateIdOnInsert && $this->findById($document[$primaryKey]) === null){
382 $document[$primaryKey] = $this->increaseCounterAndGetNextId();
383 }
384 }
385 // after the stripping and checking we apply it back
386 $data[$key] = $document;
387 }
388
389 // One or multiple documents to update or insert
390 foreach ($data as $document) {
391 // save to access file with primary key value because we secured it above
392 $storePath = $this->getDataPath() . "$document[$primaryKey].json";
393 $jsonData = json_encode($document, JSON_INVALID_UTF8_SUBSTITUTE);
394 if ($jsonData === false) {
395 throw new JsonException("Failed to encode document data to JSON: " . json_last_error_msg());
396 }
397 IoHelper::writeContentToFile($storePath, $jsonData);
398 }
399
400 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
401
402 return $data;
403 }
404
405
406 /**
407 * Update one or multiple documents.
408 * @param array $updatable
409 * @return bool true if all documents could be updated and false if one document did not exist
410 * @throws IOException
411 * @throws InvalidArgumentException
412 */
413 public function update(array $updatable): bool
414 {
415 $primaryKey = $this->getPrimaryKey();
416
417 if(empty($updatable)) {
418 throw new InvalidArgumentException("No documents to update.");
419 }
420
421 // we can use this check to determine if multiple documents are given
422 // because documents have to have at least the primary key.
423 if(array_keys($updatable) !== range(0, (count($updatable) - 1))){
424 $updatable = [ $updatable ];
425 }
426
427 // Check if all documents exist and have the primary key before updating any
428 foreach ($updatable as $key => $document){
429 if(!is_array($document)) {
430 throw new InvalidArgumentException('Documents have to be an arrays.');
431 }
432 if(!array_key_exists($primaryKey, $document)) {
433 throw new InvalidArgumentException("Documents have to have the primary key \"$primaryKey\".");
434 }
435
436 $document[$primaryKey] = $this->checkAndStripId($document[$primaryKey]);
437 // after the stripping and checking we apply it back to the updatable array.
438 $updatable[$key] = $document;
439
440 $storePath = $this->getDataPath() . "$document[$primaryKey].json";
441
442 if (!file_exists($storePath)) {
443 return false;
444 }
445 }
446
447 // One or multiple documents to update
448 foreach ($updatable as $document) {
449 // save to access file with primary key value because we secured it above
450 $storePath = $this->getDataPath() . "$document[$primaryKey].json";
451 $jsonData = json_encode($document, JSON_INVALID_UTF8_SUBSTITUTE);
452 if ($jsonData === false) {
453 throw new JsonException("Failed to encode document data to JSON: " . json_last_error_msg());
454 }
455 IoHelper::writeContentToFile($storePath, $jsonData);
456 }
457
458 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
459
460 return true;
461 }
462
463 /**
464 * Update properties of one document.
465 * @param int|string $id
466 * @param array $updatable
467 * @return array|false Updated document or false if document does not exist.
468 * @throws IOException If document could not be read or written.
469 * @throws InvalidArgumentException If one key to update is primary key or $id is not int or string.
470 * @throws JsonException If content of document file could not be decoded.
471 */
472 public function updateById($id, array $updatable)
473 {
474
475
476 $id = $this->checkAndStripId($id);
477
478 $filePath = $this->getDataPath() . "$id.json";
479
480 $primaryKey = $this->getPrimaryKey();
481
482 if(array_key_exists($primaryKey, $updatable)) {
483 throw new InvalidArgumentException("You can not update the primary key \"$primaryKey\" of documents.");
484 }
485
486 if(!file_exists($filePath)){
487 return false;
488 }
489
490 $content = IoHelper::updateFileContent($filePath, function($content) use ($filePath, $updatable){
491
492 $content = @json_decode($content, true);
493
494 if(!is_array($content)){
495 throw new JsonException("[updateFileContent] Could not decode content of \"$filePath\" with json_decode.");
496 }
497
498 foreach ($updatable as $key => $value){
499 NestedHelper::updateNestedValue($key, $content, $value);
500 }
501
502 return json_encode($content);
503
504 });
505
506 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
507
508 return json_decode($content, true);
509 }
510
511 /**
512 * Delete one or multiple documents.
513 * @param array $criteria
514 * @param int $returnOption
515 * @return array|bool|int
516 * @throws IOException
517 * @throws InvalidArgumentException
518 */
519 public function deleteBy(array $criteria, int $returnOption = Query::DELETE_RETURN_BOOL){
520
521 $query = $this->createQueryBuilder()->where($criteria)->getQuery();
522
523 $query->getCache()->deleteAllWithNoLifetime();
524
525 return $query->delete($returnOption);
526 }
527
528 /**
529 * Delete one document by its primary key. Very fast because it deletes the document by its file path.
530 * @param int|string $id
531 * @return bool true if document does not exist or deletion was successful, false otherwise
532 * @throws InvalidArgumentException
533 */
534 public function deleteById($id): bool
535 {
536
537 $id = $this->checkAndStripId($id);
538
539 $filePath = $this->getDataPath() . "$id.json";
540
541 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
542
543 return (!file_exists($filePath) || true === @unlink($filePath));
544 }
545
546 /**
547 * Remove fields from one document by its primary key.
548 * @param int|string $id
549 * @param array $fieldsToRemove
550 * @return false|array
551 * @throws IOException
552 * @throws InvalidArgumentException
553 * @throws JsonException
554 */
555 public function removeFieldsById($id, array $fieldsToRemove)
556 {
557 $id = $this->checkAndStripId($id);
558 $filePath = $this->getDataPath() . "$id.json";
559 $primaryKey = $this->getPrimaryKey();
560
561 if(in_array($primaryKey, $fieldsToRemove, false)) {
562 throw new InvalidArgumentException("You can not remove the primary key \"$primaryKey\" of documents.");
563 }
564 if(!file_exists($filePath)){
565 return false;
566 }
567
568 $content = IoHelper::updateFileContent($filePath, function($content) use ($filePath, $fieldsToRemove){
569 $content = @json_decode($content, true);
570 if(!is_array($content)){
571 throw new JsonException("[removeFieldsById] Could not decode content of \"$filePath\" with json_decode.");
572 }
573 foreach ($fieldsToRemove as $fieldToRemove){
574 NestedHelper::removeNestedField($content, $fieldToRemove);
575 }
576 return $content;
577 });
578
579 $this->createQueryBuilder()->getQuery()->getCache()->deleteAllWithNoLifetime();
580
581 return json_decode($content, true);
582 }
583
584 /**
585 * Do a fulltext like search against one or multiple fields.
586 *
587 * @param array $fields
588 * @param string $query
589 * @param array|null $orderBy
590 * @param int|null $limit
591 * @param int|null $offset
592 *
593 * @return array
594 * @throws IOException
595 * @throws InvalidArgumentException
596 */
597 public function search(array $fields, string $query, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
598 {
599
600 $qb = $this->createQueryBuilder();
601
602 $qb->search($fields, $query);
603
604 if($orderBy !== null) {
605 $qb->orderBy($orderBy);
606 }
607
608 if($limit !== null) {
609 $qb->limit($limit);
610 }
611
612 if($offset !== null) {
613 $qb->skip($offset);
614 }
615
616 return $qb->getQuery()->fetch();
617 }
618
619 /**
620 * Get the name of the field used as the primary key.
621 * @return string
622 */
623 public function getPrimaryKey(): string
624 {
625 return $this->primaryKey;
626 }
627
628 /**
629 * Returns the amount of documents in the store.
630 * @return int
631 * @throws IOException
632 */
633 public function count(): int
634 {
635 if($this->_getUseCache() === true){
636 $cacheTokenArray = ["count" => true];
637 $cache = new Cache($this->getStorePath(), $cacheTokenArray, null);
638 $cacheValue = $cache->get();
639 if(is_array($cacheValue) && array_key_exists("count", $cacheValue)){
640 return $cacheValue["count"];
641 }
642 }
643 $value = [
644 "count" => IoHelper::countFolderContent($this->getDataPath())
645 ];
646 if(isset($cache)) {
647 $cache->set($value);
648 }
649 return $value["count"];
650 }
651
652 /**
653 * Returns the search options of the store.
654 * @return array
655 */
656 public function _getSearchOptions(): array
657 {
658 return $this->searchOptions;
659 }
660
661 /**
662 * Returns if caching is enabled store wide.
663 * @return bool
664 */
665 public function _getUseCache(): bool
666 {
667 return $this->useCache;
668 }
669
670 /**
671 * Returns the store wide default cache lifetime.
672 * @return null|int
673 */
674 public function _getDefaultCacheLifetime()
675 {
676 return $this->defaultCacheLifetime;
677 }
678
679 /**
680 * @return string
681 * @deprecated since version 2.7, use getDatabasePath instead.
682 */
683 public function getDataDirectory(): string
684 {
685 // TODO remove with version 3.0
686 return $this->databasePath;
687 }
688
689 /**
690 * @throws IOException
691 */
692 private function createDatabasePath()
693 {
694 $databasePath = $this->getDatabasePath();
695 IoHelper::createFolder($databasePath, $this->folderPermissions);
696 }
697
698 /**
699 * @throws IOException
700 */
701 private function createStore()
702 {
703 $storeName = $this->getStoreName();
704 // Prepare store name.
705 IoHelper::normalizeDirectory($storeName);
706 // Store directory path.
707 $this->storePath = $this->getDatabasePath() . $storeName;
708 $storePath = $this->getStorePath();
709 IoHelper::createFolder($storePath, $this->folderPermissions);
710
711 // Create the cache directory.
712 $cacheDirectory = $storePath . 'cache';
713 IoHelper::createFolder($cacheDirectory, $this->folderPermissions);
714
715 // Create the data directory.
716 IoHelper::createFolder($storePath . self::dataDirectory, $this->folderPermissions);
717
718 // Create the store counter file.
719 $counterFile = $storePath . '_cnt.sdb';
720 if(!file_exists($counterFile)){
721 IoHelper::writeContentToFile($counterFile, '0');
722 }
723 }
724
725 /**
726 * @param array $configuration
727 * @throws InvalidConfigurationException
728 */
729 private function setConfiguration(array $configuration)
730 {
731 if(array_key_exists("auto_cache", $configuration)){
732 $autoCache = $configuration["auto_cache"];
733 if(!is_bool($configuration["auto_cache"])){
734 throw new InvalidConfigurationException("auto_cache has to be boolean");
735 }
736
737 $this->useCache = $autoCache;
738 }
739
740 if(array_key_exists("cache_lifetime", $configuration)){
741 $defaultCacheLifetime = $configuration["cache_lifetime"];
742 if(!is_int($defaultCacheLifetime) && !is_null($defaultCacheLifetime)){
743 throw new InvalidConfigurationException("cache_lifetime has to be null or int");
744 }
745
746 $this->defaultCacheLifetime = $defaultCacheLifetime;
747 }
748
749 // TODO remove timeout on major update
750 // Set timeout.
751 if (array_key_exists("timeout", $configuration)) {
752 if ((!is_int($configuration['timeout']) || $configuration['timeout'] <= 0) && !($configuration['timeout'] === false)){
753 throw new InvalidConfigurationException("timeout has to be an int > 0 or false");
754 }
755 $this->timeout = $configuration["timeout"];
756 }
757 if($this->timeout !== false){
758 $message = 'The "timeout" configuration is deprecated and will be removed with the next major update.' .
759 ' Set the "timeout" configuration to false and if needed use the set_timeout_limit() function in your own code.';
760 trigger_error($message, E_USER_DEPRECATED);
761 set_time_limit($this->timeout);
762 }
763
764 if(array_key_exists("primary_key", $configuration)){
765 $primaryKey = $configuration["primary_key"];
766 if(!is_string($primaryKey)){
767 throw new InvalidConfigurationException("primary key has to be a string");
768 }
769 $this->primaryKey = $primaryKey;
770 }
771
772 if(array_key_exists("search", $configuration)){
773 $searchConfig = $configuration["search"];
774
775 if(array_key_exists("min_length", $searchConfig)){
776 $searchMinLength = $searchConfig["min_length"];
777 if(!is_int($searchMinLength) || $searchMinLength <= 0){
778 throw new InvalidConfigurationException("min length for searching has to be an int >= 0");
779 }
780 $this->searchOptions["minLength"] = $searchMinLength;
781 }
782
783 if(array_key_exists("mode", $searchConfig)){
784 $searchMode = $searchConfig["mode"];
785 if(!is_string($searchMode) || !in_array(strtolower(trim($searchMode)), ["and", "or"])){
786 throw new InvalidConfigurationException("search mode can just be \"and\" or \"or\"");
787 }
788 $this->searchOptions["mode"] = strtolower(trim($searchMode));
789 }
790
791 if(array_key_exists("score_key", $searchConfig)){
792 $searchScoreKey = $searchConfig["score_key"];
793 if((!is_string($searchScoreKey) && !is_null($searchScoreKey))){
794 throw new InvalidConfigurationException("search score key for search has to be a not empty string or null");
795 }
796 $this->searchOptions["scoreKey"] = $searchScoreKey;
797 }
798
799 if(array_key_exists("algorithm", $searchConfig)){
800 $searchAlgorithm = $searchConfig["algorithm"];
801 if(!in_array($searchAlgorithm, Query::SEARCH_ALGORITHM, true)){
802 $searchAlgorithm = implode(', ', $searchAlgorithm);
803 throw new InvalidConfigurationException("The search algorithm has to be one of the following integer values ($searchAlgorithm)");
804 }
805 $this->searchOptions["algorithm"] = $searchAlgorithm;
806 }
807 }
808
809 if ( array_key_exists("folder_permissions", $configuration) ) {
810 $folderPermissions = $configuration["folder_permissions"];
811 if ( !is_int($folderPermissions) ) {
812 throw new InvalidConfigurationException("folder_permissions has to be an integer (e.g. 0777)");
813 }
814 $this->folderPermissions = $folderPermissions;
815 }
816 }
817
818 /**
819 * Writes an object in a store.
820 * @param array $storeData
821 * @return array
822 * @throws IOException
823 * @throws IdNotAllowedException
824 * @throws JsonException
825 */
826 private function writeNewDocumentToStore(array $storeData): array
827 {
828 $primaryKey = $this->getPrimaryKey();
829 // Check if it has the primary key
830 if (isset($storeData[$primaryKey])) {
831 throw new IdNotAllowedException(
832 "The \"$primaryKey\" index is reserved by SleekDB, please delete the $primaryKey key and try again"
833 );
834 }
835 $id = $this->increaseCounterAndGetNextId();
836 // Add the system ID with the store data array.
837 $storeData[$primaryKey] = $id;
838 // Prepare storable data
839 $storableJSON = @json_encode($storeData, JSON_INVALID_UTF8_SUBSTITUTE);
840 if ($storableJSON === false) {
841 throw new JsonException('Unable to encode the data array,
842 please provide a valid PHP associative array');
843 }
844 // Define the store path
845 $filePath = $this->getDataPath()."$id.json";
846
847 IoHelper::writeContentToFile($filePath, $storableJSON);
848
849 return $storeData;
850 }
851
852 /**
853 * Increments the store wide unique store object ID and returns it.
854 * @return int
855 * @throws IOException
856 * @throws JsonException
857 */
858 private function increaseCounterAndGetNextId(): int
859 {
860 $counterPath = $this->getStorePath() . '_cnt.sdb';
861
862 if (!file_exists($counterPath)) {
863 throw new IOException("File $counterPath does not exist.");
864 }
865
866 $dataPath = $this->getDataPath();
867
868 return (int) IoHelper::updateFileContent($counterPath, function ($counter) use ($dataPath){
869 $newCounter = ((int) $counter) + 1;
870
871 while(file_exists($dataPath."$newCounter.json") === true){
872 $newCounter++;
873 }
874 return (string)$newCounter;
875 });
876 }
877
878
879 /**
880 * @param string|int $id
881 * @return int
882 * @throws InvalidArgumentException
883 */
884 private function checkAndStripId($id): int
885 {
886 if(!is_string($id) && !is_int($id)){
887
888 throw new InvalidArgumentException("The id of the document has to be an integer or string");
889 }
890
891 if(is_string($id)){
892 $id = IoHelper::secureStringForFileAccess($id);
893 }
894
895 if(!is_numeric($id)){
896 throw new InvalidArgumentException("The id of the document has to be numeric");
897 }
898
899 return (int) $id;
900 }
901
902 /**
903 * @return string
904 */
905 private function getDataPath(): string
906 {
907 return $this->getStorePath() . self::dataDirectory;
908 }
909
910 }
911