value !== $direction ) { throw new InvalidArgumentException( sprintf( 'Invalid profile direction "%s". Profiles can only pull.', $direction ) ); } // Stages only round-trip back to the form; this layer just needs to // store them without silently corrupting the structure, so a failure // to encode or an unreasonable payload size is rejected outright. $stages_json = null; if ( [] !== $stages ) { $stages_json = wp_json_encode( $stages ); if ( false === $stages_json || strlen( $stages_json ) > 200 * 1024 ) { throw new InvalidArgumentException( 'Stages payload could not be saved: invalid or too large.' ); } } $data = [ 'label' => $clean_label, 'connection_id' => $connection_id, 'direction' => $direction, 'stages_json' => $stages_json, 'created_at' => $now, 'updated_at' => $now, 'last_used_at' => null, ]; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $result = $wpdb->insert( $wpdb->prefix . 'shiftora_profiles', $data, [ '%s', '%d', '%s', '%s', '%s', '%s', '%s' ] ); if ( false === $result ) { throw new RuntimeException( 'Shiftora: Failed to create profile record.' ); } return (int) $wpdb->insert_id; } /** * @return object|null Row object or null. */ public function find_by_id( int $id ): ?object { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d LIMIT 1', $wpdb->prefix . 'shiftora_profiles', $id ) ); return $row ?: null; } /** * List saved profiles, most recently used first. * * @return array */ public function list(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY COALESCE(last_used_at, created_at) DESC LIMIT 200', $wpdb->prefix . 'shiftora_profiles' ) ) ?: []; return $rows; } /** * Rename a profile. * * @throws InvalidArgumentException When the label is blank. */ public function rename( int $id, string $label ): bool { global $wpdb; $clean_label = Sanitizer::label( $label ); if ( '' === $clean_label ) { throw new InvalidArgumentException( 'A profile needs a name.' ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $updated = $wpdb->update( $wpdb->prefix . 'shiftora_profiles', [ 'label' => $clean_label, 'updated_at' => current_time( 'mysql', true ), ], [ 'id' => $id ], [ '%s', '%s' ], [ '%d' ] ); return false !== $updated; } /** * Mark a profile as just used, so list ordering reflects real use. */ public function touch_last_used( int $id ): bool { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $updated = $wpdb->update( $wpdb->prefix . 'shiftora_profiles', [ 'last_used_at' => current_time( 'mysql', true ), ], [ 'id' => $id ], [ '%s' ], [ '%d' ] ); return false !== $updated; } /** * Delete a profile permanently. */ public function delete( int $id ): bool { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $deleted = $wpdb->delete( $wpdb->prefix . 'shiftora_profiles', [ 'id' => $id ], [ '%d' ] ); return false !== $deleted; } // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped }