fix(sites-model): pass explicit $wpdb format to override WP's site_id %d coercion#10
Conversation
The customer-facing render surfaces displayed a hardcoded "Sorry, there was an issue creating your site. Please contact support for assistance." for every failed site, regardless of whether the actual error was something the customer could fix themselves (subdomain already taken, etc.). The real error text already exists in two places — wp_iwp_sites.api_response (DB column, set by IWP_Site_Manager when the API call fails) and _iwp_creation_errors order meta (set by the order processor) — but neither was wired into the render path. - IWP_Site_Manager::resolve_failure_message() — new private helper. Looks at api_response first, falls back to per-order _iwp_creation_errors keyed by product_id. Runs the resolved string through IWP_API_Client::humanize_error() so messages stored before the make_request humanize fix landed still get the rewrite (idempotent for already-humanized text). - get_order_sites() — both the DB-sourced branch and the order- meta branch now populate $site['error_message'] when status is 'failed', using the helper. transform_site_data_for_frontend() signature unchanged — enrichment happens at the call site so the formatter stays focused on shape conversion. - class-iwp-frontend.php:811 (HTML render) and :1044 (email render) — prefer $site['error_message'] when present, fall back to the existing generic placeholder when it isn't (e.g. the failure predates this fix and neither source has the error text). Both render points escape with esc_html. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes for the admin Sites list at wp-admin/admin.php?page=instawp-sites: 1. Hide rows that carry no actionable info on every tab — failed or trashed rows with no site_url. They were left behind by older code paths (failed creates with no API response, pre-fallback tombstones) and only clutter the list. Visible failed/trashed rows still pass through (they have a URL or dashboard link to act on). Filter applied at the data-source layer (get_sites_from_database / get_sites_from_orders) so it covers every tab — All, Failed, Trash — instead of just default view. 2. On the Failed tab, surface the real (humanized) failure cause under the Failed badge so admins see what went wrong (subdomain taken, etc.) without having to click into the order. Pulled from wp_iwp_sites.api_response, which is the single source of truth: written at the instant of failure, overwritten on success. No order-meta fallback. Refactor on resolve_failure_message(): - Single-arg signature (api_response only). Returns early on empty / no error key. Drops the per-order _iwp_creation_errors fallback and the product_id matching — the DB column already reflects the row's current state at all times. - Removes the order-meta-branch enrichment in IWP_Site_Manager::get_order_sites() (no api_response there; single-source rule). - Net: 60-line drop in site-manager (over-engineered fallback paths gone), 28-line add in list-table (skip filter + DB enrichment + column display). The customer-facing render path in class-iwp-frontend.php already reads $site['error_message'] (commit 703d3f3) and is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WordPress core hardcodes field_types['site_id']='%d' in
wp-includes/load.php:780 (for multisite's wp_blogs.site_id). When
$wpdb->insert() / $wpdb->update() is called WITHOUT an explicit
$format, wpdb's process_field_formats() at class-wpdb.php:2898 falls
back to that registry by column NAME — regardless of actual column
type. Net effect: every "pending-<uniqid>" placeholder written by
IWP_Site_Manager::create_site_with_tracking() was cast to integer 0
in PHP before the SQL was built. Once one such row existed, the
UNIQUE constraint on site_id blocked every subsequent failed-pending
insert with a duplicate-key error — so failed orders silently lost
their DB row, and the customer-facing failed card stopped rendering.
- New static $column_formats map — single source of truth for the
per-column wpdb format hint. Documents the WP-core quirk in the
docblock so the next maintainer doesn't re-introduce the bug.
- New private format_for_data($data) helper builds a positional
$format array based on $data's keys, falling back to '%s' for
unrecognized columns.
- create() now passes self::format_for_data($insert_data) as the
third arg to $wpdb->insert(). The "pending-XYZ" placeholder
stores as the actual string instead of integer 0.
- update() now passes BOTH the 4th arg (self::format_for_data on
the SET clause) and the 5th arg (array('%s') for the WHERE) to
$wpdb->update(). Without the 5th, the existing pending→real-id
update flow would silently affect 0 rows because WHERE site_id
would match against int 0 instead of the actual placeholder.
Other code paths in the model (get_by_site_id, delete,
get_by_order_id, get_by_user_id) already pass explicit %s/%d via
$wpdb->prepare() and don't need changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @randhirinsta's task —— View job Pull Request Review: WordPress Field Type Coercion FixThis PR addresses a critical WordPress core compatibility issue where ✅ Excellent Root Cause Analysis and FixThe fix correctly addresses WordPress core's hardcoded field registry that coerces any column named // Before: Silently cast to integer 0
$wpdb->insert($table, ['site_id' => 'pending-abc123'])
// SQL: INSERT INTO `t` (`site_id`) VALUES (0)
// After: Preserve string value
$wpdb->insert($table, ['site_id' => 'pending-abc123'], ['%s'])
// SQL: INSERT INTO `t` (`site_id`) VALUES ('pending-abc123')🏗️ Architectural Principles Assessment✅ SSOT (Single Source of Truth)
✅ DRY (Don't Repeat Yourself)
✅ Proper Abstraction
✅ SoC (Separation of Concerns)
🔒 Security Assessment - EXCELLENT✅ SQL Injection Prevention
✅ Data Validation
🚀 Performance Considerations✅ Efficient Implementation
🔧 Code Quality Assessment✅ WordPress Coding Standards
✅ Error Handling
📊 Additional Changes AnalysisFrontend Error Message Display (
Sites List Table Improvements (
Site Manager Updates (
🧪 Testing ConsiderationsThe PR description provides excellent test coverage guidance:
|
Reverts the $site['error_message'] precompute that ran in get_order_sites
and get_sites_from_database for every failed row. Pass api_response
through $site instead — the decode + humanize via
IWP_Site_Manager::resolve_failure_message only runs at render time,
when a failed card actually shows. Same total work in the common case
(every failed row gets rendered), but cleaner separation: the data
layer carries the raw column, the render layer interprets.
- IWP_Site_Manager::get_order_sites() — DB branch now passes
'api_response' => $db_site->api_response into $site (no
precompute).
- IWP_Sites_List_Table::get_sites_from_database() — same.
- class-iwp-frontend.php:809 (HTML render) and :1045 (email render)
— call resolve_failure_message($site['api_response'] ?? null) inline.
- class-iwp-sites-list-table.php column_status() — same inline call.
resolve_failure_message() unchanged — still the single source of
truth for decoding the JSON-stored {"error": "..."} shape and
running it through humanize_error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @randhirinsta's task —— View job Pull Request Review: WordPress Field Type Coercion FixTodo List
✅ Excellent Root Cause Analysis and FixThis PR addresses a critical WordPress core compatibility issue where Root Cause Analysis ( // WordPress core pre-populates $wpdb->field_types['site_id'] = '%d'
// When wpdb->insert() is called WITHOUT explicit $format, it falls back
// to that registry by column NAME regardless of actual column type.
// Result: "pending-XYZ" → cast to integer 0 → UNIQUE constraint violationsSolution ( $result = $wpdb->insert(
self::$table_name,
$insert_data,
self::format_for_data($insert_data) // Explicit format overrides WP's registry
);🏗️ Architectural Principles Assessment - EXCELLENT✅ SSOT (Single Source of Truth)
✅ DRY (Don't Repeat Yourself)
✅ Proper Abstraction
✅ SoC (Separation of Concerns)
🔒 Security Assessment - EXCELLENT✅ SQL Injection Prevention
✅ Data Validation
🚀 Performance Considerations - GOOD✅ Efficient Implementation
🔧 Code Quality Assessment - EXCELLENT✅ WordPress Coding Standards
✅ Error Handling
📊 Additional Changes AnalysisFrontend Error Message Display (
Sites List Table Improvements (
Site Manager Updates (
|
Replaces the technical 0.0.12 entries (HPOS-aware get_orders_with_meta, field_types['site_id']='%d' coercion, $wpdb format arrays, helper- method names, etc.) with concise plain-English bullets that describe what changed for the store owner / customer. Covers everything in PR #8 and PR #10 under the existing 0.0.12 section. README mirrored in its NEW/CHANGED/FIXED prefix style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @randhirinsta's task —— View job Pull Request Review: WordPress Field Type Coercion FixTodo List
✅ Excellent Root Cause Analysis and FixThis PR addresses a critical WordPress core compatibility issue where Root Cause Analysis ( // WordPress core pre-populates $wpdb->field_types['site_id'] = '%d'
// When wpdb->insert() is called WITHOUT explicit $format, it falls back
// to that registry by column NAME regardless of actual column type.
// Result: "pending-XYZ" → cast to integer 0 → UNIQUE constraint violationsSolution ( $result = $wpdb->insert(
self::$table_name,
$insert_data,
self::format_for_data($insert_data) // Explicit format overrides WP's registry
);🏗️ Architectural Principles Assessment - EXCELLENT✅ SSOT (Single Source of Truth)
✅ DRY (Don't Repeat Yourself)
✅ Proper Abstraction
✅ SoC (Separation of Concerns)
🔒 Security Assessment - EXCELLENT✅ SQL Injection Prevention
✅ Data Validation
🚀 Performance Considerations - GOOD✅ Efficient Implementation
🔧 Code Quality Assessment - EXCELLENT✅ WordPress Coding Standards
✅ Error Handling
📊 Additional Changes AnalysisFrontend Error Message Display (
Sites List Table Improvements (
Site Manager Updates (
|
- api-client humanize_error: rephrase "site name X is already taken"
rewrite to use the customer-facing "subdomain (X)" terminology and
parens instead of quotes.
- site-manager resolve_failure_message: drop the redundant
humanize_error() re-pass at read. The message is already humanized
at write time in IWP_API_Client::make_request(), so re-running the
regex on every render is wasted work — just return the decoded
error string directly.
- admin Sites list:
- Show failed/trashed rows that have no site_url. The empty-URL
skip filter introduced earlier was too aggressive — those rows
do carry actionable info via the fallbacks below.
- Pull site_name / user_name from source_data JSON as fallbacks
for the URL and Username columns when the direct DB columns are
empty (typical state for failed creations that never reached
the API success response).
- Move the failure reason from a permanent <br><small> line under
the Failed badge to a title= tooltip on the status <span>.
Cleaner default look; the reason is one hover away.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @randhirinsta's task —— View job Pull Request Review: WordPress Field Type Coercion FixTodo List
✅ Excellent Root Cause Analysis and FixThis PR addresses a critical WordPress core compatibility issue where Root Cause Analysis ( // WordPress core pre-populates $wpdb->field_types['site_id'] = '%d'
// When wpdb->insert() is called WITHOUT explicit $format, it falls back
// to that registry by column NAME regardless of actual column type.
// Result: "pending-XYZ" → cast to integer 0 → UNIQUE constraint violationsSolution ( $result = $wpdb->insert(
self::$table_name,
$insert_data,
self::format_for_data($insert_data) // Explicit format overrides WP's registry
);🏗️ Architectural Principles Assessment - EXCELLENT✅ SSOT (Single Source of Truth)
✅ DRY (Don't Repeat Yourself)
✅ Proper Abstraction
✅ SoC (Separation of Concerns)
🔒 Security Assessment - EXCELLENT✅ SQL Injection Prevention
✅ Data Validation
🚀 Performance Considerations - GOOD✅ Efficient Implementation
🔧 Code Quality Assessment - EXCELLENT✅ WordPress Coding Standards
✅ Error Handling
📊 Additional Changes AnalysisFrontend Error Message Display (
Sites List Table Improvements (
Site Manager Updates (
|
Summary
Follow-up to PR #8 — fixes the actual root cause of the "failed order has no DB row → no failed card on customer view" symptom we hit during testing.
WordPress core hardcodes
$wpdb->field_types['site_id'] = '%d'inwp-includes/load.php:780(for multisite'swp_blogs.site_id). When$wpdb->insert()/$wpdb->update()is called without an explicit$format, wpdb'sprocess_field_formats()atclass-wpdb.php:2898falls back to that registry by column name — regardless of actual column type. The plugin'siwp_sites.site_idisvarchar(100), but the cast happens in PHP before the SQL is built, so the column type doesn't help.Net effect: every
"pending-<uniqid>"placeholder written byIWP_Site_Manager::create_site_with_tracking()was cast to integer0in PHP. Once one such row existed iniwp_sites, the UNIQUE constraint onsite_idblocked every subsequent failed-pending insert. Failed orders silently lost their DB row, and the customer-facing failed card stopped rendering.This PR also carries the two trailing commits from PR #8 that landed on this branch after #8 was merged (
703d3f3,7b5ee45).What's in this PR
fix(sites-model)— pass explicit$wpdbformat to override WP'ssite_id%d(this commit, the actual root-cause fix)fix(sites-list): hide useless rows + show real failure cause— admin Sites list polish, single-source resolve_failure_message refactorfix(frontend): show real error on failed site cards + emails— customer-facing render uses$site['error_message']Empirical proof of the bug + fix
Same
varchar(100)column, two calls — one without$format, one with:other_colon the same row (any name not in WP's registry) is preserved as a string in BOTH calls. The cast is purely a column-name match against$wpdb->field_types.Test plan
wp_iwp_sitesrow, the second hits dup-key. With this fix: both create their own rows with distinctsite_id="pending-xxx"strings./my-account/view-order/<id>/for a failed order shows the failed card with the friendly error message (from703d3f3).site_urlare hidden from default/All/Failed/Trash tabs (from7b5ee45); rows with a URL still show with the error under the Failed badge.site_id. The 5th-arg fix ($where_format = array('%s')) ensures the WHERE clause matches the placeholder string.site_id="0"rows from before this fix should be deleted manually as a one-off — they're tombstones from the duplicate-key blocker:🤖 Generated with Claude Code