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