alleyinteractive / alleyinteractive/mantle-framework
`is_within_wordpress_install()` false-positives for wp-content-rooted checkouts, silently disabling `maybe_rsync()` / `with_sqlite()`
- Lenguaje dominante
- PHP
- Estrellas
- 28
- Forks
- 7
- Merge medio
- 2 d 4 h
- PR fusionados (30 d)
- 2
Descripción
### Description of the bug
### Summary
`Rsync_Installation::is_within_wordpress_install()` decides whether the framework is
already running inside an installed WordPress by string-matching its own vendored path:
```php
// src/mantle/testing/concerns/trait-rsync-installation.php
public function is_within_wordpress_install(): bool {
return false !== strpos( __DIR__, '/wp-content/' );
}
```
`__DIR__` here is the framework's own install location
(`.../wp-content/.../vendor/alleyinteractive/mantle-framework/...`). In any project laid
out with the repository root **named or nested under `wp-content`** — common in WordPress
VIP / wp-content-rooted monorepos — this string is always present, so the method returns
`true` even when there is **no WordPress core above the checkout**.
Because this method guards several builder calls, they all become silent no-ops.
### Affected version
- `v1.19.3` (ref `d37a354fde52bc27395e86f47295a9b721f8e782`). The check is unchanged on
current `main` at the time of writing.
### Guarded call sites (all early-return `$this` with no error/log)
- `maybe_rsync()`
- `with_vip_mu_plugins()`
- `with_object_cache()`
- `with_sqlite()`
Example:
```php
public function with_sqlite( bool $install = true ): static {
if ( $this->is_within_wordpress_install() ) {
return $this; // <-- silently does nothing for wp-content-rooted repos
}
putenv( 'MANTLE_USE_SQLITE=' . ( $install ? '1' : '0' ) );
// ...
}
```
### Impact
For wp-content-rooted checkouts, `->maybe_rsync_wp_content()->with_sqlite()` in a test
bootstrap **does nothing** — the suite silently runs against the in-place MySQL DB instead
of an rsynced SQLite install. There is:
- **No error and no log line** indicating the calls were skipped, and
- **No public override** to force the "not within an install" branch.
So the documented "Using SQLite for the database" flow
(https://mantle.alley.com/docs/testing/installation-manager#using-sqlite-for-the-database)
is unreachable in these layouts without editing vendored code.
### Reproduction
1. Check out any project whose path contains `/wp-content/` above the vendor dir
(e.g. repo root is `wp-content/`, Mantle at
`wp-content/.../vendor/alleyinteractive/mantle-framework`), with **no `wp-load.php`**
above `wp-content`.
2. Test bootstrap:
```php
use function Mantle\Testing\manager;
manager()->maybe_rsync_wp_content()->with_sqlite()->install();
```
3. Run PHPUnit on the host. **Expected:** rsync to a fresh install + SQLite drop-in.
**Actual:** both calls are no-ops; the suite runs in place against MySQL. No indication
that rsync/SQLite were skipped.
### Root cause
`strpos( __DIR__, '/wp-content/' )` is a proxy for "am I inside a real WP install," but it
matches on a directory *name* rather than the presence of WordPress itself, so it collides
with wp-content-rooted repo layouts.
### Proposed fix (suggestion)
Keep the fast string check as a cheap negative, then confirm an actual WP core exists above
`wp-content`. This also preserves the method's second role as the **rsync recursion guard**
— the rsynced scratch copy *does* have `wp-load.php` above it, so it still reads as "within
an install" and won't re-rsync:
```php
public function is_within_wordpress_install(): bool {
if ( false === strpos( __DIR__, '/wp-content/' ) ) {
return false;
}
$parent = preg_replace( '/\/wp-content\/.*$/', '', __DIR__ );
return is_file( $parent . '/wp-load.php' );
}
```
Alternatives worth considering (maintainer's call):
- Walk up from `__DIR__` looking for `wp-load.php` rather than assuming the `wp-content`
segment is the boundary (handles non-standard core locations).
- Honor an explicit env/override (e.g. `MANTLE_WITHIN_WP_INSTALL=0`) so callers can force
the rsync path deterministically.
- At minimum, `log()`/emit a notice when a guarded call is skipped because the gate is
`true`, so the no-op is not silent.
### Steps To Reproduce
See above.
### Additional Information
_No response_
Guía de contribución
Evaluación
Este issue todavía no se ha evaluado.