Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.73% covered (warning)
72.73%
64 / 88
50.00% covered (danger)
50.00%
5 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Workflow_Repository
72.73% covered (warning)
72.73%
64 / 88
50.00% covered (danger)
50.00%
5 / 10
37.68
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 find
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 find_all
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 find_by
93.10% covered (success)
93.10%
27 / 29
0.00% covered (danger)
0.00%
0 / 1
9.03
 persist
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 prepare_data_to_persist
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 insert
73.33% covered (warning)
73.33%
11 / 15
0.00% covered (danger)
0.00%
0 / 1
2.08
 update
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 delete
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
3.65
 map_row_to_workflow
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * The workflow repository responsible for communicating with the database.
4 *
5 * @package automattic/jetpack-crm
6 * @since 6.2.0
7 */
8
9namespace Automattic\Jetpack\CRM\Automation\Workflow;
10
11use Automattic\Jetpack\CRM\Automation\Automation_Workflow;
12use Automattic\Jetpack\CRM\Automation\Workflow_Exception;
13use wpdb;
14
15/**
16 * Class Workflow_Repository.
17 *
18 * @since 6.2.0
19 */
20class Workflow_Repository {
21
22    /**
23     * The WordPress database access layer.
24     *
25     * @since 6.2.0
26     * @var wpdb
27     */
28    protected $wpdb;
29
30    /**
31     * The workflows table name.
32     *
33     * @var string
34     */
35    protected $table_name;
36
37    /**
38     * Constructor.
39     *
40     * @global wpdb     $wpdb WordPress database abstraction object.
41     * @global string[] $ZBSCRM_t An array of Jetpack CRM table names.
42     * @since 6.2.0
43     */
44    public function __construct() {
45        global $wpdb, $ZBSCRM_t; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
46
47        $this->wpdb       = $wpdb;
48        $this->table_name = $ZBSCRM_t['automation-workflows']; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.VariableNotSnakeCase
49    }
50
51    /**
52     * Find Workflow by ID.
53     *
54     * @since 6.2.0
55     *
56     * @param int $id The workflow ID.
57     * @return Automation_Workflow|false The workflow object or false if not found.
58     */
59    public function find( int $id ) {
60        $row = $this->wpdb->get_row(
61            $this->wpdb->prepare( "SELECT * FROM {$this->table_name} WHERE id=%d", $id ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
62            ARRAY_A
63        );
64
65        if ( ! is_array( $row ) ) {
66            return false;
67        }
68
69        return $this->map_row_to_workflow( $row );
70    }
71
72    /**
73     * Get all workflows.
74     *
75     * @since 6.2.0
76     *
77     * @return Automation_Workflow[]
78     */
79    public function find_all(): array {
80        return $this->find_by( array() );
81    }
82
83    /**
84     * Find workflows with the given criteria.
85     *
86     * @todo Implement "order by" logic.
87     *
88     * @since 6.2.0
89     *
90     * @param array  $criteria Workflow arguments to filter the workflows result.
91     * @param string $order_by The column to order by.
92     * @param int    $limit The maximum number of results to return.
93     * @param int    $offset The offset to start from.
94     *
95     * @return Automation_Workflow[]
96     */
97    public function find_by( array $criteria, string $order_by = 'id', int $limit = 0, int $offset = 0 ): array {
98        $query = "SELECT * FROM {$this->table_name}";
99
100        $allowed_criteria = array(
101            'active'     => '%d',
102            'name'       => '%s',
103            'category'   => '%s',
104            'created_at' => '%d',
105            'updated_at' => '%d',
106        );
107
108        $where = array();
109
110        // Prepare the WHERE criteria.
111        foreach ( $criteria as $key => $value ) {
112            if ( ! isset( $allowed_criteria[ $key ] ) ) {
113                continue;
114            }
115            $where[] = $this->wpdb->prepare( "{$key}={$allowed_criteria[ $key ]}", $value ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
116        }
117
118        // Build the WHERE clause.
119        if ( ! empty( $where ) ) {
120            $query .= ' WHERE ' . implode( ' AND ', $where );
121        }
122
123        // Add limit/offset clause.
124        if ( $limit > 0 || $offset > 0 ) {
125            // It seems intuitive to provide "0" to mean "no limit", but technically it means that
126            // we do not want to return any results at all, so to help with the developer experience
127            // we convert "0" to a very high number instead.
128            if ( 0 === $limit ) {
129                $limit = PHP_INT_MAX;
130            }
131
132            $query .= $this->wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
133                ' LIMIT %d OFFSET %d',
134                $limit,
135                $offset
136            );
137        }
138
139        $rows = $this->wpdb->get_results( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
140
141        if ( empty( $rows ) ) {
142            return array();
143        }
144
145        foreach ( $rows as $index => $row ) {
146            $rows[ $index ] = $this->map_row_to_workflow( $row );
147        }
148
149        return $rows;
150    }
151
152    /**
153     * Persist a workflow.
154     *
155     * This is used to both update and create a workflow.
156     *
157     * @since 6.2.0
158     *
159     * @param Automation_Workflow $workflow The workflow to persist.
160     * @return void
161     *
162     * @throws Workflow_Exception Throw error if the workflow could not be persisted.
163     */
164    public function persist( Automation_Workflow $workflow ): void {
165        if ( $workflow->get_id() && is_numeric( $workflow->get_id() ) ) {
166            $this->update( $workflow );
167        } else {
168            $this->insert( $workflow );
169        }
170    }
171
172    /**
173     * Prepare the data to persist.
174     *
175     * @since 6.2.0
176     *
177     * @param Automation_Workflow $workflow The workflow to persist.
178     * @return array The workflow raw data.
179     */
180    protected function prepare_data_to_persist( Automation_Workflow $workflow ): array {
181        $data = $workflow->to_array();
182
183        $data['triggers'] = wp_json_encode( $data['triggers'] );
184        $data['steps']    = wp_json_encode( $data['steps'] );
185
186        return $data;
187    }
188
189    /**
190     * Insert a workflow into the database.
191     *
192     * @since 6.2.0
193     *
194     * @param Automation_Workflow $workflow The workflow to persist.
195     *
196     * @throws Workflow_Exception Throw error if the workflow could not be inserted.
197     */
198    protected function insert( Automation_Workflow $workflow ) {
199
200        $time = time();
201        $workflow->set_created_at( $time );
202        $workflow->set_updated_at( $time );
203
204        $data = $this->prepare_data_to_persist( $workflow );
205
206        // Technically speaking, then "id" could contain a string which
207        // would cause conflicts with the database, so we should unset
208        // it to be safe.
209        unset( $data['id'] );
210
211        $inserted = $this->wpdb->insert(
212            $this->table_name,
213            $data
214        );
215
216        if ( ! $inserted ) {
217            throw new Workflow_Exception(
218                $this->wpdb->last_error,
219                Workflow_Exception::FAILED_TO_INSERT
220            );
221        }
222
223        $workflow->set_id( $this->wpdb->insert_id );
224    }
225
226    /**
227     * Update a workflow.
228     *
229     * @since 6.2.0
230     *
231     * @param Automation_Workflow $workflow The workflow to persist.
232     * @return void
233     *
234     * @throws Workflow_Exception Throw error if the workflow could not be updated.
235     */
236    protected function update( Automation_Workflow $workflow ): void {
237        $workflow->set_updated_at( time() );
238
239        $data = $this->prepare_data_to_persist( $workflow );
240
241        $updated = $this->wpdb->update(
242            $this->table_name,
243            $data,
244            array( 'id' => $data['id'] )
245        );
246
247        if ( ! $updated ) {
248            throw new Workflow_Exception(
249                $this->wpdb->last_error,
250                Workflow_Exception::FAILED_TO_UPDATE
251            );
252        }
253    }
254
255    /**
256     * Delete a workflow.
257     *
258     * @since 6.2.0
259     *
260     * @param Automation_Workflow $workflow The workflow to delete.
261     * @return bool
262     *
263     * @throws Workflow_Exception Throw error if the workflow could not be deleted.
264     */
265    public function delete( Automation_Workflow $workflow ): bool {
266        if ( ! is_numeric( $workflow->get_id() ) ) {
267            /** @todo Should this return an error since tried to delete a programmatically defined workflow? */
268            return false;
269        }
270
271        $deleted = $this->wpdb->delete(
272            $this->table_name,
273            array( 'id' => $workflow->get_id() )
274        );
275
276        if ( ! $deleted ) {
277            throw new Workflow_Exception(
278                $this->wpdb->last_error,
279                Workflow_Exception::FAILED_TO_DELETE
280            );
281        }
282
283        return true;
284    }
285
286    /**
287     * Map a database row to a workflow object.
288     *
289     * @since 6.2.0
290     *
291     * @param array $row The database row.
292     * @return Automation_Workflow
293     */
294    protected function map_row_to_workflow( array $row ): Automation_Workflow {
295        $row['triggers'] = json_decode( $row['triggers'] );
296        $row['steps']    = json_decode( $row['steps'], true );
297
298        return new Automation_Workflow( $row );
299    }
300}