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
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 / 10
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 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        $queryBuilder = $this->dbFactory->getDB( DB_PRIMARY )->newInsertQueryBuilder()
62            ->insertInto( $this->table )
63            ->rows( $insertRows )
64            ->caller( __METHOD__ . " ({$this->table})" );
65        DbStorage::maybeSetInsertIgnore( $queryBuilder );
66        $queryBuilder->execute();
67
68        return $rows;
69    }
70
71    /**
72     * Update a single row in the database.
73     *
74     * @param array $old The current state of the row.
75     * @param array $new The desired new state of the row.
76     * @return bool Whether or not the operation was successful.
77     * @throws DataPersistenceException
78     */
79    public function update( array $old, array $new ) {
80        $pk = ObjectManager::splitFromRow( $old, $this->primaryKey );
81        if ( $pk === null ) {
82            $missing = array_diff( $this->primaryKey, array_keys( $old ) );
83            throw new DataPersistenceException( 'Row has null primary key: ' . implode( ', ', $missing ), 'process-data' );
84        }
85        $updates = $this->calcUpdates( $old, $new );
86        if ( !$updates ) {
87            return true; // nothing to change, success
88        }
89
90        // Only allow the row to include key/value pairs.
91        // No raw SQL.
92        $updates = $this->preprocessSqlArray( $updates );
93        $pk = $this->preprocessSqlArray( $pk );
94
95        $dbw = $this->dbFactory->getDB( DB_PRIMARY );
96        // update returns boolean true/false as $res
97        $dbw->newUpdateQueryBuilder()
98            ->update( $this->table )
99            ->set( $updates )
100            ->where( $pk )
101            ->caller( __METHOD__ . " ({$this->table})" )
102            ->execute();
103        // we also want to check that $pk actually selected a row to update
104        return (bool)$dbw->affectedRows();
105    }
106
107    /**
108     * @param array $row
109     * @return bool success
110     * @throws DataPersistenceException
111     */
112    public function remove( array $row ) {
113        $pk = ObjectManager::splitFromRow( $row, $this->primaryKey );
114        if ( $pk === null ) {
115            $missing = array_diff( $this->primaryKey, array_keys( $row ) );
116            throw new DataPersistenceException( 'Row has null primary key: ' . implode( ', ', $missing ), 'process-data' );
117        }
118
119        $pk = $this->preprocessSqlArray( $pk );
120
121        $dbw = $this->dbFactory->getDB( DB_PRIMARY );
122        $dbw->newDeleteQueryBuilder()
123            ->deleteFrom( $this->table )
124            ->where( $pk )
125            ->caller( __METHOD__ . " ({$this->table})" )
126            ->execute();
127        return (bool)$dbw->affectedRows();
128    }
129
130    /**
131     * @param array $attributes
132     * @param array $options
133     * @return array Empty array means no result.  Array with results is success.
134     * @throws DataModelException On query failure
135     */
136    public function find( array $attributes, array $options = [] ) {
137        $attributes = $this->preprocessSqlArray( $attributes );
138
139        if ( !$this->validateOptions( $options ) ) {
140            throw new InvalidArgumentException( "Validation error in database options" );
141        }
142
143        $dbr = $this->dbFactory->getDB( DB_REPLICA );
144        $res = $this->doFindQuery( $attributes, $options );
145        if ( $res === false ) {
146            throw new DataModelException( __METHOD__ . ': Query failed: ' . $dbr->lastError(), 'process-data' );
147        }
148
149        $result = [];
150        foreach ( $res as $row ) {
151            $result[] = UUID::convertUUIDs( (array)$row, 'alphadecimal' );
152        }
153        return $result;
154    }
155
156    protected function doFindQuery( array $preprocessedAttributes, array $options = [] ) {
157        return $this->dbFactory->getDB( DB_REPLICA )->newSelectQueryBuilder()
158            ->select( '*' )
159            ->from( $this->table )
160            ->where( $preprocessedAttributes )
161            ->caller( __METHOD__ . " ({$this->table})" )
162            ->options( $options )
163            ->fetchResultSet();
164    }
165
166    protected function fallbackFindMulti( array $queries, array $options ) {
167        $result = [];
168        foreach ( $queries as $key => $query ) {
169            $result[$key] = $this->find( $query, $options );
170        }
171        return $result;
172    }
173
174    /**
175     * @param array $queries
176     * @param array $options
177     * @return array
178     * @throws DataModelException
179     */
180    public function findMulti( array $queries, array $options = [] ) {
181        $keys = array_keys( reset( $queries ) );
182        $pks = $this->getPrimaryKeyColumns();
183        if ( count( $keys ) !== count( $pks ) || array_diff( $keys, $pks ) ) {
184            return $this->fallbackFindMulti( $queries, $options );
185        }
186        $conds = [];
187        $dbr = $this->dbFactory->getDB( DB_REPLICA );
188        foreach ( $queries as $query ) {
189            $conds[] = $dbr->andExpr( $this->preprocessSqlArray( $query ) );
190        }
191        unset( $query );
192
193        // options can be ignored for primary key search
194        $res = $this->find( [ $dbr->orExpr( $conds ) ] );
195
196        // create temp array with pk value (usually uuid) as key and full db row
197        // as value
198        $temp = new MultiDimArray();
199        foreach ( $res as $val ) {
200            $val = UUID::convertUUIDs( $val, 'alphadecimal' );
201            $temp[ObjectManager::splitFromRow( $val, $this->primaryKey )] = $val;
202        }
203
204        // build return value by mapping the database rows to the matching array
205        // index in $queries
206        $result = [];
207        foreach ( $queries as $i => $val ) {
208            $val = UUID::convertUUIDs( $val, 'alphadecimal' );
209            $pk = ObjectManager::splitFromRow( $val, $this->primaryKey );
210            if ( isset( $temp[$pk] ) ) {
211                $result[$i][] = $temp[$pk];
212            }
213        }
214
215        return $result;
216    }
217
218    public function getPrimaryKeyColumns() {
219        return $this->primaryKey;
220    }
221}