Skip to content

$pages->save() throws "[] operator not supported for strings" in PagesEditor.php (verbose-data references) #2269

@adrianbj

Description

@adrianbj

Short description of the issue

$pages->save() can throw Error: [] operator not supported for strings at wire/core/Pages/PagesEditor.php:876 (and the same line pattern at :1004). It occurs with the default options (getVerbose is false), so normal saving is affected, not just callers requesting verbose data.

Introduced by commit 941fd8b2 ("Add new $pages->save() options: saveAll, getVerbose, throw"). Still present in current dev — git log 941fd8b2..dev -- wire/core/Pages/PagesEditor.php is empty, so that commit is the last change to the file and upgrading dev does not resolve it.

Expected behavior

Saving a page completes normally.

Actual behavior

Error: [] operator not supported for strings
in wire/core/Pages/PagesEditor.php:876
    $logHooks[] = "saved";

Optional: Suggestion for a possible fix

The new verbose-logging code aliases $verboseData array elements by reference. In ___save() this is done only when verbose (lines 511–513, inside if($getVerbose)):

$logMessages = &$verboseData['messages'];
$logErrors   = &$verboseData['errors'];
$logHooks    = &$verboseData['hooks'];

But the two helper methods create the same aliases unconditionally, regardless of getVerbose:

  • savePageQuery() — lines 642–644
  • savePageFinish() — lines 849–851

Those aliases stay live across the hookable calls in the save path (statusChangeReady, saveReady, savePageOrFieldReady, addReady, saved, savedPageOrField, …). PHP's reference-on-copy behaviour then bites: when a live reference element exists and any structure sharing it is copied by value and that element reassigned, the original element is silently clobbered to a non-array. Minimal demonstration of the mechanism (PHP 8.5):

$vd = [];
$ref = &$vd['hooks'];        // live reference element
$ref[] = 'x'; $ref[] = 'y';
$copy = $vd;                 // copied while $ref is live -> copy shares the reference
$copy['hooks'] = "STRING";   // also clobbers $vd['hooks']
var_dump($vd['hooks']);      // string(6) "STRING"  -- no longer an array

Once $verboseData['hooks'] holds a string, $logHooks[] = … (which is &$verboseData['hooks']) throws. Two crash sites exist, both in savePageFinish() (both reached via the alias at line 851): line 876 (the "page unchanged" early-return branch) and line 1004 (the normal changed-page path).

Suggested fix: make the two helpers mirror what ___save() already does at 511–513 — alias only when verbose, otherwise append to a throwaway local array. Apply to savePageQuery() (642–644) and savePageFinish() (849–851):

$getVerbose = !empty($options['getVerbose']);
$logMessages = [];
$logErrors = [];
$logHooks = [];
if($getVerbose) {
    $logMessages = &$verboseData['messages'];
    $logErrors = &$verboseData['errors'];
    $logHooks = &$verboseData['hooks'];
}

With this, non-verbose saves (the default) send all $logMessages[] / $logErrors[] / $logHooks[] appends to discarded local arrays that never touch $verboseData, so both crash sites become impossible. Verbose behaviour is unchanged, and the live-reference exposure is reduced to the opt-in getVerbose case only.

Steps to reproduce the issue

The crash is environment-dependent: the fragile live references run for every save, but the clobbering copy-and-reassign is triggered by a save-hook in the stack, so a bare install may not surface it. Observed reliably when editing/saving a User page on a site with the usual set of save hooks (e.g. VersionControl snapshots, SeoMaestro, PrivacyWire).

  1. On dev (3.0.265+), have one or more modules hooking Pages::saveReady/Pages::saved (VersionControl, etc.).
  2. Edit and save any page (reproduced via ProcessUser editing a user).
  3. Save throws [] operator not supported for strings at PagesEditor.php:876.

Setup/Environment

  • ProcessWire version: dev 3.0.265–3.0.266 (latest dev; confirmed unfixed in 3.0.266)
  • (Optional) PHP version: 8.5.7
  • (Optional) Any 3rd party modules that are installed and could be related to the issue: modules that hook Pages::saveReady / Pages::saved (e.g. VersionControl, SeoMaestro, PrivacyWire) — anything adding save-path hooks can trigger it

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions