Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like Google often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Google, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
26 | class Google extends Feed { |
||
27 | |||
28 | |||
29 | /** |
||
30 | * Google API Client. |
||
31 | * |
||
32 | * @access private |
||
33 | * @var \Google_Client |
||
34 | */ |
||
35 | protected $google_client = null; |
||
36 | |||
37 | /** |
||
38 | * Client scopes. |
||
39 | * |
||
40 | * @access private |
||
41 | * @var array |
||
42 | */ |
||
43 | protected $google_client_scopes = array(); |
||
44 | |||
45 | /** |
||
46 | * Google Calendar API key. |
||
47 | * |
||
48 | * @access protected |
||
49 | * @var string |
||
50 | */ |
||
51 | protected $google_api_key = ''; |
||
52 | |||
53 | /** |
||
54 | * Google Calendar ID. |
||
55 | * |
||
56 | * @access protected |
||
57 | * @var string |
||
58 | */ |
||
59 | protected $google_calendar_id = ''; |
||
60 | |||
61 | /** |
||
62 | * Google recurring events query setting. |
||
63 | * |
||
64 | * @access protected |
||
65 | * @var string |
||
66 | */ |
||
67 | protected $google_events_recurring = ''; |
||
68 | |||
69 | /** |
||
70 | * Google search query setting. |
||
71 | * |
||
72 | * @access protected |
||
73 | * @var string |
||
74 | */ |
||
75 | protected $google_search_query = ''; |
||
76 | |||
77 | /** |
||
78 | * Google max results query setting. |
||
79 | * |
||
80 | * @access protected |
||
81 | * @var int |
||
82 | */ |
||
83 | protected $google_max_results = 2500; |
||
84 | |||
85 | /** |
||
86 | * Set properties. |
||
87 | * |
||
88 | * @since 3.0.0 |
||
89 | * |
||
90 | * @param string|Calendar $calendar |
||
91 | * @param bool $load_admin |
||
92 | */ |
||
93 | public function __construct( $calendar = '', $load_admin = true ) { |
||
94 | |||
95 | parent::__construct( $calendar ); |
||
96 | |||
97 | $this->type = 'google'; |
||
98 | $this->name = __( 'Google Calendar', 'google-calendar-events' ); |
||
99 | |||
100 | // Google client config. |
||
101 | $settings = get_option( 'simple-calendar_settings_feeds' ); |
||
102 | $this->google_api_key = isset( $settings['google']['api_key'] ) ? esc_attr( $settings['google']['api_key'] ) : ''; |
||
103 | $this->google_client_scopes = array( \Google_Service_Calendar::CALENDAR_READONLY ); |
||
104 | $this->google_client = $this->get_client(); |
||
105 | |||
106 | if ( $this->post_id > 0 ) { |
||
107 | |||
108 | // Google query args. |
||
109 | $this->google_calendar_id = $this->esc_google_calendar_id( get_post_meta( $this->post_id, '_google_calendar_id', true ) ); |
||
110 | $this->google_events_recurring = esc_attr( get_post_meta( $this->post_id, '_google_events_recurring', true ) ); |
||
111 | // note that google_search_query is used in a URL param and not as HTML output, so don't use esc_attr() on it |
||
112 | $this->google_search_query = get_post_meta( $this->post_id, '_google_events_search_query', true ); |
||
113 | $this->google_max_results = max( absint( get_post_meta( $this->post_id, '_google_events_max_results', true ) ), 1 ); |
||
114 | |||
115 | if ( ! is_admin() || defined( 'DOING_AJAX' ) ) { |
||
116 | $this->events = ! empty( $this->google_api_key ) ? $this->get_events() : array(); |
||
|
|||
117 | } |
||
118 | } |
||
119 | |||
120 | if ( is_admin() && $load_admin ) { |
||
121 | $admin = new Admin( $this, $this->google_api_key, $this->google_calendar_id ); |
||
122 | $this->settings = $admin->settings_fields(); |
||
123 | } |
||
124 | } |
||
125 | |||
126 | /** |
||
127 | * Decode a calendar id. |
||
128 | * |
||
129 | * @since 3.0.0 |
||
130 | * |
||
131 | * @param string $id Base64 encoded id. |
||
132 | * |
||
133 | * @return string |
||
134 | */ |
||
135 | public function esc_google_calendar_id( $id ) { |
||
138 | |||
139 | /** |
||
140 | * Get events feed. |
||
141 | * |
||
142 | * Normalizes Google data into a standard array object to list events. |
||
143 | * |
||
144 | * @since 3.0.0 |
||
145 | * |
||
146 | * @return string|array |
||
147 | */ |
||
148 | public function get_events() { |
||
149 | |||
150 | $calendar = get_transient( '_simple-calendar_feed_id_' . strval( $this->post_id ) . '_' . $this->type ); |
||
151 | |||
152 | if ( empty( $calendar ) && ! empty( $this->google_calendar_id ) ) { |
||
153 | |||
154 | $error = ''; |
||
155 | |||
156 | try { |
||
157 | $response = $this->make_request( $this->google_calendar_id ); |
||
158 | } catch ( \Exception $e ) { |
||
159 | $error .= $e->getMessage(); |
||
160 | } |
||
161 | |||
162 | if ( empty( $error ) && isset( $response['events'] ) && isset( $response['timezone'] ) ) { |
||
163 | |||
164 | $calendar = array_merge( $response, array( 'events' => array() ) ); |
||
165 | |||
166 | // If no timezone has been set, use calendar feed. |
||
167 | if ( 'use_calendar' == $this->timezone_setting ) { |
||
168 | $this->timezone = $calendar['timezone']; |
||
169 | } |
||
170 | |||
171 | $source = isset( $response['title'] ) ? sanitize_text_field( $response['title'] ) : ''; |
||
172 | |||
173 | if ( ! empty( $response['events'] ) && is_array( $response['events'] ) ) { |
||
174 | foreach ( $response['events'] as $event ) { |
||
175 | if ( $event instanceof \Google_Service_Calendar_Event ) { |
||
176 | |||
177 | // Visibility and status. |
||
178 | // Public calendars may have private events which can't be properly accessed by simple api key method. |
||
179 | // Also want to skip cancelled events (single occurences deleted from repeating events) |
||
180 | $visibility = $event->getVisibility(); |
||
181 | $status = $event->getStatus(); |
||
182 | if ( $this->type == 'google' && ( $visibility == 'private' || $visibility == 'confidential' || $status == 'cancelled' ) ) { |
||
183 | continue; |
||
184 | } |
||
185 | |||
186 | // Event title & description. |
||
187 | $title = strip_tags( $event->getSummary() ); |
||
188 | $title = sanitize_text_field( iconv( mb_detect_encoding( $title, mb_detect_order(), true ), 'UTF-8', $title ) ); |
||
189 | $description = wp_kses_post( iconv( mb_detect_encoding( $event->getDescription(), mb_detect_order(), true ), 'UTF-8', $event->getDescription() ) ); |
||
190 | |||
191 | $whole_day = false; |
||
192 | |||
193 | // Event start properties. |
||
194 | View Code Duplication | if( 'use_calendar' == $this->timezone_setting ) { |
|
195 | $start_timezone = ! $event->getStart()->timeZone ? $calendar['timezone'] : $event->getStart()->timeZone; |
||
196 | } else { |
||
197 | $start_timezone = $this->timezone; |
||
198 | } |
||
199 | |||
200 | if ( is_null( $event->getStart()->dateTime ) ) { |
||
201 | // Whole day event. |
||
202 | $date = Carbon::parse( $event->getStart()->date ); |
||
203 | $google_start = Carbon::createFromDate( $date->year, $date->month, $date->day, $start_timezone )->startOfDay()->addSeconds( 59 ); |
||
204 | $google_start_utc = Carbon::createFromDate( $date->year, $date->month, $date->day, 'UTC' )->startOfDay()->addSeconds( 59 ); |
||
205 | $whole_day = true; |
||
206 | View Code Duplication | } else { |
|
207 | $date = Carbon::parse( $event->getStart()->dateTime ); |
||
208 | |||
209 | // Check if there is an event level timezone |
||
210 | if( $event->getStart()->timeZone && 'use_calendar' == $this->timezone_setting ) { |
||
211 | |||
212 | // Get the two different times with the separate timezones so we can check the offsets next |
||
213 | $google_start1 = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, $date->timezone ); |
||
214 | $google_start2 = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, $event->getStart()->timeZone ); |
||
215 | |||
216 | // Get the offset in hours |
||
217 | $offset1 = $google_start1->offsetHours; |
||
218 | $offset2 = $google_start2->offsetHours; |
||
219 | |||
220 | // Get the difference between the two timezones |
||
221 | $total_offset = ( $offset2 - $offset1 ); |
||
222 | |||
223 | // Add the hours offset to the date hour |
||
224 | $date->hour += $total_offset; |
||
225 | } |
||
226 | |||
227 | $google_start = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, $start_timezone ); |
||
228 | $google_start_utc = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, 'UTC' ); |
||
229 | |||
230 | $this->timezone = $start_timezone; |
||
231 | } |
||
232 | // Start. |
||
233 | $start = $google_start->getTimestamp(); |
||
234 | // Start UTC. |
||
235 | $start_utc = $google_start_utc->getTimestamp(); |
||
236 | |||
237 | $end = $end_utc = $end_timezone = ''; |
||
238 | $span = 0; |
||
239 | if ( false == $event->getEndTimeUnspecified() ) { |
||
240 | |||
241 | // Event end properties. |
||
242 | View Code Duplication | if( 'use_calendar' == $this->timezone_setting ) { |
|
243 | $end_timezone = ! $event->getEnd()->timeZone ? $calendar['timezone'] : $event->getEnd()->timeZone; |
||
244 | } else { |
||
245 | $end_timezone = $this->timezone; |
||
246 | } |
||
247 | |||
248 | if ( is_null( $event->getEnd()->dateTime ) ) { |
||
249 | // Whole day event. |
||
250 | $date = Carbon::parse( $event->getEnd()->date ); |
||
251 | $google_end = Carbon::createFromDate( $date->year, $date->month, $date->day, $end_timezone )->startOfDay()->subSeconds( 59 ); |
||
252 | $google_end_utc = Carbon::createFromDate( $date->year, $date->month, $date->day, 'UTC' )->startOfDay()->subSeconds( 59 ); |
||
253 | View Code Duplication | } else { |
|
254 | $date = Carbon::parse( $event->getEnd()->dateTime ); |
||
255 | |||
256 | // Check if there is an event level timezone |
||
257 | if( $event->getEnd()->timeZone && 'use_calendar' == $this->timezone_setting ) { |
||
258 | |||
259 | // Get the two different times with the separate timezones so we can check the offsets next |
||
260 | $google_start1 = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, $date->timezone ); |
||
261 | $google_start2 = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, $event->getEnd()->timeZone ); |
||
262 | |||
263 | // Get the offset in hours |
||
264 | $offset1 = $google_start1->offsetHours; |
||
265 | $offset2 = $google_start2->offsetHours; |
||
266 | |||
267 | // Get the difference between the two timezones |
||
268 | $total_offset = ( $offset2 - $offset1 ); |
||
269 | |||
270 | // Add the hours offset to the date hour |
||
271 | $date->hour += $total_offset; |
||
272 | } |
||
273 | |||
274 | $google_end = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, $end_timezone ); |
||
275 | $google_end_utc = Carbon::create( $date->year, $date->month, $date->day, $date->hour, $date->minute, $date->second, 'UTC' ); |
||
276 | } |
||
277 | // End. |
||
278 | $end = $google_end->getTimestamp(); |
||
279 | // End UTC. |
||
280 | $end_utc = $google_end_utc->getTimestamp(); |
||
281 | |||
282 | // Count multiple days. |
||
283 | $span = $google_start->diffInDays( $google_end ); |
||
284 | |||
285 | if ( $span == 0 ) { |
||
286 | if ( ( $google_start->toDateString() !== $google_end->toDateString() ) && $google_end->toTimeString() != '00:00:00' ) { |
||
287 | $span = 1; |
||
288 | } |
||
289 | } |
||
290 | } |
||
291 | |||
292 | // Multiple days. |
||
293 | $multiple_days = $span > 0 ? $span : false; |
||
294 | |||
295 | // Google cannot have two different locations for start and end time. |
||
296 | $start_location = $end_location = $event->getLocation(); |
||
297 | |||
298 | // Recurring event. |
||
299 | $recurrence = $event->getRecurrence(); |
||
300 | $recurring_id = $event->getRecurringEventId(); |
||
301 | if ( ! $recurrence && $recurring_id ) { |
||
302 | $recurrence = true; |
||
303 | } |
||
304 | |||
305 | // Event link. |
||
306 | if ( 'use_calendar' == $this->timezone_setting ) { |
||
307 | $link = add_query_arg( array( 'ctz' => $this->timezone ), $event->getHtmlLink() ); |
||
308 | } else { |
||
309 | $link = $event->getHtmlLink(); |
||
310 | } |
||
311 | |||
312 | // Build the event. |
||
313 | $calendar['events'][ intval( $start ) ][] = array( |
||
314 | 'type' => 'google-calendar', |
||
315 | 'source' => $source, |
||
316 | 'title' => $title, |
||
317 | 'description' => $description, |
||
318 | 'link' => $link, |
||
319 | 'visibility' => $visibility, |
||
320 | 'uid' => $event->id, |
||
321 | 'ical_id' => $event->getICalUID(), |
||
322 | 'calendar' => $this->post_id, |
||
323 | 'timezone' => $this->timezone, |
||
324 | 'start' => $start, |
||
325 | 'start_utc' => $start_utc, |
||
326 | 'start_timezone' => $start_timezone, |
||
327 | 'start_location' => $start_location, |
||
328 | 'end' => $end, |
||
329 | 'end_utc' => $end_utc, |
||
330 | 'end_timezone' => $end_timezone, |
||
331 | 'end_location' => $end_location, |
||
332 | 'whole_day' => $whole_day, |
||
333 | 'multiple_days' => $multiple_days, |
||
334 | 'recurrence' => $recurrence, |
||
335 | 'template' => $this->events_template, |
||
336 | ); |
||
337 | |||
338 | } |
||
339 | } |
||
340 | |||
341 | if ( ! empty( $calendar['events'] ) ) { |
||
342 | |||
343 | ksort( $calendar['events'], SORT_NUMERIC ); |
||
344 | |||
345 | set_transient( |
||
346 | '_simple-calendar_feed_id_' . strval( $this->post_id ) . '_' . $this->type, |
||
347 | $calendar, |
||
348 | max( absint( $this->cache ), 1 ) // Since a value of 0 means forever we set the minimum here to 1 if the user has set it to be 0 |
||
349 | ); |
||
350 | } |
||
351 | } |
||
352 | |||
353 | } else { |
||
354 | |||
355 | $message = __( 'While trying to retrieve events, Google returned an error:', 'google-calendar-events' ); |
||
356 | $message .= '<br><br>' . $error . '<br><br>'; |
||
357 | $message .= __( 'Please ensure that both your Google Calendar ID and API Key are valid and that the Google Calendar you want to display is public.', 'google-calendar-events' ) . '<br><br>'; |
||
358 | $message .= __( 'Only you can see this notice.', 'google-calendar-events' ); |
||
359 | |||
360 | return $message; |
||
361 | } |
||
362 | |||
363 | } |
||
364 | |||
365 | // If no timezone has been set, use calendar feed. |
||
366 | if ( 'use_calendar' == $this->timezone_setting && isset( $calendar['timezone'] ) ) { |
||
367 | $this->timezone = $calendar['timezone']; |
||
368 | } |
||
369 | |||
370 | return isset( $calendar['events'] ) ? $calendar['events'] : array(); |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * Query Google Calendar. |
||
375 | * |
||
376 | * @since 3.0.0 |
||
377 | * |
||
378 | * @param string $id A valid Google Calendar ID. |
||
379 | * @param int $time_min Lower bound timestamp. |
||
380 | * @param int $time_max Upper bound timestamp. |
||
381 | * |
||
382 | * @return array |
||
383 | * |
||
384 | * @throws \Exception On request failure will throw an exception from Google. |
||
385 | */ |
||
386 | public function make_request( $id = '', $time_min = 0, $time_max = 0 ) { |
||
454 | |||
455 | /** |
||
456 | * Google API Client. |
||
457 | * |
||
458 | * @since 3.0.0 |
||
459 | * @access private |
||
460 | * |
||
461 | * @return \Google_Client |
||
462 | */ |
||
463 | private function get_client() { |
||
473 | |||
474 | /** |
||
475 | * Google Calendar Service. |
||
476 | * |
||
477 | * @since 3.0.0 |
||
478 | * @access protected |
||
479 | * |
||
480 | * @return null|\Google_Service_Calendar |
||
481 | */ |
||
482 | protected function get_service() { |
||
485 | |||
486 | } |
||
487 |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.