Skip to content

Commit 8614aaf

Browse files
committed
Improve inline documentation
This was a big one...
1 parent 787c0c1 commit 8614aaf

17 files changed

Lines changed: 618 additions & 137 deletions

classes/actions/class-content.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,23 @@ private function should_skip_saving( $post ) {
187187
/**
188188
* Check if there is a recent activity for this post.
189189
*
190+
* Prevents duplicate activity records by checking if a similar activity was already recorded.
191+
* Different activity types use different timeframes:
192+
*
193+
* Update activities (±12 hours):
194+
* - Uses a 24-hour window (±12 hours from modification time) to group related updates
195+
* - Prevents multiple update records when a post is saved repeatedly during editing
196+
* - Example: Editing a post at 3 PM won't create new activities if one exists between 3 AM and 3 AM next day
197+
* - The window accounts for timezone differences and allows one update record per day
198+
*
199+
* Other activities (exact match):
200+
* - Publish, trash, delete, etc. check for exact type/post matches
201+
* - No date window needed since these are discrete, one-time events
202+
*
190203
* @param \WP_Post $post The post object.
191204
* @param string $type The type of activity (ie publish, update, trash, delete etc).
192205
*
193-
* @return bool
206+
* @return bool True if a recent activity exists (skip recording), false otherwise (record new activity).
194207
*/
195208
private function is_there_recent_activity( $post, $type ) {
196209
// Query arguments.
@@ -200,7 +213,9 @@ private function is_there_recent_activity( $post, $type ) {
200213
'data_id' => (string) $post->ID,
201214
];
202215

203-
// If it's an update add the start and end date. We don't want to add multiple update activities for the same post on the same day.
216+
// For updates, use a ±12 hour window to prevent duplicate update records during editing sessions.
217+
// This groups all updates within a 24-hour period into a single activity.
218+
// Other activity types (publish, trash, delete) don't need a window since they're one-time events.
204219
if ( 'update' === $type ) {
205220
$query_args['start_date'] = \progress_planner()->get_utils__date()->get_datetime_from_mysql_date( $post->post_modified )->modify( '-12 hours' );
206221
$query_args['end_date'] = \progress_planner()->get_utils__date()->get_datetime_from_mysql_date( $post->post_modified )->modify( '+12 hours' );

classes/actions/class-maintenance.php

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,22 +134,50 @@ protected function create_maintenance_activity( $type ) {
134134
}
135135

136136
/**
137-
* Get the type of the update.
137+
* Get the type of the update from WordPress upgrade_* action options.
138138
*
139-
* @param array $options The options.
139+
* WordPress passes different type values depending on what was updated:
140+
* - 'plugin': Single plugin update via upgrader_process_complete
141+
* - 'theme': Single theme update
142+
* - 'core': WordPress core update
143+
* - 'translation': Language pack update
140144
*
141-
* @return string
145+
* Returns 'unknown' when:
146+
* - The type field is missing from options (shouldn't happen in normal operation)
147+
* - Hook is called incorrectly without proper options
148+
*
149+
* @param array $options {
150+
* Options array from WordPress upgrader_process_complete action.
151+
*
152+
* @type string $type The type of update: 'plugin', 'theme', 'core', 'translation'.
153+
* @type string $action The action performed: 'update', 'install'.
154+
* }
155+
*
156+
* @return string The update type ('plugin', 'theme', 'core', 'translation', or 'unknown').
142157
*/
143158
protected function get_update_type( $options ) {
144159
return isset( $options['type'] ) ? $options['type'] : 'unknown';
145160
}
146161

147162
/**
148-
* Get the type of the install.
163+
* Get the type of the install from WordPress install action options.
164+
*
165+
* WordPress passes different type values depending on what was installed:
166+
* - 'plugin': New plugin installation
167+
* - 'theme': New theme installation
168+
*
169+
* Returns 'unknown' when:
170+
* - The type field is missing from options
171+
* - Installation fails or is interrupted
172+
*
173+
* @param array $options {
174+
* Options array from WordPress upgrader_process_complete action.
149175
*
150-
* @param array $options The options.
176+
* @type string $type The type of installation: 'plugin' or 'theme'.
177+
* @type string $action The action performed: 'install'.
178+
* }
151179
*
152-
* @return string
180+
* @return string The install type ('plugin', 'theme', or 'unknown').
153181
*/
154182
protected function get_install_type( $options ) {
155183
return isset( $options['type'] ) ? $options['type'] : 'unknown';

classes/activities/class-query.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,17 +181,32 @@ public function query_activities_get_raw( $args ) {
181181
return [];
182182
}
183183

184-
// Remove duplicates. This could be removed in a future release.
184+
// Remove duplicate activities and clean up the database.
185+
// Duplicates can occur due to race conditions in concurrent processes.
186+
// This cleanup routine identifies duplicates by creating a unique key from:
187+
// - category (e.g., 'content', 'maintenance')
188+
// - type (e.g., 'post_publish', 'plugin_update')
189+
// - data_id (e.g., post ID, plugin slug)
190+
// - date (Y-m-d format)
191+
// When duplicates are found, only the first occurrence is kept, and subsequent
192+
// duplicates are permanently deleted from the database.
193+
// This could be removed in a future release once all legacy duplicates are cleaned up.
185194
$results_unique = [];
186195
foreach ( $results as $key => $result ) {
196+
// Generate unique key for this activity based on its core identifying attributes.
187197
$result_key = $result->category . $result->type . $result->data_id . $result->date; // @phpstan-ignore-line property.nonObject
188-
// Cleanup any duplicates that may exist.
198+
199+
// If we've already seen an activity with this key, it's a duplicate - delete it.
189200
if ( isset( $results_unique[ $result_key ] ) ) {
190201
$this->delete_activity_by_id( $result->id ); // @phpstan-ignore-line property.nonObject
191202
continue;
192203
}
193-
$results_unique[ $result->category . $result->type . $result->data_id . $result->date ] = $result; // @phpstan-ignore-line property.nonObject
204+
205+
// First occurrence of this activity - keep it.
206+
$results_unique[ $result_key ] = $result;
194207
}
208+
209+
// Return array values to reset numeric keys (0, 1, 2...) after filtering.
195210
return \array_values( $results_unique );
196211
}
197212

classes/admin/class-page.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,20 +228,26 @@ public function enqueue_scripts() {
228228
* @return void
229229
*/
230230
public function maybe_enqueue_focus_el_script( $hook ) {
231+
// Get all registered task providers from the task manager.
231232
$tasks_providers = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_providers();
232233
$tasks_details = [];
233234
$total_points = 0;
234235
$completed_points = 0;
236+
237+
// Filter providers to only those relevant to the current admin page.
235238
foreach ( $tasks_providers as $provider ) {
236239
$link_setting = $provider->get_link_setting();
240+
241+
// Skip tasks that aren't configured for this admin page.
237242
if ( ! isset( $link_setting['hook'] ) ||
238243
$hook !== $link_setting['hook']
239244
) {
240245
continue;
241246
}
242247

248+
// Build task details for JavaScript.
243249
$details = [
244-
'link_setting' => $link_setting,
250+
'link_setting' => $link_setting, // Contains selector, hook, and highlight config.
245251
'task_id' => $provider->get_task_id(),
246252
'points' => $provider->get_points(),
247253
'is_complete' => $provider->is_task_completed(),
@@ -254,11 +260,12 @@ public function maybe_enqueue_focus_el_script( $hook ) {
254260
}
255261
}
256262

263+
// No tasks for this page - don't enqueue the script.
257264
if ( empty( $tasks_details ) ) {
258265
return;
259266
}
260267

261-
// Register the scripts.
268+
// Enqueue the focus element script with task data.
262269
\progress_planner()->get_admin__enqueue()->enqueue_script(
263270
'focus-element',
264271
[

classes/class-badges.php

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,35 +156,52 @@ public function clear_content_progress() {
156156
}
157157

158158
/**
159-
* Get the latest completed badge.
159+
* Get the latest completed badge across all badge types.
160160
*
161-
* @return \Progress_Planner\Badges\Badge|null
161+
* Badge selection algorithm:
162+
* 1. Iterates through all badge contexts (content, maintenance, monthly_flat)
163+
* 2. For each badge, checks if it's 100% complete
164+
* 3. Compares completion dates stored in settings to find the most recent
165+
* 4. Returns the badge with the most recent completion date
166+
*
167+
* The completion date is stored in settings when a badge reaches 100% progress:
168+
* - Format: 'Y-m-d H:i:s' (e.g., '2025-10-31 14:30:00')
169+
* - Compared as Unix timestamps for accurate chronological ordering
170+
* - Later completion dates take precedence (>= comparison ensures newer badges win)
171+
*
172+
* This is used to:
173+
* - Display the most recent achievement on the dashboard
174+
* - Trigger celebrations for newly completed badges
175+
* - Track user progress momentum
176+
*
177+
* @return \Progress_Planner\Badges\Badge|null The most recently completed badge, or null if none completed.
162178
*/
163179
public function get_latest_completed_badge() {
164180
if ( $this->latest_completed_badge ) {
165181
return $this->latest_completed_badge;
166182
}
167183

168-
// Get the settings for badges.
184+
// Get the settings for badges (stores completion dates).
169185
$settings = \progress_planner()->get_settings()->get( 'badges', [] );
170186

171187
$latest_date = null;
172188

189+
// Loop through all badge contexts to find the most recently completed badge.
173190
foreach ( [ 'content', 'maintenance', 'monthly_flat' ] as $context ) {
174191
foreach ( $this->$context as $badge ) {
175-
// Skip if the badge has no date.
192+
// Skip badges that don't have a completion date recorded.
176193
if ( ! isset( $settings[ $badge->get_id() ]['date'] ) ) {
177194
continue;
178195
}
179196

180197
$badge_progress = $badge->get_progress();
181198

182-
// Continue if the badge is not completed.
199+
// Skip badges that aren't 100% complete.
183200
if ( 100 > (int) $badge_progress['progress'] ) {
184201
continue;
185202
}
186203

187-
// Set the first badge as the latest.
204+
// Initialize with the first completed badge found.
188205
if ( null === $latest_date ) {
189206
$this->latest_completed_badge = $badge;
190207
if ( isset( $settings[ $badge->get_id() ]['date'] ) ) {
@@ -193,7 +210,8 @@ public function get_latest_completed_badge() {
193210
continue;
194211
}
195212

196-
// Compare dates.
213+
// Compare completion dates as Unix timestamps to find the most recent.
214+
// Using >= ensures that if multiple badges complete simultaneously, the last one processed wins.
197215
if ( \DateTime::createFromFormat( 'Y-m-d H:i:s', $settings[ $badge->get_id() ]['date'] )->format( 'U' ) >= \DateTime::createFromFormat( 'Y-m-d H:i:s', $latest_date )->format( 'U' ) ) {
198216
$latest_date = $settings[ $badge->get_id() ]['date'];
199217
$this->latest_completed_badge = $badge;

classes/class-base.php

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -173,45 +173,86 @@ public function init() {
173173
}
174174

175175
/**
176-
* Magic method to get properties.
177-
* We use this to avoid a lot of code duplication.
176+
* Magic method to dynamically instantiate and cache plugin classes.
178177
*
179-
* Use a double underscore to separate namespaces:
180-
* - get_foo() will return an instance of Progress_Planner\Foo.
181-
* - get_foo_bar() will return an instance of Progress_Planner\Foo_Bar.
182-
* - get_foo_bar__baz() will return an instance of Progress_Planner\Foo_Bar\Baz.
178+
* This method enables lazy-loading of plugin classes using a simple naming convention,
179+
* reducing code duplication and improving performance by instantiating classes only when needed.
183180
*
184-
* @param string $name The name of the property.
185-
* @param array $arguments The arguments passed to the class constructor.
181+
* Naming convention and transformation rules:
182+
* - Method names must start with 'get_'
183+
* - Single underscore (_) = word boundary, becomes uppercase in class name
184+
* - Double underscore (__) = namespace separator, becomes backslash (\)
186185
*
187-
* @return mixed
186+
* Examples:
187+
* ```
188+
* get_settings() → Progress_Planner\Settings
189+
* get_admin__page() → Progress_Planner\Admin\Page
190+
* get_activities__query() → Progress_Planner\Activities\Query
191+
* get_suggested_tasks_db() → Progress_Planner\Suggested_Tasks_Db
192+
* get_admin__widgets__todo() → Progress_Planner\Admin\Widgets\Todo
193+
* ```
194+
*
195+
* Transformation process:
196+
* 1. Remove 'get_' prefix from method name
197+
* 2. Split on '__' to separate namespace parts
198+
* 3. For each part, split on '_', uppercase first letter of each word, rejoin
199+
* 4. Join namespace parts with '\' and prepend 'Progress_Planner\'
200+
*
201+
* Caching:
202+
* - Once instantiated, classes are cached in $this->cached array
203+
* - Subsequent calls return the cached instance (singleton pattern per class)
204+
* - Cache key is the method name without 'get_' prefix
205+
*
206+
* Backwards compatibility:
207+
* - Deprecated method names are mapped in Deprecations::BASE_METHODS
208+
* - Triggers WordPress deprecation notice and redirects to new method
209+
*
210+
* @param string $name The method name being called (e.g., 'get_admin__page').
211+
* @param array $arguments Arguments passed to the method (forwarded to class constructor).
212+
*
213+
* @return object|null The instantiated class, cached instance, or null if method doesn't start with 'get_'.
188214
*/
189215
public function __call( $name, $arguments ) {
216+
// Only handle methods starting with 'get_'.
190217
if ( 0 !== \strpos( $name, 'get_' ) ) {
191-
return;
218+
return null;
192219
}
220+
221+
// Extract cache key by removing 'get_' prefix.
193222
$cache_name = \substr( $name, 4 );
223+
224+
// Return cached instance if already instantiated (singleton pattern).
194225
if ( isset( $this->cached[ $cache_name ] ) ) {
195226
return $this->cached[ $cache_name ];
196227
}
197228

229+
// Transform method name to fully qualified class name.
230+
// Step 1: Split on '__' to get namespace parts (e.g., 'admin__page' → ['admin', 'page']).
198231
$class_name = \implode( '\\', \explode( '__', $cache_name ) );
232+
// Step 2: Split each part on '_', capitalize words, and rejoin.
233+
// e.g., 'suggested_tasks_db' → 'Suggested_Tasks_Db'.
234+
// Then prepend namespace: 'Progress_Planner\Suggested_Tasks_Db'.
199235
$class_name = 'Progress_Planner\\' . \implode( '_', \array_map( 'ucfirst', \explode( '_', $class_name ) ) );
236+
237+
// Instantiate the class if it exists.
200238
if ( \class_exists( $class_name ) ) {
201239
$this->cached[ $cache_name ] = new $class_name( $arguments );
202240
return $this->cached[ $cache_name ];
203241
}
204242

205-
// Backwards-compatibility.
243+
// Handle deprecated method names for backwards compatibility.
206244
if ( isset( Deprecations::BASE_METHODS[ $name ] ) ) {
207-
// Deprecated method.
245+
// Trigger WordPress deprecation notice.
208246
\_deprecated_function(
209247
\esc_html( $name ),
210-
\esc_html( Deprecations::BASE_METHODS[ $name ][1] ),
211-
\esc_html( Deprecations::BASE_METHODS[ $name ][0] )
248+
\esc_html( Deprecations::BASE_METHODS[ $name ][1] ), // Version deprecated.
249+
\esc_html( Deprecations::BASE_METHODS[ $name ][0] ) // Replacement method.
212250
);
251+
// Call the replacement method.
213252
return $this->{Deprecations::BASE_METHODS[ $name ][0]}();
214253
}
254+
255+
return null;
215256
}
216257

217258
/**

0 commit comments

Comments
 (0)