Passed
Push — develop ( 96efe9...5c0545 )
by Jens
02:56
created

Repository::publishOrUnpublishDocumentByPath()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 4
nop 2
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
1
<?php
2
/**
3
 * User: Jens
4
 * Date: 30-1-2017
5
 * Time: 20:15
6
 */
7
8
namespace CloudControl\Cms\storage;
9
10
use CloudControl\Cms\storage\storage\DocumentStorage;
11
12
/**
13
 * Class Repository
14
 * @package CloudControl\Cms\storage
15
 * @property array sitemap
16
 * @property array applicationComponents
17
 * @property array documentTypes
18
 * @property array bricks
19
 * @property array imageSet
20
 * @property array images
21
 * @property array files
22
 * @property array users
23
 * @property array valuelists
24
 * @property array redirects
25
 */
26
class Repository
27
{
28
    protected $storagePath;
29
30
    protected $fileBasedSubsets = array('sitemap', 'applicationComponents', 'documentTypes', 'bricks', 'imageSet', 'images', 'files', 'users', 'valuelists', 'redirects');
31
32
    protected $sitemap;
33
    protected $sitemapChanges = false;
34
35
    protected $applicationComponents;
36
    protected $applicationComponentsChanges = false;
37
38
    protected $documentTypes;
39
    protected $documentTypesChanges = false;
40
41
    protected $bricks;
42
    protected $bricksChanges = false;
43
44
    protected $imageSet;
45
    protected $imageSetChanges = false;
46
47
    protected $images;
48
    protected $imagesChanges = false;
49
50
    protected $files;
51
    protected $filesChanges = false;
52
53
    protected $users;
54
    protected $usersChanges = false;
55
56
    protected $valuelists;
57
    protected $valuelistsChanges = false;
58
59
    protected $redirects;
60
    protected $redirectsChanges = false;
61
62
    protected $contentDbHandle;
63
64
65
    /**
66
     * Repository constructor.
67
     * @param $storagePath
68
     * @throws \Exception
69
     */
70
    public function __construct($storagePath)
71
    {
72
        $storagePath = realpath($storagePath);
73
        if (is_dir($storagePath) && $storagePath !== false) {
74
            $this->storagePath = $storagePath;
75
        } else {
76
            throw new \Exception('Repository not yet initialized.');
77
        }
78
    }
79
80
    /**
81
     * Creates the folder in which to create all storage related files
82
     *
83
     * @param $storagePath
84
     * @return bool
85
     */
86
    public static function create($storagePath)
87
    {
88
        return mkdir($storagePath);
89
    }
90
91
    /**
92
     * Initiates default storage
93
     * @param $baseStorageDefaultPath
94
     * @param $baseStorageSqlPath
95
     */
96
    public function init($baseStorageDefaultPath, $baseStorageSqlPath)
97
    {
98
        $storageDefaultPath = realpath($baseStorageDefaultPath);
99
        $contentSqlPath = realpath($baseStorageSqlPath);
100
101
        $this->initConfigStorage($storageDefaultPath);
102
        $this->initContentDb($contentSqlPath);
103
104
        $this->save();
105
    }
106
107
    /**
108
     * Load filebased subset and return it's contents
109
     *
110
     * @param $name
111
     * @return mixed|string
112
     * @throws \Exception
113
     */
114
    public function __get($name)
115
    {
116
        if (isset($this->$name)) {
117 View Code Duplication
            if (in_array($name, $this->fileBasedSubsets)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
118
                return $this->$name;
119
            } else {
120
                throw new \Exception('Trying to get undefined property from Repository: ' . $name);
121
            }
122 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
123
            if (in_array($name, $this->fileBasedSubsets)) {
124
                return $this->loadSubset($name);
125
            } else {
126
                throw new \Exception('Trying to get undefined property from Repository: ' . $name);
127
            }
128
        }
129
    }
130
131
    /**
132
     * Set filebased subset contents
133
     * @param $name
134
     * @param $value
135
     * @throws \Exception
136
     */
137
    public function __set($name, $value)
138
    {
139
        if (in_array($name, $this->fileBasedSubsets)) {
140
            $this->$name = $value;
141
            $changes = $name . 'Changes';
142
            $this->$changes = true;
143
        } else {
144
            throw new \Exception('Trying to persist unknown subset in repository: ' . $name . ' <br /><pre>' . print_r($value, true) . '</pre>');
145
        }
146
    }
147
148
    /**
149
     * Persist all subsets
150
     */
151
    public function save()
152
    {
153
        $host = $this;
154
        array_map(function ($value) use ($host) {
155
            $host->saveSubset($value);
156
        }, $this->fileBasedSubsets);
157
    }
158
159
    /**
160
     * Persist subset to disk
161
     * @param $subset
162
     */
163
    public function saveSubset($subset)
164
    {
165
        $changes = $subset . 'Changes';
166
        if ($this->$changes === true) {
167
            if (!defined('JSON_PRETTY_PRINT')) {
168
                $json = json_encode($this->$subset);
169
            } else {
170
                $json = json_encode($this->$subset, JSON_PRETTY_PRINT);
171
            }
172
            $subsetStoragePath = $this->storagePath . DIRECTORY_SEPARATOR . $subset . '.json';
173
            file_put_contents($subsetStoragePath, $json);
174
175
            $this->$changes = false;
176
        }
177
    }
178
179
    /**
180
     * Load subset from disk
181
     * @param $subset
182
     * @return mixed|string
183
     */
184
    protected function loadSubset($subset)
185
    {
186
        $subsetStoragePath = $this->storagePath . DIRECTORY_SEPARATOR . $subset . '.json';
187
        $json = file_get_contents($subsetStoragePath);
188
        $json = json_decode($json);
189
        $this->$subset = $json;
190
        return $json;
191
    }
192
193
    /**
194
     * @param $contentSqlPath
195
     */
196
    protected function initContentDb($contentSqlPath)
197
    {
198
        $db = $this->getContentDbHandle();
199
        $sql = file_get_contents($contentSqlPath);
200
        $db->exec($sql);
201
    }
202
203
    /**
204
     * @param $storageDefaultPath
205
     */
206
    protected function initConfigStorage($storageDefaultPath)
207
    {
208
        $json = file_get_contents($storageDefaultPath);
209
        $json = json_decode($json);
210
        $this->initConfigIfNotExists($json, 'sitemap');
211
        $this->initConfigIfNotExists($json, 'applicationComponents');
212
        $this->initConfigIfNotExists($json, 'documentTypes');
213
        $this->initConfigIfNotExists($json, 'bricks');
214
        $this->initConfigIfNotExists($json, 'imageSet');
215
        $this->initConfigIfNotExists($json, 'images');
216
        $this->initConfigIfNotExists($json, 'files');
217
        $this->initConfigIfNotExists($json, 'users');
218
        $this->initConfigIfNotExists($json, 'valuelists');
219
        $this->initConfigIfNotExists($json, 'redirects');
220
    }
221
222
    /**
223
     * @return \PDO
224
     */
225
    public function getContentDbHandle()
226
    {
227
        if ($this->contentDbHandle === null) {
228
            $this->contentDbHandle = new \PDO('sqlite:' . $this->storagePath . DIRECTORY_SEPARATOR . 'content.db');
229
        }
230
        return $this->contentDbHandle;
231
    }
232
233
    /**
234
     * Get all documents
235
     *
236
     * @param string $state
237
     *
238
     * @return array
239
     * @throws \Exception
240
     */
241
    public function getDocuments($state = 'published')
242
    {
243
        if (!in_array($state, Document::$DOCUMENT_STATES)) {
244
            throw new \Exception('Unsupported document state: ' . $state);
245
        }
246
        return $this->getDocumentsByPath('/', $state);
247
    }
248
249
    public function getDocumentsWithState($folderPath = '/')
250
    {
251
        $db = $this->getContentDbHandle();
252
        $folderPathWithWildcard = $folderPath . '%';
253
254
        $ifRootIndex = 1;
255
        if ($folderPath == '/') {
256
            $ifRootIndex = 0;
257
        }
258
259
        $sql = '
260
            SELECT documents_unpublished.*,
261
            	   IFNULL(documents_published.state,"unpublished") as state,
262
            	   IFNULL(documents_published.publicationDate,NULL) as publicationDate,
263
            	   (documents_published.lastModificationDate != documents_unpublished.lastModificationDate) as unpublishedChanges 
264
              FROM documents_unpublished
265
		 LEFT JOIN documents_published
266
         		ON documents_published.path = documents_unpublished.path
267
             WHERE documents_unpublished.`path` LIKE ' . $db->quote($folderPathWithWildcard) . '
268
               AND substr(documents_unpublished.`path`, ' . (strlen($folderPath) + $ifRootIndex + 1) . ') NOT LIKE "%/%"
269
               AND length(documents_unpublished.`path`) > ' . (strlen($folderPath) + $ifRootIndex) . '
270
               AND documents_unpublished.path != ' . $db->quote($folderPath) . '
271
          ORDER BY documents_unpublished.`type` DESC, documents_unpublished.`path` ASC
272
        ';
273
        $stmt = $this->getDbStatement($sql);
274
275
276
        $documents = $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
277
        foreach ($documents as $key => $document) {
278
            $documents = $this->setAssetsToDocumentFolders($document, $db, $documents, $key);
279
        }
280
        //dump($documents);
281
        return $documents;
282
    }
283
284
    /**
285
     * Get all documents and folders in a certain path
286
     *
287
     * @param        $folderPath
288
     * @param string $state
289
     *
290
     * @return array
291
     * @throws \Exception
292
     */
293
    public function getDocumentsByPath($folderPath, $state = 'published')
294
    {
295
        if (!in_array($state, Document::$DOCUMENT_STATES)) {
296
            throw new \Exception('Unsupported document state: ' . $state);
297
        }
298
        $db = $this->getContentDbHandle();
299
        $folderPathWithWildcard = $folderPath . '%';
300
301
        $sql = 'SELECT *
302
              FROM documents_' . $state . '
303
             WHERE `path` LIKE ' . $db->quote($folderPathWithWildcard) . '
304
               AND substr(`path`, ' . (strlen($folderPath) + 1) . ') NOT LIKE "%/%"
305
               AND path != ' . $db->quote($folderPath) . '
306
          ORDER BY `type` DESC, `path` ASC';
307
        $stmt = $this->getDbStatement($sql);
308
309
        $documents = $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
310
        foreach ($documents as $key => $document) {
311
            $documents = $this->setAssetsToDocumentFolders($document, $db, $documents, $key);
312
        }
313
        return $documents;
314
    }
315
316
317
    /**
318
     * @param $path
319
     * @return bool|Document
320
     */
321
    public function getDocumentContainerByPath($path)
322
    {
323
        $document = $this->getDocumentByPath($path, 'unpublished');
324
        if ($document === false) {
325
            return false;
326
        }
327
        $slugLength = strlen($document->slug);
328
        $containerPath = substr($path, 0, -$slugLength);
329
        if ($containerPath === '/') {
330
            return $this->getRootFolder();
331
        }
332
        if (substr($containerPath, -1) === '/') {
333
            $containerPath = substr($containerPath, 0, -1);
334
        }
335
        $containerFolder = $this->getDocumentByPath($containerPath, 'unpublished');
336
        return $containerFolder;
337
    }
338
339
    /**
340
     * @param        $path
341
     * @param string $state
342
     *
343
     * @return bool|\CloudControl\Cms\storage\Document
344
     * @throws \Exception
345
     */
346
    public function getDocumentByPath($path, $state = 'published')
347
    {
348
        if (!in_array($state, Document::$DOCUMENT_STATES)) {
349
            throw new \Exception('Unsupported document state: ' . $state);
350
        }
351
        $db = $this->getContentDbHandle();
352
        $document = $this->fetchDocument('
353
            SELECT *
354
              FROM documents_' . $state . '
355
             WHERE path = ' . $db->quote($path) . '
356
        ');
357
        if ($document instanceof Document && $document->type === 'folder') {
358
            $document->dbHandle = $db;
359
            $document->documentStorage = new DocumentStorage($this);
360
        }
361
        return $document;
362
    }
363
364
    /**
365
     * Returns the count of all documents stored in the db
366
     *
367
     * @param string $state
368
     *
369
     * @return int
370
     * @throws \Exception
371
     */
372
    public function getTotalDocumentCount($state = 'published')
373
    {
374
        if (!in_array($state, Document::$DOCUMENT_STATES)) {
375
            throw new \Exception('Unsupported document state: ' . $state);
376
        }
377
        $db = $this->getContentDbHandle();
378
        $stmt = $db->query('
379
			SELECT count(*)
380
			  FROM documents_' . $state . '
381
			 WHERE `type` != "folder"
382
		');
383
        $result = $stmt->fetch(\PDO::FETCH_ASSOC);
384
        if (!is_array($result)) {
385
            return 0;
386
        }
387
        return intval(current($result));
388
    }
389
390
    public function getPublishedDocumentsNoFolders()
391
    {
392
        $db = $this->getContentDbHandle();
393
        $sql = '
394
			SELECT *
395
			  FROM documents_published
396
			 WHERE `type` != "folder"
397
		';
398
        $stmt = $db->query($sql);
399
        $result = $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
400
        if ($stmt === false || !$stmt->execute()) {
401
            $errorInfo = $db->errorInfo();
402
            $errorMsg = $errorInfo[2];
403
            throw new \Exception('SQLite Exception: ' . $errorMsg . ' in SQL: <br /><pre>' . $sql . '</pre>');
404
        }
405
        return $result;
406
    }
407
408
    private function publishOrUnpublishDocumentByPath($path, $publish = true)
409
    {
410
        if ($publish) {
411
            $sql = '
412
				INSERT OR REPLACE INTO documents_published 
413
					  (`id`,`path`,`title`,`slug`,`type`,`documentType`,`documentTypeSlug`,`state`,`lastModificationDate`,`creationDate`,`publicationDate`,`lastModifiedBy`,`fields`,`bricks`,`dynamicBricks`)
414
				SELECT `id`,`path`,`title`,`slug`,`type`,`documentType`,`documentTypeSlug`,"published" as state,`lastModificationDate`,`creationDate`,' . time() . ' as publicationDate, `lastModifiedBy`,`fields`,`bricks`,`dynamicBricks`
415
				  FROM documents_unpublished
416
				 WHERE `path` = :path
417
			';
418
        } else {
419
            $sql = 'DELETE FROM documents_published
420
					  WHERE `path` = :path';
421
        }
422
        $db = $this->getContentDbHandle();
423
        $stmt = $db->prepare($sql);
424
        if ($stmt === false) {
425
            $errorInfo = $db->errorInfo();
426
            $errorMsg = $errorInfo[2];
427
            throw new \Exception('SQLite Exception: ' . $errorMsg . ' in SQL: <br /><pre>' . $sql . '</pre>');
428
        }
429
        $stmt->bindValue(':path', $path);
430
        $stmt->execute();
431
    }
432
433
    public function publishDocumentByPath($path)
434
    {
435
        $this->publishOrUnpublishDocumentByPath($path);
436
    }
437
438
    public function unpublishDocumentByPath($path)
439
    {
440
        $this->publishOrUnpublishDocumentByPath($path, false);
441
    }
442
443
    public function cleanPublishedDeletedDocuments()
444
    {
445
        $sql = '   DELETE FROM documents_published
446
						 WHERE documents_published.path IN (
447
						SELECT documents_published.path
448
						  FROM documents_published
449
					 LEFT JOIN documents_unpublished
450
							ON documents_unpublished.path = documents_published.path
451
						 WHERE documents_unpublished.path IS NULL
452
		)';
453
        $stmt = $this->getDbStatement($sql);
454
        $stmt->execute();
455
    }
456
457
    /**
458
     * Return the results of the query as array of Documents
459
     * @param $sql
460
     * @return array
461
     * @throws \Exception
462
     */
463
    protected function fetchAllDocuments($sql)
464
    {
465
        $stmt = $this->getDbStatement($sql);
466
        return $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
467
    }
468
469
    /**
470
     * Return the result of the query as Document
471
     * @param $sql
472
     * @return mixed
473
     * @throws \Exception
474
     */
475
    protected function fetchDocument($sql)
476
    {
477
        $stmt = $this->getDbStatement($sql);
478
        return $stmt->fetchObject('\CloudControl\Cms\storage\Document');
479
    }
480
481
    /**
482
     * Prepare the sql statement
483
     * @param $sql
484
     * @return \PDOStatement
485
     * @throws \Exception
486
     */
487
    protected function getDbStatement($sql)
488
    {
489
        $db = $this->getContentDbHandle();
490
        $stmt = $db->query($sql);
491
        if ($stmt === false) {
492
            $errorInfo = $db->errorInfo();
493
            $errorMsg = $errorInfo[2];
494
            throw new \Exception('SQLite Exception: ' . $errorMsg . ' in SQL: <br /><pre>' . $sql . '</pre>');
495
        }
496
        return $stmt;
497
    }
498
499
    /**
500
     * Create a (non-existent) root folder Document and return it
501
     * @return Document
502
     */
503
    protected function getRootFolder()
504
    {
505
        $rootFolder = new Document();
506
        $rootFolder->path = '/';
507
        $rootFolder->type = 'folder';
508
        return $rootFolder;
509
    }
510
511
    /**
512
     * Save the document to the database
513
     *
514
     * @param Document $documentObject
515
     * @param string $state
516
     *
517
     * @return bool
518
     * @throws \Exception
519
     * @internal param $path
520
     */
521
    public function saveDocument($documentObject, $state = 'published')
522
    {
523
        if (!in_array($state, Document::$DOCUMENT_STATES)) {
524
            throw new \Exception('Unsupported document state: ' . $state);
525
        }
526
        $db = $this->getContentDbHandle();
527
        $stmt = $this->getDbStatement('
528
            INSERT OR REPLACE INTO documents_' . $state . ' (`path`,`title`,`slug`,`type`,`documentType`,`documentTypeSlug`,`state`,`lastModificationDate`,`creationDate`,`lastModifiedBy`,`fields`,`bricks`,`dynamicBricks`)
529
            VALUES(
530
              ' . $db->quote($documentObject->path) . ',
531
              ' . $db->quote($documentObject->title) . ',
532
              ' . $db->quote($documentObject->slug) . ',
533
              ' . $db->quote($documentObject->type) . ',
534
              ' . $db->quote($documentObject->documentType) . ',
535
              ' . $db->quote($documentObject->documentTypeSlug) . ',
536
              ' . $db->quote($documentObject->state) . ',
537
              ' . $db->quote($documentObject->lastModificationDate) . ',
538
              ' . $db->quote($documentObject->creationDate) . ',
539
              ' . $db->quote($documentObject->lastModifiedBy) . ',
540
              ' . $db->quote(json_encode($documentObject->fields)) . ',
541
              ' . $db->quote(json_encode($documentObject->bricks)) . ',
542
              ' . $db->quote(json_encode($documentObject->dynamicBricks)) . '
543
            )
544
        ');
545
        $result = $stmt->execute();
546
        return $result;
547
    }
548
549
    /**
550
     * Delete the document from the database
551
     * If it's a folder, also delete it's contents
552
     *
553
     * @param        $path
554
     *
555
     * @internal param string $state
556
     *
557
     */
558
    public function deleteDocumentByPath($path)
559
    {
560
        $db = $this->getContentDbHandle();
561
        $documentToDelete = $this->getDocumentByPath($path, 'unpublished');
562
        if ($documentToDelete instanceof Document) {
563
            if ($documentToDelete->type == 'document') {
564
                $stmt = $this->getDbStatement('
565
                    DELETE FROM documents_unpublished
566
                          WHERE path = ' . $db->quote($path) . '
567
                ');
568
                $stmt->execute();
569
            } elseif ($documentToDelete->type == 'folder') {
570
                $folderPathWithWildcard = $path . '%';
571
                $stmt = $this->getDbStatement('
572
                    DELETE FROM documents_unpublished
573
                          WHERE (path LIKE ' . $db->quote($folderPathWithWildcard) . '
574
                            AND substr(`path`, ' . (strlen($path) + 1) . ', 1) = "/")
575
                            OR path = ' . $db->quote($path) . '
576
                ');
577
                $stmt->execute();
578
            }
579
        }
580
    }
581
582
    /**
583
     * @param $document
584
     * @param $db
585
     * @param $documents
586
     * @param $key
587
     *
588
     * @return mixed
589
     */
590
    private function setAssetsToDocumentFolders($document, $db, $documents, $key)
591
    {
592
        if ($document->type === 'folder') {
593
            $document->dbHandle = $db;
594
            $document->documentStorage = new DocumentStorage($this);
595
            $documents[$key] = $document;
596
        }
597
598
        return $documents;
599
    }
600
601
    private function initConfigIfNotExists($json, $subsetName)
602
    {
603
        $subsetFileName = $this->storagePath . DIRECTORY_SEPARATOR . $subsetName . '.json';
604
        if (file_exists($subsetFileName)) {
605
            $this->loadSubset($subsetName);
606
        } else {
607
            $changes = $subsetName . 'Changes';
608
            $this->$subsetName = $json->$subsetName;
609
            $this->$changes = true;
610
        }
611
    }
612
}