Problem/Motivation

In PHP 8.3, strrpos() no longer accepts null as parameter #1. In LeafletMap.php, when a View uses #rendered_view_fields for either Popup Content or Tooltips, strrpos(): Passing null to parameter #1 ($haystack) of type string is deprecated warnings are logged for every row, leading to watchdog spam and memory exhaustion on large datasets.

There are two distinct root causes in LeafletMap.php:
1. In processFeatureTooltip(): The render array key is written as "markup" instead of "#markup". Without the # prefix, Drupal's ThemeManager receives a NULL theme hook, triggering strrpos(null).
2. In getPopupContent(): $this->view->rowPlugin->render($result) returns either a string/Markup object or a render array whose #theme array may contain null suggestions. Passing this directly to
renderInIsolation() without checking types or filtering #theme causes strrpos() deprecations.

Steps to reproduce

1. Setup Drupal 11 on PHP 8.3.
2. Create a View using Leaflet Map style.
3. In style settings, set Popup Source or Tooltip Source to #rendered_view_fields.
4. View the map and observe watchdog logs for strrpos(): Passing null to parameter #1.

Proposed resolution

1. Fix the array key in processFeatureTooltip() from "markup" to "#markup".
2. In both getPopupContent() and processFeatureTooltip(), check if $rendered_output is an array:
- If array: sanitize #theme suggestions using array_filter($rendered_output['#theme'], 'is_string').
- If string/Markup: wrap it in ['#markup' => (string) $rendered_output].

Issue fork leaflet-3612703

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

konot created an issue. See original summary.

konot’s picture

There is a difference between 10.4.9 release and 10.4.x-dev, so I append here a patch specifically for compatibility with 10.4.9.

konot’s picture

Assigned: konot » Unassigned
Status: Active » Needs review
konot’s picture

Issue summary: View changes
itamair’s picture

Thanks @konot for reporting all this.

The issue report correctly identifies **one real bug** in `processFeatureTooltip()`.
But the popup path was never broken.
The MR fix works but is over-engineered and introduces a subtle new risk.

---

### Root Cause #1: The Tooltip Bug — Real and Clearly Broken

**Original code in `processFeatureTooltip()`** (upstream `10.4.x`):
```php
$render_row = [
"markup" => $this->view->rowPlugin->render($result),
];
$feature['tooltip']['value'] = $this->renderer->renderInIsolation($render_row);
```

This is **doubly wrong**:
1. `"markup"` (no `#` prefix) is not a recognized Drupal render property — the renderer ignores it, sees an element with no `#type`, `#theme`, `#markup`, or `#plain_text`, and has no idea what to render. In PHP 8.3, that broken render path ends up passing `null` as haystack to `strrpos()` inside Drupal's `ThemeManager`.
2. Even if the key were `"#markup"`, it would still be wrong, because `#markup` expects a string value — not the render array that `rowPlugin->render()` returns.

**This is a genuine bug that has been broken since this code was introduced.**

---

### Root Cause #2: The Popup Bug — Not Broken

**Original code in `getPopupContent()`**:
```php
$render_row = $this->view->rowPlugin->render($result);
$popup_content = $this->renderer->renderInIsolation($render_row);
```

This is **correct**. `rowPlugin->render()` returns a proper render array (for the Fields row plugin, it's `#theme => [array of theme suggestions]`, all strings derived from `buildThemeFunctions()`). Passing it straight to `renderInIsolation()` is exactly what the renderer expects. There is no PHP 8.3 issue here in normal operation.

---

### The `#theme` Null Filtering — Unnecessary Complexity

The MR filters `#theme` with `array_filter($rendered_output['#theme'], 'is_string')` in both paths. Looking at how Drupal core builds those theme suggestions (in `ViewExecutable::buildThemeFunctions()`):

```php
$themes[] = $hook . '__' . $id . '__' . $display['id'];
$themes[] = $hook . '__' . $display['id'];
// ...tags...
$themes[] = $hook . '__' . $id;
$themes[] = $hook;
```

These are all string concatenations. No `null` values can appear here unless `$hook` itself is null (which would be a different upstream bug entirely). The `is_string` filter solves a problem that doesn't exist in practice.

---

### The MR's Dangerous Edge Case

When `#theme` filtering produces an empty array, the MR does:
```php
unset($rendered_output['#theme']);
$rendered_output['#markup'] = $rendered_output['#markup'] ?? '';
```

This discards the entire render context (`#view`, `#options`, `#row`) and renders empty markup. If this branch ever executes (it won't in normal operation, but still), the popup/tooltip silently goes blank with no error. That's a hidden regression.

---

### The Correct Minimal Fix

The tooltip fix should simply mirror the popup code, which was already correct:

```php
// processFeatureTooltip() — was broken, correct fix:
$render_row = $this->view->rowPlugin->render($result);
$feature['tooltip']['value'] = (string) $this->renderer->renderInIsolation($render_row);
```

The `(string)` cast (which the MR does add everywhere, correctly) is a minor improvement since `renderInIsolation()` returns `MarkupInterface`, not a plain string.

For `getPopupContent()`, the original was fine — at most add the `(string)` cast.

itamair’s picture

Status: Needs review » Fixed

In the end, the correct minimal fix is two lines in `processFeatureTooltip()`:

drop the wrapping array entirely and pass `rowPlugin->render()` directly to `renderInIsolation()`, matching the popup code.

And this is exactly what have already been done/accomplished with this latest 10.4.x branch commit:
https://git.drupalcode.org/project/leaflet/-/commit/4eb392fa0d05e0b13190...

The popup path should not be touched.

Closing this as (already) Fixed".

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.