Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
BasicDbStorage
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 9
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 insert
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 update
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 remove
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 find
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 doFindQuery
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 fallbackFindMulti
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 findMulti
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 getPrimaryKeyColumns
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow\Data\Storage;
4
5use Flow\Data\ObjectManager;
6use Flow\Data\Utils\MultiDimArray;
7use Flow\Data\Utils\RawSql;
8use Flow\DbFactory;
9use Flow\Exception\DataModelException;
10use Flow\Exception\DataPersistenceException;
11use Flow\Model\UUID;
12use InvalidArgumentException;
13
14/**
15 * Standard backing store for data model with no special cases which is stored
16 * in a single table in mysql.
17 *
18 * Doesn't support updating primary key value yet
19 * Doesn't support auto-increment pk yet
20 */
21class BasicDbStorage extends DbStorage {
22    /**
23     * @var string
24     */
25    protected $table;
26
27    /**
28     * @var string[]
29     */
30    protected $primaryKey;
31
32    /**
33     * @param DbFactory $dbFactory
34     * @param string $table
35     * @param string[] $primaryKey
36     * @throws DataModelException
37     */
38    public function __construct( DbFactory $dbFactory, $table, array $primaryKey ) {
39        if ( !$primaryKey ) {
40            throw new DataModelException( 'PK required', 'process-data' );
41        }
42        parent::__construct( $dbFactory );
43        $this->table = $table;
44        $this->primaryKey = $primaryKey;
45    }
46
47    /**
48     * Inserts a set of rows into the database
49     *
50     * @param array $rows The rows to insert. Also accepts a single row.
51     * @return array|false An array of the rows that now exist
52     * in the database. Integrity of keys is guaranteed.
53     */
54    public function insert( array $rows ) {
55        // Only allow the row to include key/value pairs.
56        // No raw SQL.
57        if ( is_array( reset( $rows ) ) ) {
58            $insertRows = $this->preprocessNestedSqlArray( $rows );
59        } else {
60            $insertRows = [ $this->preprocessSqlArray( $rows ) ];
61        }
62
63        // insert returns boolean true/false
64        $this->dbFactory->getDB( DB_PRIMARY )->newInsertQueryBuilder()
65            ->insertInto( $this->table )
66            ->rows( $insertRows )
67            ->caller( __METHOD__ . " ({$this->table})" )
68            ->execute();
69
70        return $rows;
71    }
72
73    /**
74     * Update a single row in the database.
75     *
76     * @param array $old The current state of the row.
77     * @param array $new The desired new state of the row.
78     * @return bool Whether or not the operation was successful.
79     * @throws DataPersistenceException
80     */
81    public function update( array $old, array $new ) {
82        $pk = ObjectManager::splitFromRow( $old, $this->primaryKey );
83        if ( $pk === null ) {
84            $missing = array_diff( $this->primaryKey, array_keys( $old ) );
85            throw new DataPersistenceException( 'Row has null primary key: ' . implode( ', ', $missing ), 'process-data' );
86        }
87        $updates = $this->calcUpdates( $old, $new );
88        if ( !$updates ) {
89            return true; // nothing to change, success
90        }
91
92        // Only allow the row to include key/value pairs.
93        // No raw SQL.
94        $updates = $this->preprocessSqlArray( $updates );
95        $pk = $this->preprocessSqlArray( $pk );
96
97        $dbw = $this->dbFactory->getDB( DB_PRIMARY );
98        // update returns boolean true/false as $res
99        $dbw->newUpdateQueryBuilder()
100            ->update( $this->table )
101            ->set( $updates )
102            ->where( $pk )
103            ->caller( __METHOD__ . " ({$this->table})" )
104            ->execute();
105        // we also want to check that $pk actually selected a row to update
106        return $dbw->affectedRows() ? true : false;
107    }
108
109    /**
110     * @param array $row
111     * @return bool success
112     * @throws DataPersistenceException
113     */
114    public function remove( array $row ) {
115        $pk = ObjectManager::splitFromRow( $row, $this->primaryKey );
116        if ( $pk === null ) {
117            $missing = array_diff( $this->primaryKey, array_keys( $row ) );
118            throw new DataPersistenceException( 'Row has null primary key: ' . implode( ', ', $missing ), 'process-data' );
119        }
120
121        $pk = $this->preprocessSqlArray( $pk );
122
123        $dbw = $this->dbFactory->getDB( DB_PRIMARY );
124        $dbw->newDeleteQueryBuilder()
125            ->deleteFrom( $this->table )
126            ->where( $pk )
127            ->caller( __METHOD__ . " ({$this->table})" )
128            ->execute();
129        return (bool)$dbw->affectedRows();
130    }
131
132    /**
133     * @param array $attributes
134     * @param array $options
135     * @return array Empty array means no result.  Array with results is success.
136     * @throws DataModelException On query failure
137     */
138    public function find( array $attributes, array $options = [] ) {
139        $attributes = $this->preprocessSqlArray( $attributes );
140
141        if ( !$this->validateOptions( $options ) ) {
142            throw new InvalidArgumentException( "Validation error in database options" );
143        }
144
145        $dbr = $this->dbFactory->getDB( DB_REPLICA );
146        $res = $this->doFindQuery( $attributes, $options );
147        if ( $res === false ) {
148            throw new DataModelException( __METHOD__ . ': Query failed: ' . $dbr->lastError(), 'process-data' );
149        }
150
151        $result = [];
152        foreach ( $res as $row ) {
153            $result[] = UUID::convertUUIDs( (array)$row, 'alphadecimal' );
154        }
155        return $result;
156    }
157
158    protected function doFindQuery( array $preprocessedAttributes, array $options = [] ) {
159        return $this->dbFactory->getDB( DB_REPLICA )->select(
160            $this->table,
161            '*',
162            $preprocessedAttributes,
163            __METHOD__ . " ({$this->table})",
164            $options
165        );
166    }
167
168    protected function fallbackFindMulti( array $queries, array $options ) {
169        $result = [];
170        foreach ( $queries as $key => $query ) {
171            $result[$key] = $this->find( $query, $options );
172        }
173        return $result;
174    }
175
176    /**
177     * @param array $queries
178     * @param array $options
179     * @return array
180     * @throws DataModelException
181     */
182    public function findMulti( array $queries, array $options = [] ) {
183        $keys = array_keys( reset( $queries ) );
184        $pks = $this->getPrimaryKeyColumns();
185        if ( count( $keys ) !== count( $pks ) || array_diff( $keys, $pks ) ) {
186            return $this->fallbackFindMulti( $queries, $options );
187        }
188        $conds = [];
189        $dbr = $this->dbFactory->getDB( DB_REPLICA );
190        foreach ( $queries as $query ) {
191            $conds[] = $dbr->makeList( $this->preprocessSqlArray( $query ), LIST_AND );
192        }
193        unset( $query );
194
195        $conds = $dbr->makeList( $conds, LIST_OR );
196
197        // options can be ignored for primary key search
198        $res = $this->find( [ new RawSql( $conds ) ] );
199
200        // create temp array with pk value (usually uuid) as key and full db row
201        // as value
202        $temp = new MultiDimArray();
203        foreach ( $res as $val ) {
204            $val = UUID::convertUUIDs( $val, 'alphadecimal' );
205            $temp[ObjectManager::splitFromRow( $val, $this->primaryKey )] = $val;
206        }
207
208        // build return value by mapping the database rows to the matching array
209        // index in $queries
210        $result = [];
211        foreach ( $queries as $i => $val ) {
212            $val = UUID::convertUUIDs( $val, 'alphadecimal' );
213            $pk = ObjectManager::splitFromRow( $val, $this->primaryKey );
214            if ( isset( $temp[$pk] ) ) {
215                $result[$i][] = $temp[$pk];
216            }
217        }
218
219        return $result;
220    }
221
222    public function getPrimaryKeyColumns() {
223        return $this->primaryKey;
224    }
225}