Completed
Push — develop ( a1bd27...53d72c )
by David
03:08
created

Sync_Service::get_payload_as_string()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 19
rs 9.6333
c 0
b 0
f 0
1
<?php
2
3
namespace Wordlift\Dataset;
4
5
use Wordlift\Api\Api_Service;
6
use Wordlift\Jsonld\Jsonld_Service;
7
use Wordlift\Object_Type_Enum;
8
9
class Sync_Service {
10
	const JSONLD_HASH = '_wl_jsonld_hash';
11
	const SYNCED_GMT = '_wl_synced_gmt';
12
13
	/**
14
	 * @var \Wordlift_Log_Service
15
	 */
16
	private $log;
17
18
	/**
19
	 * @var Api_Service
20
	 */
21
	private $api_service;
22
23
	/**
24
	 * @var Jsonld_Service
25
	 */
26
	private $jsonld_service;
27
28
	/**
29
	 * @var Sync_Background_Process
30
	 */
31
	private $sync_background_process;
32
33
	/**
34
	 * The number of posts processed in one call.
35
	 *
36
	 * @var int The batch size.
37
	 */
38
	private $batch_size;
39
40
	/**
41
	 * @var Sync_Object_Adapter_Factory
42
	 */
43
	private $sync_object_adapter_factory;
44
45
	/**
46
	 * @var Sync_Service
47
	 */
48
	private static $instance;
49
	private $entity_service;
50
51
	/**
52
	 * Constructor.
53
	 *
54
	 * @param Api_Service $api_service The {@link Api_Service} used to communicate with the remote APIs.
55
	 * @param Sync_Object_Adapter_Factory $sync_object_adapter_factory
56
	 * @param Jsonld_Service $jsonld_service
57
	 * @param \Wordlift_Entity_Service $entity_service
58
	 */
59
	public function __construct( $api_service, $sync_object_adapter_factory, $jsonld_service, $entity_service ) {
60
61
		$this->log = \Wordlift_Log_Service::get_logger( get_class() );
62
63
		$this->api_service                 = $api_service;
64
		$this->sync_object_adapter_factory = $sync_object_adapter_factory;
65
		$this->jsonld_service              = $jsonld_service;
66
		$this->entity_service              = $entity_service;
67
		$this->batch_size                  = 10;
68
69
		// You need to initialize this early, otherwise the Background Process isn't registered in AJAX calls.
70
		$this->sync_background_process = new Sync_Background_Process( $this );;
71
72
		// Exclude the JSONLD_HASH meta key from those that require a resync.
73
		add_filter( 'wl_dataset__sync_hooks__ignored_meta_keys', function ( $args ) {
74
			$args[] = Sync_Service::JSONLD_HASH;
75
			$args[] = Sync_Service::SYNCED_GMT;
76
77
			return $args;
78
		} );
79
80
		self::$instance = $this;
81
	}
82
83
	public static function get_instance() {
84
		return self::$instance;
85
	}
86
87
	/**
88
	 * @param int $type
89
	 * @param int $object_id
90
	 *
91
	 * @return array|false
92
	 * @throws \Exception
93
	 */
94
	public function sync_one( $type, $object_id ) {
95
96
		$object = $this->sync_object_adapter_factory->create( $type, $object_id );
97
98
		// Bail out if no payload.
99
		$payload_as_string = $this->get_payload_as_string( $object );
100
		if ( empty( $payload_as_string ) ) {
101
			return false;
102
		}
103
104
		// JSON-LD hasn't changed, bail out.
105
		$new_hash = sha1( $payload_as_string );
106
		$old_hash = $object->get_meta( self::JSONLD_HASH, true );
107
		if ( $new_hash === $old_hash ) {
108
			return false;
109
		}
110
111
		$response = $this->api_service->request(
112
			'POST', '/middleware/dataset/batch',
113
			array( 'Content-Type' => 'application/json', ),
114
			// Put the payload in a JSON array w/o decoding/encoding again.
115
			"[ $payload_as_string ]" );
116
117
		// Update the sync date in case of success, otherwise log an error.
118
		if ( ! $response->is_success() ) {
119
			return false;
120
		}
121
122
		$object->update_meta( self::JSONLD_HASH, $new_hash );
123
		$object->update_meta( self::SYNCED_GMT, current_time( 'mysql', true ) );
124
125
		return true;
126
	}
127
128
	public function delete_one( $type, $object_id ) {
129
		$object = $this->sync_object_adapter_factory->create( $type, $object_id );
130
		$uri    = $object->get_meta( 'entity_url', true );
131
132
		// Entity URL isn't set, bail out.
133
		if ( empty( $uri ) ) {
134
			return false;
135
		}
136
137
		$response = $this->api_service->request(
138
			'DELETE', sprintf( '/middleware/dataset?uri=%s', rawurlencode( $uri ) ) );
139
140
		// Update the sync date in case of success, otherwise log an error.
141
		if ( ! $response->is_success() ) {
142
			return false;
143
		}
144
145
		return true;
146
	}
147
148
	public function sync_many( $objects ) {
149
150
		$hashes   = array();
151
		$payloads = array();
152
		foreach ( $objects as $object ) {
153
			// Bail out if no payload.
154
			$payload_as_string = $this->get_payload_as_string( $object );
155
			if ( empty( $payload_as_string ) ) {
156
				continue;
157
			}
158
159
			$new_hash = sha1( $payload_as_string );
160
			$old_hash = $object->get_meta( self::JSONLD_HASH, true );
161
			// JSON-LD hasn't changed, bail out.
162
			if ( $new_hash === $old_hash ) {
163
				continue;
164
			}
165
166
			// Collect the hashes and the payloads.
167
			$hashes[]   = array( $object, $new_hash );
168
			$payloads[] = $payload_as_string;
169
		}
170
171
		$response = $this->api_service->request(
172
			'POST', '/middleware/dataset/batch',
173
			array( 'Content-Type' => 'application/json', ),
174
			// Put the payload in a JSON array w/o decoding/encoding again.
175
			'[ ' . implode( ', ', $payloads ) . ' ]' );
176
177
		// Update the sync date in case of success, otherwise log an error.
178
		if ( ! $response->is_success() ) {
179
			return false;
180
		}
181
182
		// If successful update the hashes and sync'ed datetime.
183
		foreach ( $hashes as $hash ) {
184
			$object   = $hash[0];
185
			$new_hash = $hash[1];
186
			$object->update_meta( self::JSONLD_HASH, $new_hash );
187
			$object->update_meta( self::SYNCED_GMT, current_time( 'mysql', true ) );
188
		}
189
190
		return true;
191
	}
192
193
	/**
194
	 * @param Sync_Object_Adapter $object
195
	 *
196
	 * @return false|string
197
	 * @throws \Exception
198
	 */
199
	private function get_payload_as_string( $object ) {
200
		$type      = $object->get_type();
201
		$object_id = $object->get_object_id();
202
203
		$jsonld_as_string = wp_json_encode( apply_filters( 'wl_dataset__sync_service__sync_item__jsonld',
204
			$this->jsonld_service->get( $type, $object_id ), $type, $object_id ) );
205
		$uri              = $this->entity_service->get_uri( $object_id, $type );
206
207
		// Entity URL isn't set, bail out.
208
		if ( empty( $uri ) ) {
209
			return false;
210
		}
211
212
		return wp_json_encode( array(
213
			'uri'     => $uri,
214
			'model'   => $jsonld_as_string,
215
			'private' => ! $object->is_public(),
216
		) );
217
	}
218
219
	/**
220
	 * Starts a new synchronization.
221
	 */
222
	public function start() {
223
224
		// Create the Sync_Background_Process.
225
		$this->sync_background_process->start();
226
227
	}
228
229
	/**
230
	 * Request to cancel a background process.
231
	 */
232
	public function request_cancel() {
233
234
		$this->sync_background_process->request_cancel();
235
236
	}
237
238
	/**
239
	 * Get the next post IDs to synchronize.
240
	 *
241
	 * @return array An array of post IDs.
242
	 */
243
	public function next() {
244
		global $wpdb;
245
246
		$state = $this->info();
247
248
		// Limit the query to the allowed post types.
249
		$post_type_in = implode( "','", array_map( 'esc_sql', \Wordlift_Entity_Service::valid_entity_post_types() ) );
250
251
		// Get the next post ID.
252
		return $wpdb->get_col( "
253
			SELECT p.ID
254
			FROM $wpdb->posts p
255
			WHERE p.post_status = 'publish'
256
			  AND p.post_type IN ('$post_type_in')
257
			ORDER BY p.ID
258
			LIMIT {$state->index},{$this->batch_size}
259
			" );
260
	}
261
262
	public function count() {
263
		global $wpdb;
264
		$post_type_in = implode( "','", array_map( 'esc_sql', \Wordlift_Entity_Service::valid_entity_post_types() ) );
265
266
		return $wpdb->get_var( "
267
			SELECT COUNT(1)
268
			FROM $wpdb->posts p
269
			WHERE p.post_status = 'publish'
270
			  AND p.post_type IN ('$post_type_in')
271
			" );
272
	}
273
274
	public function info() {
275
		return Sync_Background_Process::get_state();
276
	}
277
278
	public function sync_items(
279
		$post_ids, $clear = true
280
	) {
281
282
		$this->log->debug( sprintf( 'Synchronizing post(s) %s...', implode( ', ', $post_ids ) ) );
283
284
		// If we're starting the sync, try to clear the dataset.
285
		if ( $clear && 0 === $this->info()->index ) {
286
			$this->api_service->request( 'DELETE', '/middleware/dataset/delete' );
287
		}
288
289
		$that         = $this;
290
		$request_body = array_filter( array_map( function ( $post_id ) use ( $that ) {
291
			// Check if the post type is public.
292
			$post_type     = get_post_type( $post_id );
293
			$post_type_obj = get_post_type_object( $post_type );
294
			if ( ! $post_type_obj->public ) {
295
				return false;
296
			}
297
298
			$is_private       = ( 'publish' !== get_post_status( $post_id ) );
299
			$uri              = get_post_meta( $post_id, 'entity_url', true );
300
			$object_adapter   = $that->sync_object_adapter_factory->create( Object_Type_Enum::POST, $post_id );
301
			$jsonld           = $object_adapter->get_jsonld_and_update_hash();
302
			$jsonld_as_string = wp_json_encode( $jsonld );
303
304
			$that->log->trace( "Posting JSON-LD:\n$jsonld_as_string" );
305
306
			return array(
307
				'uri'     => $uri,
308
				'model'   => $jsonld_as_string,
309
				'private' => $is_private
310
			);
311
		}, $post_ids ) );
312
313
		// There's no point in making a request if the request is empty.
314
		if ( empty( $request_body ) ) {
315
			return true;
316
		}
317
318
		// Make a request to the remote endpoint.
319
		$state              = $this->info();
320
		$state_header_value = str_replace( "\n", '', wp_json_encode( $state ) );
321
		$response           = $this->api_service->request(
322
			'POST', '/middleware/dataset/batch',
323
			array(
324
				'Content-Type'                     => 'application/json',
325
				'X-Wordlift-Dataset-Sync-State-V1' => $state_header_value
326
			),
327
			wp_json_encode( $request_body ) );
328
329
		$this->log->debug( "Response received: " . ( $response->is_success() ? 'yes' : 'no' ) );
330
331
		// Update the sync date in case of success, otherwise log an error.
332
		if ( $response->is_success() ) {
333
334
			foreach ( $post_ids as $post_id ) {
335
				update_post_meta( $post_id, '_wl_synced_gmt', current_time( 'mysql', true ) );
336
			}
337
338
			$this->log->debug( sprintf( 'Posts %s synchronized.', implode( ', ', $post_ids ) ) );
339
340
			return true;
341
		} else {
342
			// @@todo: should we put a limit retry here?
343
			$response_dump = var_export( $response, true );
344
			$this->log->error(
345
				sprintf( 'An error occurred while synchronizing the data for post IDs %s: %s', implode( ', ', $post_ids ), $response_dump ) );
346
347
			return false;
348
		}
349
350
	}
351
352
	/**
353
	 * @param $post_id
354
	 *
355
	 * @todo Complete the delete item.
356
	 */
357
	public function delete_item( $post_id ) {
358
		$uri = get_post_meta( $post_id, 'entity_url', true );
359
		// Make a request to the remote endpoint.
360
		$state_header_value = str_replace( wp_json_encode( $this->info() ), "\n", '' );
361
		$response           = $this->api_service->request(
0 ignored issues
show
Unused Code introduced by
$response is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
362
			'DELETE', '/middleware/dataset?uri=' . rawurlencode( $uri ),
363
			array(
364
				'Content-Type'                     => 'application/ld+json',
365
				'X-Wordlift-Dataset-Sync-State-V1' => $state_header_value
366
			) );
367
	}
368
369
	public function get_batch_size() {
370
371
		return $this->batch_size;
372
	}
373
374
}
375