Completed
Push — master ( 829da2...8585d5 )
by David
03:05
created

Wordlift_Entity_Link_Service::slug_exists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * The Wordlift_Entity_Link_Service handles linking and rendering of entities. In order to perform such actions it hooks
5
 * on different WordPress' actions and filters:
6
 *
7
 *  1. to change links to entities, also supporting an empty entity post type slug, e.g. http://example.org/entity-name
8
 *     instead of http://example.org/entity/entity-name, this requires hooking to:
9
 *     a) post_type_link in order to remove the 'entity' post type slug, when links are rendered by WordPress,
10
 *     b) pre_get_posts in order to alter the WP Query instance and add our own entity post type to the query when WordPress
11
 *        needs to decide which post to show (otherwise we would get a 404).
12
 *
13
 *  2. when using an empty entity post type slug (but we perform in any case, should the entity post type slug set to
14
 *     empty later on), we need to ensure that the posts/pages/entities' slugs to not conflict (even though WordPress
15
 *     itself allows conflicts, see https://core.trac.wordpress.org/ticket/13459 ). To do so we hook to a couple of filters
16
 *     to validate a slug:
17
 *     a) for posts and entities, to wp_unique_post_slug_is_bad_flat_slug, and we check that no other page, post or entity
18
 *        uses that slug,
19
 *     b) for pages, to wp_unique_post_slug_is_bad_hierarchical_slug.
20
 *     If we find that the slug (postname) is already used, we tell WordPress, which in turn will append a sequential number.
21
 *
22
 *
23
 * @since 3.6.0
24
 */
