PSQLStore   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 206
Duplicated Lines 0 %

Importance

Changes 9
Bugs 4 Features 5
Metric Value
wmc 21
eloc 101
c 9
b 4
f 5
dl 0
loc 206
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A save() 0 18 5
A ls() 0 4 1
A delete() 0 12 1
A get() 0 13 4
B initialize() 0 49 5
A __construct() 0 6 1
A cd() 0 3 1
A exists() 0 6 1
A parents() 0 8 1
A find() 0 5 1
1
<?php
2
namespace arc\store;
3
4
/*
5
TODO: implement links
6
*/
7
final class PSQLStore implements Store {
8
9
    private $db;
10
    private $queryParser;
11
    private $resultHandler;
12
    private $path;
13
14
    /**
15
     * PSQLStore constructor.
16
     * @param \PDO $db
17
     * @param callable $queryParser
18
     * @param callable $resultHandler
19
     * @param string $path
20
     */
21
    public function __construct($db = null, $queryParser = null, $resultHandler = null, $path = '/')
22
    {
23
        $this->db            = $db;
24
        $this->queryParser   = $queryParser;
25
        $this->resultHandler = $resultHandler;
26
        $this->path          = \arc\path::collapse($path);
27
    }
28
29
    /**
30
     * change the current path, returns a new store instance for that path
31
     * @param string $path
32
     * @return PSQLStore
33
     */
34
    public function cd($path)
35
    {
36
        return new self( $this->db, $this->queryParser, $this->resultHandler, \arc\path::collapse($path, $this->path) );
37
    }
38
39
    /**
40
     * creates sql query for the search query and returns the resulthandler
41
     * @param string $query
42
     * @param string $path
43
     * @return mixed
44
     */
45
    public function find($query, $path='')
46
    {
47
        $path = \arc\path::collapse($path, $this->path);
48
        $sql  = $this->queryParser->parse($query, $path);
0 ignored issues
show
Bug introduced by
The method parse() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

48
        /** @scrutinizer ignore-call */ 
49
        $sql  = $this->queryParser->parse($query, $path);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
49
        return ($this->resultHandler)( $sql, [] );
50
    }
51
52
    /**
53
     * get a single object from the store by path
54
     * @param string $path
55
     * @return mixed
56
     */
57
    public function get($path='')
58
    {
59
        $path   = \arc\path::collapse($path, $this->path);
60
        $parent = ($path=='/' ? '' : \arc\path::parent($path));
61
        $name   = ($path=='/' ? '' : basename($path));
62
        $result = ($this->resultHandler)(
63
            'parent=:parent and name=:name', 
64
            [':parent' => $parent, ':name' => $name]
65
        );
66
        if (!is_array($result)) {
67
            $result = iterator_to_array($result);
68
        }
69
        return array_pop($result);
70
    }
71
72
    /**
73
     * list all parents, including self, by path, starting from the root
74
     * @param string $path
75
     * @param string $top
76
     * @return mixed
77
     */
78
    public function parents($path='', $top='/')
79
    {
80
        $path   = \arc\path::collapse($path, $this->path);
81
        return ($this->resultHandler)(
82
            /** @lang sql */
83
            'lower(path)=lower(substring(:path,1,length(path))) '
84
            . ' and lower(path) LIKE lower(:top) order by path',
85
            [':path' => $path, ':top' => $top.'%']
86
        );
87
    }
88
89
    /**
90
     * list all child objects by path
91
     * @param string $path
92
     * @return mixed
93
     */
94
    public function ls($path='')
95
    {
96
        $path   = \arc\path::collapse($path, $this->path);
97
        return ($this->resultHandler)('parent=:path', [':path' => $path]);
98
    }
99
100
    /**
101
     * returns true if an object with the given path exists
102
     * @param string $path
103
     * @return bool
104
     */
105
    public function exists($path='')
106
    {
107
        $path   = \arc\path::collapse($path, $this->path);
108
        $query  = $this->db->prepare('select count(*) from nodes where path=:path');
0 ignored issues
show
Bug introduced by
The method prepare() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

108
        /** @scrutinizer ignore-call */ 
109
        $query  = $this->db->prepare('select count(*) from nodes where path=:path');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
109
        $result = $query->execute([':path' => $path]);
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
110
        return ($query->fetchColumn(0)>0);
111
    }
112
113
    /**
114
     * initialize the postgresql database, if it wasn't before
115
     * @return bool|mixed
116
     */
117
    public function initialize() {
118
        try {
119
            if ($result=$this->exists('/')) {
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
120
                return false;
121
            }
122
        } catch (\PDOException $e) {
123
            // expected exception
124
        }
125
126
        $queries = [];
127
        $queries[] = "begin;";
128
        $queries[] = "create extension if not exists pgcrypto;";
129
        $queries[] = <<<SQL
130
create table objects (
131
    id     uuid primary key default gen_random_uuid(),
132
    parent text not null ,
133
    name   text not null,
134
    data   jsonb not null,
135
    ctime  timestamp default current_timestamp,
136
    mtime  timestamp default current_timestamp,
137
    UNIQUE(parent,name)
138
);
139
SQL;
140
        $queries[] = "create unique index path on objects ((parent || name || '/'));";
141
        $queries[] = "create unique index lower_path on objects ((lower(parent) || lower(name) || '/' ));";
142
        $queries[] = "create index datagin on objects using gin (data);";
143
        $queries[] = "create view nodes as select (parent || name || '/') as path, * from objects;";
144
        $queries[] = <<<SQL
145
create table links (
146
    from_id  uuid references objects(id),
147
    to_id    uuid references objects(id),
148
    relation text not null,
149
    UNIQUE(from_id,to_id)
150
);
151
SQL;
152
        $queries[] = "create index link_from on links(from_id);";
153
        $queries[] = "create index link_to on links(to_id);";
154
        foreach ( $queries as $query ) {
155
            $result = $this->db->exec($query);
156
            if ($result===false) {
157
                $this->db->exec('rollback;');
158
                return false;
159
            }
160
        }
161
        $this->db->exec('commit;');
162
163
        return $this->save(\arc\prototype::create([
164
            'name' => 'Root'
165
        ]),'/');
166
    }
167
168
    /**
169
     * save (insert or update) a single object on the given path
170
     * @param $data
171
     * @param string $path
172
     * @return mixed
173
     */
174
    public function save($data, $path='') {
175
        $path   = \arc\path::collapse($path, $this->path);
176
        $parent = ($path=='/' ? '' : \arc\path::parent($path));
177
        if ($path!='/' && !$this->exists($parent)) {
178
            throw new \arc\IllegalRequest("Parent $parent not found.", \arc\exceptions::OBJECT_NOT_FOUND);
179
        }
180
        $name = ($path=='/' ? '' : basename($path));
181
        $queryStr = <<<EOF
182
insert into objects (parent, name, data) 
183
values (:parent, :name, :data) 
184
on conflict(parent, name) do update 
185
  set data = :data;
186
EOF;
187
        $query = $this->db->prepare($queryStr);
188
        return $query->execute([
189
            ':parent' => $parent,
190
            ':name'   => $name,
191
            ':data'   => json_encode($data)
192
        ]);
193
    }
194
195
    /**
196
     * remove the object with the given path and all its children
197
     * won't remove the root object ever
198
     * @param string $path
199
     * @return mixed
200
     */
201
    public function delete($path = '') {
202
        $path   = \arc\path::collapse($path, $this->path);
203
        $parent = \arc\path::parent($path);
204
        $name   = basename($path);
205
        $queryStr = <<<EOF
206
delete from objects where (parent like :path or (parent = :parent and name = :name ))
207
EOF;
208
        $query = $this->db->prepare($queryStr);
209
        return $query->execute([
210
            ':path' => $path.'%',
211
            ':parent' => $parent,
212
            ':name' => $name
213
        ]);
214
    }
215
216
}
217