Completed
Push — develop ( da4181...8a2a4a )
by Naveen
26s queued 10s
created

Post_Adapter::wp_insert_post_data()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 10
nop 2
dl 0
loc 34
rs 8.4426
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file hooks to WordPress' post-related events in order to store entities.
4
 *
5
 * In particular we register the `wordlift/classification` block type and hook to the `wp_insert_post_data` hook
6
 * to parse the block type, retrieve the entities data and store it to the database.
7
 *
8
 * @authod David Riccitelli <[email protected]>
9
 * @since 3.23.0
10
 *
11
 * @package Wordlift
12
 * @subpackage Wordlift\Post
13
 */
14
15
namespace Wordlift\Post;
16
17
use Wordlift\Entity\Entity_Store;
18
19
class Post_Adapter {
20
21
	/**
22
	 * A {@link Wordlift_Log_Service} logging instance.
23
	 *
24
	 * @access private
25
	 * @var \Wordlift_Log_Service A {@link Wordlift_Log_Service} logging instance.
26
	 */
27
	private $log;
28
29
	/**
30
	 * A {@link Wordlift_Entity_Service} instance.
31
	 *
32
	 * @access private
33
	 * @var \Wordlift_Entity_Service A {@link Wordlift_Entity_Service} instance.
34
	 */
35
	private $entity_service;
36
37
	/**
38
	 * A {@link Entity_Store} instance.
39
	 *
40
	 * @access private
41
	 * @var Entity_Store $entity_store A {@link Entity_Store} instance.
42
	 */
43
	private $entity_store;
44
	/**
45
	 * @var \Wordlift_Entity_Uri_Service
46
	 */
47
	private $entity_uri_service;
48
49
	public function __construct() {
50
51
		// Bail out if block editor's functions aren't available.
52
		if ( ! function_exists( 'register_block_type' ) || ! function_exists( 'parse_blocks' ) ) {
53
			return;
54
		}
55
56
		$this->log = \Wordlift_Log_Service::get_logger( get_class() );
57
58
		$this->entity_service     = \Wordlift_Entity_Service::get_instance();
59
		$this->entity_store       = Entity_Store::get_instance();
60
		$this->entity_uri_service = \Wordlift_Entity_Uri_Service::get_instance();
61
		add_action( 'init', array( $this, 'init' ) );
62
		add_filter( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 );
63
64
	}
65
66
	/**
67
	 * Initialize by registering our block type `wordlift/classification`, required for {@link parse_blocks) to work
68
	 * correctly.
69
	 */
70
	public function init() {
71
72
		register_block_type( 'wordlift/classification', array(
73
			'editor_script' => 'wl-block-editor',
74
			'attributes'    => array(
75
				'entities' => array( 'type' => 'array' ),
76
			),
77
		) );
78
79
	}
80
81
	/**
82
	 * A sample structure:
83
	 *
84
	 * {
85
	 *   "entities": [
86
	 *     {
87
	 *       "annotations": {
88
	 *         "urn:enhancement-7e8e66fc": {
89
	 *           "start": 3480,
90
	 *           "end": 3486,
91
	 *           "text": "libero"
92
	 *         }
93
	 *       },
94
	 *       "description": "Le libero ou libéro est un poste défensif du volley-ball. Des règles particulières le concernant ont été introduites à la fin des années 1990. De par sa spécificité, le libéro a un statut à part au sein d’une équipe de volley-ball. Pour être identifié, il doit porter un uniforme qui contraste avec ceux des autres membres de son équipe, titulaires ou remplaçants.",
95
	 *       "id": "http://fr.dbpedia.org/resource/Libero_(volley-ball)",
96
	 *       "label": "Libero (volley-ball)",
97
	 *       "mainType": "other",
98
	 *       "occurrences": ["urn:enhancement-7e8e66fc"],
99
	 *       "sameAs": null,
100
	 *       "synonyms": [],
101
	 *       "types": ["other"]
102
	 *     }
103
	 *   ]
104
	 * }
105
	 *
106
	 * @param array $data An array of slashed post data.
107
	 * @param array $postarr An array of sanitized, but otherwise unmodified post data.
108
	 *
109
	 * @return array The data array.
110
	 * @throws \Exception
111
	 */
112
	public function wp_insert_post_data( $data, $postarr ) {
0 ignored issues
show
Unused Code introduced by
The parameter $postarr is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
113
114
		$post_status = $data['post_status'];
115
		if ( 'auto-draft' === $post_status || 'inherit' === $post_status ) {
116
			return $data;
117
		}
118
119
		$this->log->trace( "The following data has been received by `wp_insert_post_data`:\n"
120
		                   . var_export( $data, true ) );
121
122
123
		try {
124
			$entities = $this->parse_content( wp_unslash( $data['post_content'] ) );
125
126
			foreach ( $entities as $entity ) {
127
128
				$entity_uris = $this->get_entity_uris( $entity );
129
130
				if ( $this->get_first_matching_entity_by_uri( $entity_uris ) === null &&
131
				     Post_Entities_Validator::is_local_entity_uri_exist( $this->entity_uri_service, $entity_uris ) ) {
132
					// Skip the entity
133
					continue;
134
				}
135
				$this->create_or_update_entity( $entity, $data['post_status'] );
136
137
			}
138
139
		} catch ( \Exception $e ) {
140
			$this->log->error( $e->getMessage() );
141
		}
142
143
144
		return $data;
145
	}
146
147
	/**
148
	 * Parse the post content to find the `wordlift/classification` block and return the entities' data.
149
	 *
150
	 * @param string $post_content The post content.
151
	 *
152
	 * @return array An array of entities' structures.
153
	 * @throws \Exception
154
	 */
155
	private function parse_content( $post_content ) {
156
157
		$all_blocks = parse_blocks( $post_content );
158
		$this->log->trace( "The following blocks have been parsed while in `wp_insert_post`:\n"
159
		                   . var_export( $all_blocks, true ) );
160
161
		$blocks = array_filter( $all_blocks, function ( $item ) {
162
			return ! empty( $item['blockName'] ) && 'wordlift/classification' === $item['blockName'];
163
		} );
164
165
		// Bail out if the blocks' array is empty.
166
		if ( empty( $blocks ) ) {
167
			return array();
168
		}
169
170
		$block = current( $blocks );
171
		$this->log->trace( "The following block has been found while in `wp_insert_post`:\n"
172
		                   . var_export( $block, true ) );
173
174
		// Bail out if the entities array is empty.
175
		if ( empty( $block['attrs'] ) && empty( $block['attrs']['entities'] ) ) {
176
			return array();
177
		}
178
179
		return $block['attrs']['entities'];
180
	}
181
182
	/**
183
	 * Collect entity labels from the entity array.
184
	 *
185
	 * This function expects an array with the following keys:
186
	 *
187
	 * array(
188
	 *   'label'       => ...,
189
	 *   'synonyms'    => array( ... ),
190
	 *   'annotations' => array(
191
	 *     ...id...      => array( text => ... ),
192
	 *   ),
193
	 *   'occurrences' => array( ... ),
194
	 * )
195
	 *
196
	 * and it is going to output an array with all the labels, keeping the `label` at first position:
197
	 *
198
	 * array(
199
	 *   ...label...,
200
	 *   ...synonyms...,
201
	 *   ...texts...,
202
	 * )
203
	 *
204
	 * This function is going to collect the label from the `label` property, from the `synonyms` property and from
205
	 * `annotations` property. Since the `annotations` property contains all the annotations including those that
206
	 * haven't been selected, this function is going to only get the `text` for the annotations property listed in
207
	 * `occurrences`.
208
	 *
209
	 * @param array $entity {
210
	 *  The entity data.
211
	 *
212
	 * @type string $label The entity label.
213
	 * @type array $synonyms The entity synonyms.
214
	 * @type array $occurrences The selected occurrences.
215
	 * @type array $annotations The annotations.
216
	 * }
217
	 *
218
	 * @return array An array of labels.
219
	 */
220
	public function get_labels( $entity ) {
221
222
		$args = wp_parse_args( $entity, array(
223
			'label'       => array(),
224
			'synonyms'    => array(),
225
			'annotations' => array(),
226
			'occurrences' => array(),
227
		) );
228
229
		// We gather all the labels, occurrences texts and synonyms into one array.
230
		$initial = array_merge(
231
			(array) $args['label'],
232
			(array) $args['synonyms']
233
		);
234
235
		$annotations = $args['annotations'];
236
237
		return array_reduce( $args['occurrences'], function ( $carry, $item ) use ( $annotations ) {
238
239
			// Bail out if occurrences->$item->text isn't set or its contents are already
240
			// in `$carry`.
241
			if ( ! isset( $annotations[ $item ]['text'] )
242
			     || in_array( $annotations[ $item ]['text'], $carry ) ) {
243
				return $carry;
244
			}
245
246
			// Push the label.
247
			$carry[] = $annotations[ $item ]['text'];
248
249
			return $carry;
250
		}, $initial );
251
	}
252
253
	/**
254
	 * Create or update the entity.
255
	 *
256
	 * An entity lookup is performed on the local vocabulary using the `id` and `sameAs` URIs. If an entity is found
257
	 * the {@link Entity_Store} update function is called to update the `labels` and the `sameAs` values.
258
	 *
259
	 * If an entity is not found the {@link Entity_Store} create function is called to create a new entity.
260
	 *
261
	 * @param array $entity {
262
	 * The entity parameters.
263
	 *
264
	 * @type string The entity item id URI.
265
	 * @type string|array The entity sameAs URI(s).
266
	 * @type string $description The entity description.
267
	 * }
268
	 *
269
	 * @param       $string $post_status The post status, default 'draft'.
0 ignored issues
show
Documentation introduced by
The doc-type $string could not be parsed: Unknown type name "$string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
270
	 *
271
	 * @return int|\WP_Error
272
	 * @throws \Exception
273
	 */
274
	private function create_or_update_entity( $entity, $post_status = 'draft' ) {
275
276
		// Get only valid IDs.
277
		$uris = $this->get_entity_uris( $entity );
278
279
		$post = $this->get_first_matching_entity_by_uri( $uris );
280
281
		$this->log->trace( 'Entity' . ( empty( $post ) ? ' not' : '' ) . " found with the following URIs:\n"
282
		                   . var_export( $uris, true ) );
283
284
		// Get the labels.
285
		$labels = $this->get_labels( $entity );
286
287
		if ( empty( $post ) ) {
288
			// Create the entity if it doesn't exist.
289
			$post_id = $this->entity_store->create( array(
290
				'labels'      => $labels,
291
				'description' => $entity['description'],
292
				'same_as'     => $uris,
293
			), $post_status );
294
295
			// Return the WP_Error if we got one.
296
			if ( is_wp_error( $post_id ) ) {
297
				return $post_id;
298
			}
299
300
			// Add the entity type.
301
			if ( isset( $entity['mainType'] ) ) {
302
				wp_set_object_terms( $post_id, $entity['mainType'], \Wordlift_Entity_Type_Taxonomy_Service::TAXONOMY_NAME );
303
			}
304
305
			if ( isset( $entity['properties'] ) && isset( $entity['properties']['latitude'] ) && isset( $entity['properties']['longitude'] ) ) {
306
				add_post_meta( $post_id, \Wordlift_Schema_Service::FIELD_GEO_LATITUDE, $entity['properties']['latitude'] );
307
				add_post_meta( $post_id, \Wordlift_Schema_Service::FIELD_GEO_LONGITUDE, $entity['properties']['longitude'] );
308
			}
309
		} else {
310
			// Update the entity otherwise.
311
			$post_id = $this->entity_store->update( array(
312
				'ID'      => $post->ID,
313
				'labels'  => $labels,
314
				'same_as' => $uris,
315
			) );
316
317
			// Add the entity type.
318
			if ( isset( $entity['mainType'] ) ) {
319
				wp_add_object_terms( $post_id, $entity['mainType'], \Wordlift_Entity_Type_Taxonomy_Service::TAXONOMY_NAME );
320
			}
321
322
			// see https://github.com/insideout10/wordlift-plugin/issues/1304
323
			// Set the post status, we need to set that in order to support entities
324
			// created using rest endpoint on block editor, so that they get published
325
			// when the post is published.
326
			// Once the entity is published dont update the post status.
327
			if ( $post->post_status !== 'publish' ) {
328
				wp_update_post( array(
329
					'ID'          => $post->ID,
330
					'post_status' => $post_status
331
				) );
332
			}
333
		}
334
335
		return $post_id;
336
	}
337
338
	/**
339
	 * Get the first matching entity for the provided URI array.
340
	 *
341
	 * Entities IDs and sameAs are searched.
342
	 *
343
	 * @param array $uris An array of URIs.
344
	 *
345
	 * @return \WP_Post|null The entity WP_Post if found or null if not found.
346
	 */
347
	private function get_first_matching_entity_by_uri( $uris ) {
348
349
		foreach ( $uris as $uri ) {
350
			$existing_entity = $this->entity_service->get_entity_post_by_uri( $uri );
0 ignored issues
show
Deprecated Code introduced by
The method Wordlift_Entity_Service::get_entity_post_by_uri() has been deprecated with message: in favor of Wordlift_Entity_Uri_Service->get_entity( $uri );

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
351
			if ( isset( $existing_entity ) ) {
352
				return $existing_entity;
353
			}
354
		}
355
356
		return null;
357
	}
358
359
	/**
360
	 * @param $entity
361
	 *
362
	 * @return array
363
	 */
364
	private function filter_valid_entity_ids( $entity ) {
365
		$id = $entity['id'];
366
367
		return array_filter( (array) $id, function ( $id ) {
368
			return preg_match( '|^https?://|', $id );
369
		} );
370
	}
371
372
	/**
373
	 * @param array $entity
374
	 *
375
	 * @return array
376
	 */
377
	private function get_entity_uris( $entity ) {
378
		$ids = $this->filter_valid_entity_ids( $entity );
379
380
		return array_merge(
381
			(array) $ids,
382
			(array) $entity['sameAs']
383
		);
384
	}
385
386
}
387