PluginProbe
seQura / 4.1.0
seQura v4.1.0
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
← All changes | src/Repositories/class-repository.php +328 -40 3.0.24.1.0 View file →
@@ -7,8 +7,9 @@
7 7 */
8 8
9 9 namespace SeQura\WC\Repositories;
10 10
11 +use Exception;
11 12 use SeQura\Core\Infrastructure\ORM\Entity;
12 13 use SeQura\Core\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException;
13 14 use SeQura\Core\Infrastructure\ORM\Interfaces\RepositoryInterface;
14 15 use SeQura\Core\Infrastructure\ORM\QueryFilter\QueryCondition;
@@ -14,14 +15,16 @@
14 15 use SeQura\Core\Infrastructure\ORM\QueryFilter\QueryCondition;
15 16 use SeQura\Core\Infrastructure\ORM\QueryFilter\QueryFilter;
16 17 use SeQura\Core\Infrastructure\ORM\Utility\IndexHelper;
17 18 use SeQura\Core\Infrastructure\ServiceRegister;
19 +use SeQura\WC\Dto\Table_Index;
20 +use SeQura\WC\Dto\Table_Index_Column;
18 21 use wpdb;
19 22
20 23 /**
21 24 * Shared repository functionality.
22 25 */
23 -abstract class Repository implements RepositoryInterface, Interface_Deletable_Repository {
26 +abstract class Repository implements RepositoryInterface, Interface_Deletable_Repository, Interface_Table_Migration_Repository {
24 27
25 28 /**
26 29 * Entity class FQN.
27 30 *
@@ -43,13 +46,22 @@
43 46
44 47 /**
45 48 * Returns full table name.
46 49 */
47 - protected function get_table_name(): string {
50 + public function get_table_name(): string {
48 51 return $this->db->prefix . $this->get_unprefixed_table_name();
49 52 }
50 53
51 54 /**
55 + * Get the name that is set to the original table during the migration.
56 + *
57 + * @return string The name of the old table.
58 + */
59 + public function get_legacy_table_name() {
60 + return $this->get_table_name() . '_legacy';
61 + }
62 +
63 + /**
52 64 * Constructor.
53 65 *
54 66 * @throws \RuntimeException If database service not found.
55 67 */
@@ -77,9 +89,9 @@
77 89 *
78 90 * @param string $entity_class Entity class.
79 91 * @return void
80 92 */
81 - public function setEntityClass( $entity_class ) {
93 + public function setEntityClass( $entity_class ): void {
82 94 $this->entity_class = $entity_class;
83 95 }
84 96
85 97 /**
@@ -84,18 +96,14 @@
84 96
85 97 /**
86 98 * Executes select query.
87 99 *
88 - * @param QueryFilter $filter Filter for query.
100 + * @param QueryFilter|null $filter Filter for query.
89 101 *
90 102 * @return Entity[] A list of found entities ot empty array.
91 103 * @throws QueryFilterInvalidParamException If filter condition is invalid.
92 104 */
93 - public function select( QueryFilter $filter = null ) {
94 - if ( ! $this->table_exists() ) {
95 - return array();
96 - }
97 -
105 + public function select( ?QueryFilter $filter = null ) {
98 106 /**
99 107 * Entity object.
100 108 *
101 109 * @var Entity $entity
@@ -101,18 +109,30 @@
101 109 * @var Entity $entity
102 110 */
103 111 $entity = new $this->entity_class();
104 112 $type = $entity->getConfig()->getType();
105 -
113 +
106 114 $query = "SELECT * FROM {$this->get_table_name()} WHERE type = '$type' ";
107 115 if ( $filter ) {
108 116 $query .= $this->apply_query_filter( $filter, IndexHelper::mapFieldsToIndexes( $entity ) );
109 117 }
110 -
111 - $raw_results = $this->db->get_results( $query, ARRAY_A );
112 - if ( ! is_array( $raw_results ) ) {
113 - return array();
118 +
119 + $raw_results = array();
120 + if ( $this->table_exists() ) {
121 + $raw_results = $this->db->get_results( $query, ARRAY_A );
122 + if ( ! is_array( $raw_results ) ) {
123 + $raw_results = array();
124 + }
114 125 }
126 + if ( $this->table_exists( true ) ) {
127 + // If the legacy table exists the data may be there.
128 + $query = str_replace( $this->get_table_name(), $this->get_legacy_table_name(), $query );
129 + $legacy_raw_results = $this->db->get_results( $query, ARRAY_A );
130 + if ( ! is_array( $legacy_raw_results ) ) {
131 + $legacy_raw_results = array();
132 + }
133 + $raw_results = array_merge( $raw_results, $legacy_raw_results );
134 + }
115 135
116 136 return $this->translateToEntities( $raw_results );
117 137 }
118 138
@@ -118,14 +138,14 @@
118 138
119 139 /**
120 140 * Executes select query and returns first result.
121 141 *
122 - * @param QueryFilter $filter Filter for query.
142 + * @param QueryFilter|null $filter Filter for query.
123 143 *
124 144 * @return Entity|null First found entity or NULL.
125 145 * @throws QueryFilterInvalidParamException If filter condition is invalid.
126 146 */
127 - public function selectOne( QueryFilter $filter = null ) {
147 + public function selectOne( ?QueryFilter $filter = null ) {
128 148 if ( ! $filter ) {
129 149 $filter = new QueryFilter();
130 150 }
131 151
@@ -166,13 +186,36 @@
166 186 public function update( Entity $entity ) {
167 187 if ( ! $this->table_exists() ) {
168 188 return false;
169 189 }
170 -
171 - $item = $this->prepare_entity_for_storage( $entity );
172 -
190 + $item = $this->prepare_entity_for_storage( $entity );
191 + $where = array( 'id' => $entity->getId() );
192 +
193 + // Check if entity wasn't already migrated and migrate it including the new data.
194 + if ( $this->table_exists( true ) && $this->entity_exists( $entity->getId(), true ) ) {
195 + if ( 1 !== $this->db->update( $this->get_legacy_table_name(), $item, $where ) ) {
196 + return false;
197 + }
198 + // Read from the legacy table.
199 + $raw_results = $this->db->get_results( "SELECT * FROM {$this->get_legacy_table_name()} WHERE id = {$entity->getId()} LIMIT 1;", ARRAY_A );
200 + if ( empty( $raw_results ) ) {
201 + return false;
202 + }
203 + $entity = $this->translateToEntities( $raw_results )[0] ?? null;
204 + if ( ! $entity ) {
205 + return false;
206 + }
207 + // Insert into the new table.
208 + $item = $this->prepare_entity_for_storage( $entity );
209 + if ( false !== $this->db->insert( $this->get_table_name(), $item ) ) {
210 + return false;
211 + }
212 + // Delete the row from the legacy table.
213 + $this->db->delete( $this->get_legacy_table_name(), $where );
214 + return true;
215 + }
173 216 // Only one record should be updated.
174 - return 1 === $this->db->update( $this->get_table_name(), $item, array( 'id' => $entity->getId() ) );
217 + return 1 === $this->db->update( $this->get_table_name(), $item, $where );
175 218 }
176 219
177 220 /**
178 221 * Executes delete query and returns success flag.
@@ -181,26 +224,31 @@
181 224 *
182 225 * @return bool TRUE if operation succeeded; otherwise, FALSE.
183 226 */
184 227 public function delete( Entity $entity ) {
185 - if ( ! $this->table_exists() ) {
186 - return false;
228 + $where = array( 'id' => $entity->getId() );
229 + $deleted = false;
230 + if ( $this->table_exists() ) {
231 + $result = $this->db->delete( $this->get_table_name(), $where );
232 + $deleted = ! empty( $result );
187 233 }
188 - return false !== $this->db->delete( $this->get_table_name(), array( 'id' => $entity->getId() ) );
234 + if ( $this->table_exists( true ) ) {
235 + // Delete from legacy table.
236 + $result = $this->db->delete( $this->get_legacy_table_name(), $where );
237 + $deleted = $deleted || ! empty( $result );
238 + }
239 + return $deleted;
189 240 }
190 241
191 242 /**
192 243 * Counts records that match filter criteria.
193 244 *
194 - * @param QueryFilter $filter Filter for query.
245 + * @param QueryFilter|null $filter Filter for query.
195 246 *
196 247 * @return int Number of records that match filter criteria.
197 248 * @throws QueryFilterInvalidParamException If filter condition is invalid.
198 249 */
199 - public function count( QueryFilter $filter = null ) {
200 - if ( ! $this->table_exists() ) {
201 - return 0;
202 - }
250 + public function count( ?QueryFilter $filter = null ) {
203 251 /**
204 252 * Entity object.
205 253 *
206 254 * @var Entity $entity
@@ -211,12 +259,20 @@
211 259 $query = "SELECT COUNT(*) as `total` FROM {$this->get_table_name()} WHERE type = '$type' ";
212 260 if ( $filter ) {
213 261 $query .= $this->apply_query_filter( $filter, IndexHelper::mapFieldsToIndexes( $entity ) );
214 262 }
215 -
216 - $result = $this->db->get_results( $query, ARRAY_A );
217 -
218 - return empty( $result[0]['total'] ) ? 0 : intval( $result[0]['total'] );
263 + $count = 0;
264 + if ( $this->table_exists() ) {
265 + $result = $this->db->get_results( $query, ARRAY_A );
266 + $count += empty( $result[0]['total'] ) || ! is_numeric( $result[0]['total'] ) ? 0 : (int) $result[0]['total'];
267 + }
268 + if ( $this->table_exists( true ) ) {
269 + // If the legacy table exists, count the data there too.
270 + $query = str_replace( $this->get_table_name(), $this->get_legacy_table_name(), $query );
271 + $result = $this->db->get_results( $query, ARRAY_A );
272 + $count += empty( $result[0]['total'] ) || ! is_numeric( $result[0]['total'] ) ? 0 : (int) $result[0]['total'];
273 + }
274 + return $count;
219 275 }
220 276
221 277 /**
222 278 * Escapes provided value.
@@ -371,9 +427,11 @@
371 427 * @var Entity $entity
372 428 */
373 429 $entity = isset( $data['class_name'] ) ? new $data['class_name']() : new $this->entity_class();
374 430 $entity->inflate( $data );
375 - $entity->setId( $item['id'] );
431 + if ( is_numeric( $item['id'] ) ) {
432 + $entity->setId( (int) $item['id'] );
433 + }
376 434
377 435 $entities[] = $entity;
378 436 }
379 437
@@ -418,11 +476,15 @@
418 476 'index_4' => null,
419 477 'index_5' => null,
420 478 'index_6' => null,
421 479 'index_7' => null,
422 - 'data' => wp_json_encode( $entity->toArray() ),
480 + 'data' => \wp_json_encode( $entity->toArray() ),
423 481 );
424 482
483 + if ( $entity->getId() ) {
484 + $storage_item['id'] = $entity->getId();
485 + }
486 +
425 487 foreach ( $indexes as $index => $value ) {
426 488 $storage_item[ 'index_' . $index ] = $value;
427 489 }
428 490
@@ -446,20 +508,246 @@
446 508 }
447 509
448 510 /**
449 511 * Delete all the entities.
512 + *
513 + * @param string|null $store_id Delete entities from this store. Passing null will delete all entities.
450 514 */
451 - public function delete_all(): bool {
515 + public function delete_all( $store_id = null ): bool {
516 + $deleted = false;
517 + $sql = 'DELETE FROM ' . \sanitize_text_field( $this->get_table_name() );
518 + if ( $store_id ) {
519 + $column = $this->get_store_id_index_column();
520 + if ( ! $column ) {
521 + return false;
522 + }
523 + $sql .= ' WHERE ' . \sanitize_text_field( $column ) . ' = ' . \sanitize_text_field( $store_id );
524 + }
525 + if ( $this->table_exists() ) {
526 + $result = $this->db->query( $sql );
527 + $deleted = ! empty( $result );
528 + }
529 + if ( $this->table_exists( true ) ) {
530 + $result = $this->db->query( str_replace( $this->get_table_name(), $this->get_legacy_table_name(), $sql ) );
531 + $deleted = $deleted || ! empty( $result );
532 + }
533 + return $deleted;
534 + }
535 +
536 + /**
537 + * Get the index column name that stores the store ID.
538 + *
539 + * @return string Index column name or empty string if not applicable.
540 + */
541 + protected function get_store_id_index_column(): string {
542 + return 'index_1';
543 + }
544 +
545 + /**
546 + * Check if table exists in the database.
547 + *
548 + * @param boolean $legacy If true, check for legacy table.
549 + */
550 + public function table_exists( $legacy = false ): bool {
551 + $table_name = \sanitize_text_field( ! $legacy ? $this->get_table_name() : $this->get_legacy_table_name() );
552 + return $this->db->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name;
553 + }
554 +
555 + /**
556 + * Remove entities that are older than a certain date or that are invalid.
557 + * This performs a cleanup of the repository data.
558 + */
559 + public function delete_old_and_invalid() {
560 + // Do nothing by default. Implement in child class if needed.
561 + }
562 +
563 + /**
564 + * Check if the index exists.
565 + *
566 + * @param Table_Index $index The index to check.
567 + * @return bool True if the index exists, false otherwise.
568 + */
569 + public function index_exists( $index ) {
570 + $index_name = \sanitize_key( $index->name );
571 + return ! empty( $this->db->get_col( "SHOW INDEX FROM `{$this->get_table_name()}` WHERE Key_name = '{$index_name}'" ) );
572 + }
573 +
574 + /**
575 + * Add an index to the table.
576 + *
577 + * @param Table_Index $index The index.
578 + * @return bool True if the index was added or already exists, false otherwise.
579 + */
580 + public function add_index( $index ) {
581 + if ( $this->index_exists( $index ) ) {
582 + return true;
583 + }
584 + $index_name = \sanitize_key( $index->name );
585 + $columns = array();
586 + foreach ( $index->columns as $column ) {
587 + $columns[] = '`' . \sanitize_key( $column->name ) . '`' . ( null !== $column->char_limit ? "({$column->char_limit})" : '' );
588 + }
589 + $columns = implode( ',', $columns );
590 + return false !== $this->db->query( "ALTER TABLE `{$this->get_table_name()}` ADD INDEX `{$index_name}` ({$columns})" );
591 + }
592 +
593 + /**
594 + * Execute the migration process one by one.
595 + * This implementation is intended for migrations that don't change the table structure.
596 + */
597 + public function migrate_next_row() {
598 + if ( ! $this->table_exists() || ! $this->table_exists( true ) ) {
599 + return;
600 + }
601 + $raw_results = $this->db->get_results( "SELECT * FROM {$this->get_legacy_table_name()} LIMIT 1;", ARRAY_A );
602 + if ( ! is_array( $raw_results ) ) {
603 + return;
604 + }
605 +
606 + $entity = $this->translateToEntities( $raw_results )[0] ?? null;
607 + if ( ! $entity ) {
608 + return;
609 + }
610 + // Check if entity already exists in the table.
611 + if ( $this->entity_exists( $entity->getId() ) ) {
612 + return;
613 + }
614 +
615 + $storage_item = $this->prepare_entity_for_storage( $entity );
616 + $result = $this->db->insert( $this->get_table_name(), $storage_item );
617 + if ( false !== $result ) {
618 + // Delete the row from the legacy table.
619 + $this->db->delete( $this->get_legacy_table_name(), array( 'id' => $entity->getId() ) );
620 + }
621 + }
622 +
623 + /**
624 + * Check if the migration process is complete.
625 + *
626 + * @return bool True if the migration process is complete, false otherwise.
627 + */
628 + public function is_migration_complete() {
629 + // Check if the legacy table exists.
630 + if ( $this->table_exists( true ) ) {
631 + return false;
632 + }
633 + // Check if the indexes exist.
634 + $indexes = $this->get_required_indexes();
635 + foreach ( $indexes as $index ) {
636 + if ( ! $this->index_exists( $index ) ) {
637 + return false;
638 + }
639 + }
640 +
641 + return true;
642 + }
643 +
644 + /**
645 + * Get the SQL statement to create the table without the indexes definition.
646 + * Resulting string should include an additional %s placeholder for the indexes.
647 + *
648 + * @return string The SQL statement to create the table.
649 + */
650 + protected function get_create_table_sql() {
651 + $charset_collate = $this->db->get_charset_collate();
652 + return "CREATE TABLE {$this->get_table_name()} (
653 + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
654 + `type` VARCHAR(255),
655 + `index_1` VARCHAR(127),
656 + `index_2` VARCHAR(127),
657 + `index_3` VARCHAR(127),
658 + `index_4` VARCHAR(127),
659 + `index_5` VARCHAR(127),
660 + `index_6` VARCHAR(127),
661 + `index_7` VARCHAR(127),
662 + `data` LONGTEXT,
663 + PRIMARY KEY (id) %s) $charset_collate;";
664 + }
665 +
666 + /**
667 + * Create the table if it doesn't exist.
668 + *
669 + * @throws Exception If the table creation fails.
670 + */
671 + public function create_table() {
672 + $indexes = array();
673 + foreach ( $this->get_required_indexes() as $index ) {
674 + $indexes[] = $index->to_sql();
675 + }
676 + $indexes = implode( ', ', $indexes );
677 + if ( ! empty( $indexes ) ) {
678 + $indexes = ', ' . $indexes;
679 + }
680 +
681 + $sql = sprintf( $this->get_create_table_sql(), $indexes );
682 + require_once ABSPATH . 'wp-admin/includes/upgrade.php';
683 + $result = \dbDelta( $sql );
452 684 if ( ! $this->table_exists() ) {
685 + throw new Exception( \esc_html( "SQL: $sql\nResult: " . implode( '. ', $result ) ) );
686 + }
687 + }
688 +
689 + /**
690 + * Make sure that the required tables for the migration are created.
691 + *
692 + * @throws Exception If cannot prepare tables for migration.
693 + */
694 + public function prepare_tables_for_migration() {
695 + // Rename the table to legacy table if it doesn't exist.
696 + if ( ! $this->table_exists( true ) && false === $this->db->query( "RENAME TABLE {$this->get_table_name()} TO {$this->get_legacy_table_name()};" ) ) {
697 + throw new Exception( \esc_html( "Could not rename table {$this->get_table_name()} to {$this->get_legacy_table_name()}" ) );
698 + }
699 +
700 + if ( ! $this->table_exists() ) {
701 + // Create the table if not exists.
702 + $this->create_table();
703 +
704 + // Add the auto-increment next value to the new table.
705 + $raw_id = $this->db->get_var( "SELECT MAX(id) FROM {$this->get_legacy_table_name()};" );
706 + $auto_increment = null !== $raw_id && is_numeric( $raw_id ) ? (int) $raw_id + 1 : 1;
707 + if ( false === $this->db->query( "ALTER TABLE {$this->get_table_name()} AUTO_INCREMENT = {$auto_increment};" ) ) {
708 + throw new Exception( \esc_html( "Could not set auto-increment value for table {$this->get_table_name()} to {$auto_increment}" ) );
709 + }
710 + }
711 + }
712 +
713 + /**
714 + * Evaluates if the legacy table should be removed and if so, removes it.
715 + *
716 + * @return bool True if the legacy table was removed or did not exist, false otherwise.
717 + */
718 + public function maybe_remove_legacy_table() {
719 + if ( ! $this->table_exists( true ) ) {
720 + return true;
721 + }
722 + $raw_results = $this->db->get_results( "SELECT 1 FROM `{$this->get_legacy_table_name()}` LIMIT 1;", ARRAY_A );
723 + if ( ! empty( $raw_results ) ) {
724 + // Legacy table is not empty, do not remove it.
453 725 return false;
454 726 }
455 - return false !== $this->db->query( 'DELETE FROM ' . sanitize_text_field( $this->get_table_name() ) );
727 + return false !== $this->db->query( "DROP TABLE IF EXISTS `{$this->get_legacy_table_name()}`;" );
456 728 }
457 729
458 730 /**
459 - * Check if table exists in the database.
731 + * Get a list of indexes that are required for the table.
732 + *
733 + * @return Table_Index[] The list of indexes.
460 734 */
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;
735 + public function get_required_indexes() {
736 + return array(
737 + new Table_Index( $this->get_table_name() . '_type', array( new Table_Index_Column( 'type', 64 ) ) ),
738 + );
739 + }
740 +
741 + /**
742 + * Check if entity exists in the database.
743 + *
744 + * @param int $id Entity ID.
745 + * @param bool $legacy If true, check for legacy table.
746 + * @return bool True if entity exists, false otherwise.
747 + */
748 + protected function entity_exists( $id, $legacy = false ): bool {
749 + $table_name = $legacy ? $this->get_legacy_table_name() : $this->get_table_name();
750 + $raw_results = $this->db->get_results( "SELECT 1 FROM `$table_name` WHERE id = {$id} LIMIT 1;", ARRAY_A );
751 + return ! empty( $raw_results );
464 752 }
465 753 }