25
class Wordlift_Entity_Link_Service {
26
27
	/**
28
	 * The entity type service.
29
	 *
30
	 * @since 3.6.0
31
	 * @access private
32
	 * @var Wordlift_Entity_Type_Service $entity_type_service The entity type service.
33
	 */
34
	private $entity_type_service;
35
36
	/**
37
	 * The entity post type slug.
38
	 *
39
	 * @since 3.6.0
40
	 * @access private
41
	 * @var string $slug The entity post type slug.
42
	 */
43
	private $slug;
44
45
	/**
46
	 * A logger instance.
47
	 *
48
	 * @since 3.6.0
49
	 * @access private
50
	 * @var Wordlift_Log_Service
51
	 */
52
	private $log;
53
54
	/**
55
	 * Wordlift_Entity_Link_Service constructor.
56
	 *
57
	 * @since 3.6.0
58
	 *
59
	 * @param Wordlift_Entity_Type_Service $entity_type_service
60
	 * @param string $slug The entity post type slug.
61
	 */
62
	public function __construct( $entity_type_service, $slug ) {
63
64
		$this->log = Wordlift_Log_Service::get_logger( 'Wordlift_Entity_Link_Service' );
65
66
		$this->entity_type_service = $entity_type_service;
67
		$this->slug                = $slug;
68
69
	}
70
71
	/**
72
	 * Intercept link generation to posts in order to customize links to entities.
73
	 *
74
	 * @since 3.6.0
75
	 *
76
	 * @param string $post_link The post's permalink.
77
	 * @param WP_Post $post The post in question.
78
	 * @param bool $leavename Whether to keep the post name.
79
	 * @param bool $sample Is it a sample permalink.
80
	 *
81
	 * @return string The link to the post.
82
	 */
83
	public function post_type_link( $post_link, $post, $leavename, $sample ) {
0 ignored issues
show
Unused Code introduced by
The parameter $leavename 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...
Unused Code introduced by
The parameter $sample 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...
84
85
		// Return the post link if this is not our post type.
86
		if ( ! empty( $this->slug ) || $this->entity_type_service->get_post_type() !== get_post_type( $post ) ) {
87
			return $post_link;
88
		}
89
90
		// Replace /slug/post_name/ with /post_name/
91
		// The slug comes from the Entity Type Service since that service is responsible for registering the default
92
		// slug.
93
		return str_replace( "/{$this->entity_type_service->get_slug()}/$post->post_name/", "/$post->post_name/", $post_link );
94
	}
95
96
	/**
97
	 * Alter the query to look for our own custom type.
98
	 *
99
	 * @since 3.6.0
100
	 *
101
	 * @param WP_Query $query
102
	 */
103
	public function pre_get_posts( $query ) {
104
105
		// If a slug has been set, we don't need to alter the query.
106
		if ( ! empty( $this->slug ) ) {
107
			return;
108
		}
109
110
		// Check if it's a query we should extend with our own custom post type.
111
		if ( ! $query->is_main_query() || 2 != count( $query->query ) || ! isset( $query->query['page'] ) || empty( $query->query['name'] ) ) {
112
			return;
113
		}
114
115
		// Add our own post type to the query.
116
		$post_type = is_array( $query->get( 'post_type' ) ) ? $query->get( 'post_type' ) : array();
117
		$query->set( 'post_type', array_merge( $post_type, array( 'post', $this->entity_type_service->get_post_type(), 'page' ) ) );
118
119
	}
120
121
	/**
122
	 * Hook to WordPress' wp_unique_post_slug_is_bad_flat_slug filter. This is called when a page is saved.
123
	 *
124
	 * @since 3.6.0
125
	 *
126
	 * @param bool $bad_slug Whether the post slug would be bad as a flat slug.
127
	 * @param string $slug The post slug.
128
	 * @param string $post_type Post type.
129
	 *
130
	 * @return bool Whether the slug is bad.
131
	 */
132
	public function wp_unique_post_slug_is_bad_flat_slug( $bad_slug, $slug, $post_type ) {
133
134
		// The list of post types that might have conflicting slugs.
135
		$post_types = array( 'post', 'page', $this->entity_type_service->get_post_type() );
136
137
		// Ignore post types different from the ones we need to check.
138
		if ( ! in_array( $post_type, $post_types ) ) {
139
			return $bad_slug;
140
		}
141
142
		$exists = $this->slug_exists( $slug, array_diff( $post_types, array( $post_type ) ) );
143
144
		$this->log->debug( "[ exists :: " . ( $exists ? "yes" : "no" ) . " ]" );
145
146
		return $exists;
147
	}
148
149
	/**
150
	 * Hook to WordPress' wp_unique_post_slug_is_bad_hierarchical_slug filter. This is called when a page is saved.
151
	 *
152
	 * @since 3.6.0
153
	 *
154
	 * @param bool $bad_slug Whether the post slug would be bad as a flat slug.
155
	 * @param string $slug The post slug.
156
	 * @param string $post_type Post type.
157
	 * @param int $post_parent
158
	 *
159
	 * @return bool Whether the slug is bad.
160
	 */
161
	public function wp_unique_post_slug_is_bad_hierarchical_slug( $bad_slug, $slug, $post_type, $post_parent ) {
0 ignored issues
show
Unused Code introduced by
The parameter $post_parent 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...
162
163
		// We only care about pages here.
164
		if ( 'page' !== $post_type ) {
165
			return $bad_slug;
166
		}
167
168
		// We redirect the call to the flat hook, this means that this check is going to solve also the 6-years old issue
169
		// about overlapping slugs among pages and posts:
170
		//  https://core.trac.wordpress.org/ticket/13459
171
		return $this->wp_unique_post_slug_is_bad_flat_slug( $bad_slug, $slug, $post_type );
172
	}
173
174
	/**
175
	 * Check whether a slug exists already for the specified post types.
176
	 *
177
	 * @since 3.6.0
178
	 *
179
	 * @param string $slug The slug.
180
	 * @param array $post_types An array of post types.
181
	 *
182
	 * @return bool True if the slug exists, otherwise false.
183
	 */
184
	private function slug_exists( $slug, $post_types ) {
185
		global $wpdb;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
186
187
		// Post slugs must be unique across all posts.
188
		$check_sql = "SELECT post_name FROM $wpdb->posts WHERE post_name = %s AND post_type IN ('" . implode( "', '", array_map( 'esc_sql', $post_types ) ) . "') LIMIT 1";
189
190
		return null !== $wpdb->get_var( $wpdb->prepare( $check_sql, $slug ) );
191
	}
192
193
}
194