PluginProbe
seQura / 3.0.6
seQura v3.0.6
4.3.4 4.3.3 4.3.2 4.3.1 trunk 2.0.0 2.0.10 2.0.11 2.0.12 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 3.0.0 3.0.2 3.0.5 3.0.6 3.0.7 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 4.0.0 All 30 releases
sequra / src / Repositories / class-repository.php

class-repository.php in seQura 3.0.6, at src/Repositories/class-repository.php

466 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shared repository functionality.
4 *
5 * @package SeQura/WC
6 * @subpackage SeQura/WC/Repositories
7 */
8
9 namespace SeQura\WC\Repositories;
10
11 use SeQura\Core\Infrastructure\ORM\Entity;
12 use SeQura\Core\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException;
13 use SeQura\Core\Infrastructure\ORM\Interfaces\RepositoryInterface;
14 use SeQura\Core\Infrastructure\ORM\QueryFilter\QueryCondition;
15 use SeQura\Core\Infrastructure\ORM\QueryFilter\QueryFilter;
16 use SeQura\Core\Infrastructure\ORM\Utility\IndexHelper;
17 use SeQura\Core\Infrastructure\ServiceRegister;
18 use wpdb;
19
20 /**
21 * Shared repository functionality.
22 */
23 abstract class Repository implements RepositoryInterface, Interface_Deletable_Repository {
24
25 /**
26 * Entity class FQN.
27 *
28 * @var string
29 */
30 protected $entity_class;
31
32 /**
33 * Database session object.
34 *
35 * @var \wpdb
36 */
37 protected $db;
38
39 /**
40 * Returns unprefixed table name.
41 */
42 abstract protected function get_unprefixed_table_name(): string;
43
44 /**
45 * Returns full table name.
46 */
47 protected function get_table_name(): string {
48 return $this->db->prefix . $this->get_unprefixed_table_name();
49 }
50
51 /**
52 * Constructor.
53 *
54 * @throws \RuntimeException If database service not found.
55 */
56 public function __construct() {
57 $db = ServiceRegister::getService( \wpdb::class );
58 if ( ! $db instanceof \wpdb ) {
59 throw new \RuntimeException( 'Database service not found.' );
60 }
61 $this->db = $db;
62 }
63
64 /**
65 * Returns full class name.
66 *
67 * @return string Full class name.
68 */
69 public static function getClassName() {
70 return __CLASS__;
71 }
72
73 /**
74 * Sets repository entity
75 *
76 * @noinspection PhpDocMissingThrowsInspection
77 *
78 * @param string $entity_class Entity class.
79 * @return void
80 */
81 public function setEntityClass( $entity_class ) {
82 $this->entity_class = $entity_class;
83 }
84
85 /**
86 * Executes select query.
87 *
88 * @param QueryFilter $filter Filter for query.
89 *
90 * @return Entity[] A list of found entities ot empty array.
91 * @throws QueryFilterInvalidParamException If filter condition is invalid.
92 */
93 public function select( QueryFilter $filter = null ) {
94 if ( ! $this->table_exists() ) {
95 return array();
96 }
97
98 /**
99 * Entity object.
100 *
101 * @var Entity $entity
102 */
103 $entity = new $this->entity_class();
104 $type = $entity->getConfig()->getType();
105
106 $query = "SELECT * FROM {$this->get_table_name()} WHERE type = '$type' ";
107 if ( $filter ) {
108 $query .= $this->apply_query_filter( $filter, IndexHelper::mapFieldsToIndexes( $entity ) );
109 }
110
111 $raw_results = $this->db->get_results( $query, ARRAY_A );
112 if ( ! is_array( $raw_results ) ) {
113 return array();
114 }
115
116 return $this->translateToEntities( $raw_results );
117 }
118
119 /**
120 * Executes select query and returns first result.
121 *
122 * @param QueryFilter $filter Filter for query.
123 *
124 * @return Entity|null First found entity or NULL.
125 * @throws QueryFilterInvalidParamException If filter condition is invalid.
126 */
127 public function selectOne( QueryFilter $filter = null ) {
128 if ( ! $filter ) {
129 $filter = new QueryFilter();
130 }
131
132 $filter->setLimit( 1 );
133 $results = $this->select( $filter );
134
135 return ! empty( $results ) ? $results[0] : null;
136 }
137
138 /**
139 * Executes insert query and returns ID of created entity. Entity will be updated with new ID.
140 *
141 * @param Entity $entity Entity to be saved.
142 *
143 * @return int Identifier of saved entity.
144 */
145 public function save( Entity $entity ) {
146 if ( ! $this->table_exists() ) {
147 return -1;
148 }
149
150 if ( $entity->getId() ) {
151 $this->update( $entity );
152
153 return $entity->getId();
154 }
155
156 return $this->save_entity_to_storage( $entity );
157 }
158
159 /**
160 * Executes update query and returns success flag.
161 *
162 * @param Entity $entity Entity to be updated.
163 *
164 * @return bool TRUE if operation succeeded; otherwise, FALSE.
165 */
166 public function update( Entity $entity ) {
167 if ( ! $this->table_exists() ) {
168 return false;
169 }
170
171 $item = $this->prepare_entity_for_storage( $entity );
172
173 // Only one record should be updated.
174 return 1 === $this->db->update( $this->get_table_name(), $item, array( 'id' => $entity->getId() ) );
175 }
176
177 /**
178 * Executes delete query and returns success flag.
179 *
180 * @param Entity $entity Entity to be deleted.
181 *
182 * @return bool TRUE if operation succeeded; otherwise, FALSE.
183 */
184 public function delete( Entity $entity ) {
185 if ( ! $this->table_exists() ) {
186 return false;
187 }
188 return false !== $this->db->delete( $this->get_table_name(), array( 'id' => $entity->getId() ) );
189 }
190
191 /**
192 * Counts records that match filter criteria.
193 *
194 * @param QueryFilter $filter Filter for query.
195 *
196 * @return int Number of records that match filter criteria.
197 * @throws QueryFilterInvalidParamException If filter condition is invalid.
198 */
199 public function count( QueryFilter $filter = null ) {
200 if ( ! $this->table_exists() ) {
201 return 0;
202 }
203 /**
204 * Entity object.
205 *
206 * @var Entity $entity
207 */
208 $entity = new $this->entity_class();
209 $type = $entity->getConfig()->getType();
210
211 $query = "SELECT COUNT(*) as `total` FROM {$this->get_table_name()} WHERE type = '$type' ";
212 if ( $filter ) {
213 $query .= $this->apply_query_filter( $filter, IndexHelper::mapFieldsToIndexes( $entity ) );
214 }
215
216 $result = $this->db->get_results( $query, ARRAY_A );
217
218 return empty( $result[0]['total'] ) ? 0 : intval( $result[0]['total'] );
219 }
220
221 /**
222 * Escapes provided value.
223 *
224 * @param mixed $value Value to be escaped.
225 *
226 * @return string Escaped value.
227 */
228 protected function escape( $value ) {
229 return addslashes( strval( $value ) );
230 }
231
232 /**
233 * Checks if value exists and escapes it if it's not.
234 *
235 * @param mixed $value Value to be escaped.
236 *
237 * @return string Escaped value.
238 */
239 protected function escape_value( $value ) {
240 return null === $value ? 'NULL' : "'" . $this->escape( $value ) . "'";
241 }
242
243 /**
244 * Builds WHERE part of select query.
245 *
246 * @param mixed[]$filter_by Filter conditions in query.
247 *
248 * @return string Where condition.
249 */
250 protected function build_condition( $filter_by ) {
251 if ( empty( $filter_by ) ) {
252 return '';
253 }
254
255 $where = array();
256 foreach ( $filter_by as $key => $value ) {
257 if ( null === $value ) {
258 $where[] = "`$key` IS NULL";
259 } else {
260 $where[] = "`$key` = '" . $this->escape( $value ) . "'";
261 }
262 }
263
264 return ' WHERE ' . implode( ' AND ', $where );
265 }
266
267 /**
268 * Converts filter value to index string representation.
269 *
270 * @param QueryCondition $condition Query condition.
271 *
272 * @return string|null Converted value.
273 */
274 protected function convert_value( QueryCondition $condition ) {
275 $value = IndexHelper::castFieldValue( $condition->getValue(), $condition->getValueType() );
276 switch ( $condition->getValueType() ) {
277 case 'string':
278 $value = $this->escape_value( $condition->getValue() );
279 break;
280 case 'array':
281 /**
282 * Values
283 *
284 * @var mixed[] $values
285 */
286 $values = $condition->getValue();
287 $escaped_values = array();
288 foreach ( $values as $value ) {
289 $escaped_values[] = is_string( $value ) ? $this->escape_value( $value ) : $value;
290 }
291
292 $value = '(' . implode( ', ', $escaped_values ) . ')';
293 break;
294 default:
295 // 'integer', 'dateTime','boolean','double'
296 $value = $this->escape_value( $value );
297 break;
298 }
299
300 return $value;
301 }
302
303 /**
304 * Builds query filter part of the query.
305 *
306 * @param QueryFilter $filter Query filter object.
307 * @param mixed[] $field_index_map Property to index number map.
308 *
309 * @return string Query filter addendum.
310 * @throws QueryFilterInvalidParamException If filter condition is invalid.
311 */
312 protected function apply_query_filter( QueryFilter $filter, array $field_index_map = array() ) {
313 $query = '';
314 $conditions = $filter->getConditions();
315 if ( ! empty( $conditions ) ) {
316 $query .= ' AND (';
317 $first = true;
318 foreach ( $conditions as $condition ) {
319 $this->validate_index_column( $condition->getColumn(), $field_index_map );
320 $chain_op = $first ? '' : $condition->getChainOperator();
321 $first = false;
322 $column = 'id' === $condition->getColumn() ? 'id' : 'index_' . $field_index_map[ $condition->getColumn() ];
323 $operator = $condition->getOperator();
324 $query .= " $chain_op $column $operator " . $this->convert_value( $condition );
325 }
326
327 $query .= ')';
328 }
329
330 if ( $filter->getOrderByColumn() ) {
331 $this->validate_index_column( $filter->getOrderByColumn(), $field_index_map );
332 $order_index = 'id' === $filter->getOrderByColumn() ? 'id' : 'index_' . $field_index_map[ $filter->getOrderByColumn() ];
333 $query .= " ORDER BY {$order_index} {$filter->getOrderDirection()}";
334 }
335
336 if ( $filter->getLimit() ) {
337 $offset = (int) $filter->getOffset();
338 $query .= " LIMIT {$offset}, {$filter->getLimit()}";
339 }
340
341 return $query;
342 }
343
344 /**
345 * Transforms raw database query rows to entities.
346 *
347 * @param mixed[]$result Raw database query result.
348 *
349 * @return Entity[] Array of transformed entities.
350 */
351 protected function translateToEntities( array $result ) {
352 /**
353 * Array of decoded entities.
354 *
355 * @var Entity[] $entities
356 */
357 $entities = array();
358 foreach ( $result as $item ) {
359 /**
360 * Raw data.
361 *
362 * @var mixed[] $item
363 */
364 if ( ! isset( $item['data'] ) || ! isset( $item['id'] ) ) {
365 continue;
366 }
367 $data = (array) json_decode( strval( $item['data'] ), true );
368 /**
369 * Entity object.
370 *
371 * @var Entity $entity
372 */
373 $entity = isset( $data['class_name'] ) ? new $data['class_name']() : new $this->entity_class();
374 $entity->inflate( $data );
375 $entity->setId( $item['id'] );
376
377 $entities[] = $entity;
378 }
379
380 return $entities;
381 }
382
383 /**
384 * Saves entity to system storage.
385 *
386 * @param Entity $entity Entity to be stored.
387 *
388 * @return int Inserted entity identifier.
389 */
390 protected function save_entity_to_storage( Entity $entity ) {
391 if ( ! $this->table_exists() ) {
392 return -1;
393 }
394 $storage_item = $this->prepare_entity_for_storage( $entity );
395
396 $this->db->insert( $this->get_table_name(), $storage_item );
397
398 $insert_id = (int) $this->db->insert_id;
399 $entity->setId( $insert_id );
400
401 return $insert_id;
402 }
403
404 /**
405 * Prepares entity in format for storage.
406 *
407 * @param Entity $entity Entity to be stored.
408 *
409 * @return mixed[] Item prepared for storage.
410 */
411 protected function prepare_entity_for_storage( Entity $entity ) {
412 $indexes = IndexHelper::transformFieldsToIndexes( $entity );
413 $storage_item = array(
414 'type' => $entity->getConfig()->getType(),
415 'index_1' => null,
416 'index_2' => null,
417 'index_3' => null,
418 'index_4' => null,
419 'index_5' => null,
420 'index_6' => null,
421 'index_7' => null,
422 'data' => \wp_json_encode( $entity->toArray() ),
423 );
424
425 foreach ( $indexes as $index => $value ) {
426 $storage_item[ 'index_' . $index ] = $value;
427 }
428
429 return $storage_item;
430 }
431
432 /**
433 * Validates if column can be filtered or sorted by.
434 *
435 * @param string $column Column name.
436 * @param mixed[] $index_map Index map.
437 *
438 * @throws QueryFilterInvalidParamException If filter condition is invalid.
439 *
440 * @return void
441 */
442 protected function validate_index_column( $column, array $index_map ) {
443 if ( 'id' !== $column && ! array_key_exists( $column, $index_map ) ) {
444 throw new QueryFilterInvalidParamException( \esc_html__( 'Column is not id or index.', 'sequra' ) );
445 }
446 }
447
448 /**
449 * Delete all the entities.
450 */
451 public function delete_all(): bool {
452 if ( ! $this->table_exists() ) {
453 return false;
454 }
455 return false !== $this->db->query( 'DELETE FROM ' . \sanitize_text_field( $this->get_table_name() ) );
456 }
457
458 /**
459 * Check if table exists in the database.
460 */
461 protected function table_exists(): bool {
462 $table_name = \sanitize_text_field( $this->get_table_name() );
463 return $this->db->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name;
464 }
465 }
466