Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.slic
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ SLIC_GIT_HANDLE=stellarwp

# The path where `slic here` was executed.
# SLIC_HERE_DIR=
# The WordPress content directory inside the containers.
SLIC_WP_CONTENT_CONTAINER_DIR=/var/www/html/wp-content
# The path from which to read plugins from.
SLIC_PLUGINS_DIR=./_plugins
# The site's plugins path when a target-local override changes the effective Docker mount. Managed by `slic here`.
SLIC_SITE_PLUGINS_DIR=
# The path from which to read must-use plugins from.
SLIC_MU_PLUGINS_DIR=./_wordpress/wp-content/mu-plugins
# The path from which to read themes from.
SLIC_THEMES_DIR=./_wordpress/wp-content/themes
# The site's themes path when the effective Docker mount uses Slic's bundled themes. Managed by `slic here`.
SLIC_SITE_THEMES_DIR=
# The path from which to read WordPress core code from.
SLIC_WP_DIR=./_wordpress
# The path where mounted scripts will live
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export PATH=$PATH:path/to/slic

When we prep a release, we follow these steps:

1. Create a branch with the version number of the release, e.g. `1.0.0`
1. Create a branch named for the release, e.g. `release/1.0.0`
2. Merge changes into that branch. This will trigger the Docker images to build.
3. Ensure that the `CLI_VERSION` has been updated in `slic.php` to reflect the version number of the release.
4. Ensure that the `changelog.md` file has been updated.
5. Merge the release branch into `main`.
6. Delete the release branch.
7. Tag the release with the changelog.md contents for that release. This will trigger the final Docker images to build.
7. Publish a GitHub Release using the changelog contents for that release. This will trigger the final Docker images to build.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,19 @@ slic here
By running `slic here` at the site level, this allows you to set plugins, themes, or the site itself as the location
from which to run tests. This also has the benefit of running tests within the WP version that your site uses.

`slic here` mounts the complete site root at `/var/www/html`. WordPress core remains at `/var/www/html/wp` when the
project installs it in `wp/`, while root installations remain at `/var/www/html`. The detected `content/` or
`wp-content/` directory is mounted at the matching container path.

When a project installs WordPress in `wp/`, selecting an individual plugin or theme uses Slic's isolated WordPress
installation instead. This preserves existing component test configurations that expect core at `/var/www/html`.
Plugin targets use Slic's bundled default themes, while theme targets mount the project's themes directory. Selecting
`site` switches back to the complete project root. Slic recreates running PHP containers when these mounts change and
leaves the database and other services running.

`slic here` also disables subdirectory builds for sites and enables them for plugin and theme directories. You can
override the detected setting with `slic build-subdir on|off`.

![slic here](docs/images/slic-here-wp.gif)

### Preparing your project
Expand Down
16 changes: 16 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

# [2.4.0] - 2026-08-06
- Added - First-class support for site-mode projects that use `content/` instead of `wp-content/`, including plugin and theme container paths.
- Changed - `slic here` now mounts the complete project root for every WordPress site, including projects that install core into `wp/`.
- Changed - `slic here` now disables subdirectory builds for sites and enables them for plugin and theme directories.
- Fixed - Site mode no longer creates an unused `wp-content/` directory when the project uses a custom content directory.
- Fixed - Plugin and theme targets in projects that install WordPress in `wp/` continue to use Slic's isolated WordPress installation.
- Fixed - Switching between site and isolated component targets recreates only the running PHP containers so changed mounts take effect without resetting the test database.
- Fixed - Site theme discovery remains stable while plugin and theme targets use their required theme mounts.
- Fixed - Target-local mount overrides remain authoritative when Slic selects a site layout.
- Changed - Multi-target commands reject mixed site, plugin, and theme targets when their WordPress mounts are incompatible.
- Changed - Multi-target commands reject targets with different target-local bind-mount settings.
- Fixed - Target-local Composer cache and Slic scripts bind mounts now recreate running PHP containers and persist their runtime state.
- Fixed - Target switching now stops without persisting new mount state when Docker cannot determine which services are running.
- Fixed - Test containers now disable the object-cache drop-in without modifying the project file.
- Fixed - Airplane mode now uses and creates the configured must-use plugin directory.

# [2.3.0] - 2026-06-25
- Added - PCOV support to the slic and WordPress containers to make code coverage generation faster than Xdebug-backed coverage.
- Added - The `slic pcov on|off|status` command to configure PCOV and apply it to running containers and checks if xdebug is running and asks the user if they want to disable it.
Expand Down
4 changes: 3 additions & 1 deletion containers/slic/php.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ post_max_size=128M
;Allow long request.
max_execution_time=300

;Disable project object-cache drop-ins without modifying host files.
auto_prepend_file=/slic-disable-object-cache.php

;If XDebug is active, it should always start.
xdebug.start_with_request=yes
xdebug.mode=develop,debug,coverage
Expand All @@ -27,4 +30,3 @@ opcache.validate_timestamps=1
opcache.max_accelerated_files=25000
opcache.memory_consumption=128
opcache.interned_strings_buffer=16

26 changes: 26 additions & 0 deletions containers/wordpress/disable-object-cache.php

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice usage of preinitialized hooks!

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* Disable persistent object caching inside Slic test containers.
*
* WordPress automatically loads `object-cache.php` from `WP_CONTENT_DIR` before
* normal plugins. A project's drop-in can retain data between tests or require
* services and configuration that are not part of the test environment.
*
* `slic-stack.yml` bind-mounts this file read-only at
* `/slic-disable-object-cache.php` in both the WordPress and Slic/Codeception
* containers. Their PHP configuration loads it with `auto_prepend_file`, before
* WordPress begins bootstrapping. Unlike mounting over the drop-in itself, this
* does not create, replace, or remove anything in the host content directory.
*
* WordPress supports registering filters before its plugin API is loaded using
* this array structure. When `wp-includes/plugin.php` loads, it converts the
* entry into a `WP_Hook`. Later, `wp_start_object_cache()` applies the filter and
* skips the project's drop-in. WordPress then loads its built-in, non-persistent
* `WP_Object_Cache` implementation instead.
*/
$GLOBALS['wp_filter']['enable_loading_object_cache_dropin'][10][] = [
'function' => static function () {
return false;
},
'accepted_args' => 0,
];
3 changes: 3 additions & 0 deletions containers/wordpress/php.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ post_max_size=128M
;Allow long request.
max_execution_time=300

;Disable project object-cache drop-ins without modifying host files.
auto_prepend_file=/slic-disable-object-cache.php

;If XDebug is active, it should always start.
xdebug.start_with_request=yes
xdebug.mode=develop,debug,coverage
Expand Down
4 changes: 2 additions & 2 deletions docs/xdebug.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ In PHPStorm settings:
5. Set the `Host` to whatever `slic xdebug status` returns. (typically `http://localhost:8888` ... yes, put the whole thing in the `Host` field)
6. Set the `Port` to `80`
7. Check the `Use path mappings` checkbox
8. Find the `wp-content/plugins` directory and set the `Absolute path on the server` to `/var/www/html/wp-content/plugins`
8. Find the plugins directory and set the `Absolute path on the server` to the path reported by `slic xdebug status`
9. If you've added the `slic` directory to your workspace, find the `slic/_wordpress` directory and set the `Absolute path on the server` to `/var/www/html`

Screenshot from PhpStorm's video:
Expand Down Expand Up @@ -143,4 +143,4 @@ If breakpoints still don't work:
slic xdebug on
```

4. **Verify the port**: Ensure port 9001 is not blocked by a firewall and that your code editor is listening on that port.
4. **Verify the port**: Ensure port 9001 is not blocked by a firewall and that your code editor is listening on that port.
2 changes: 1 addition & 1 deletion slic-stack.site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ services:
slic:
environment:
# Move to the target directory before running the command from the plugins directory.
CODECEPTION_PROJECT_DIR: /var/www/html
CODECEPTION_PROJECT_DIR: ${SLIC_CURRENT_PROJECT_CONTAINER_PATH:-/var/www/html}
15 changes: 9 additions & 6 deletions slic-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ services:
volumes:
# Paths are relative to the directory that contains this file, NOT the current working directory.
- ${SLIC_WP_DIR}:/var/www/html
- ${SLIC_PLUGINS_DIR}:/var/www/html/wp-content/plugins
- ${SLIC_THEMES_DIR}:/var/www/html/wp-content/themes
- ${SLIC_PLUGINS_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins
- ${SLIC_THEMES_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/themes
- ./containers/wordpress/disable-object-cache.php:/slic-disable-object-cache.php:ro
- ${COMPOSER_CACHE_DIR:-./.cache}:/composer-cache
- ./containers/wordpress/php.ini:/usr/local/etc/php/conf.d/zz-docker.ini
healthcheck: # Apache service should be running correctly.
Expand Down Expand Up @@ -170,7 +171,7 @@ services:
# XDH=$(ip route | grep docker0 | awk '{print $9}') docker compose ...
XDEBUG_CONFIG: "idekey=${XDK:-slic} remote_enable=${XDE:-1} remote_host=${XDH:-host.docker.internal} remote_port=${XDP:-9001} client_host=${XDH:-host.docker.internal} client_port=${XDP:-9001}"
# Move to the target directory before running Codeception commands.
CODECEPTION_PROJECT_DIR: ${SLIC_CURRENT_PROJECT_CONTAINER_PATH:-/var/www/html/wp-content/plugins/test}
CODECEPTION_PROJECT_DIR: ${SLIC_CURRENT_PROJECT_CONTAINER_PATH:-${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins/test}
# When running the container in shell mode (using the slic `shell` command), then use this CC configuration.
CODECEPTION_SHELL_CONFIG: "-c codeception.slic.yml"
# Whether to disable the XDebug extension in the Codeception container completely or not.
Expand All @@ -194,9 +195,11 @@ services:
# Paths are relative to the directory that contains this file, NOT the current working directory.
# Share the WordPress core installation files in the `_wordpress` directory.
- ${SLIC_WP_DIR}:/var/www/html
# Share the plugins in the `/var/www/hmtl/wp-content/plugins` directory.
- ${SLIC_PLUGINS_DIR}:/var/www/html/wp-content/plugins
- ${SLIC_THEMES_DIR}:/var/www/html/wp-content/themes
# Share plugins and themes in the configured WordPress content directory.
- ${SLIC_PLUGINS_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/plugins
- ${SLIC_THEMES_DIR}:${SLIC_WP_CONTENT_CONTAINER_DIR}/themes
# Disable the project's persistent object-cache drop-in without modifying the host file.
- ./containers/wordpress/disable-object-cache.php:/slic-disable-object-cache.php:ro
# In some plugins we use function-mocker and set it up to cache in `/tmp/function-mocker`.
# To avoid a long re-caching on each run, let's cache in a docker volume, caching on the host
# filesystem would be a worse cure than the disease.
Expand Down
2 changes: 1 addition & 1 deletion slic.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
] );

$cli_name = 'slic';
const CLI_VERSION = '2.3.0';
const CLI_VERSION = '2.4.0';

// If the run-time option `-q`, for "quiet", is specified, then do not print the header.
if ( in_array( '-q', $argv, true ) || ( in_array( 'exec', $argv, true ) && ! in_array( 'help', $argv, true ) ) ) {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/airplane-mode.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
$plugin_dir = $mu_plugins_dir . DIRECTORY_SEPARATOR . 'airplane-mode';

if (
! is_dir( dirname( $mu_plugins_dir ) )
&& ! mkdir( $concurrentDirectory = dirname( $mu_plugins_dir ), 0755, true )
! is_dir( $mu_plugins_dir )
&& ! mkdir( $concurrentDirectory = $mu_plugins_dir, 0755, true )
&& ! is_dir( $concurrentDirectory )
) {
echo magenta( "Failed to create mu-plugins directory {$mu_plugins_dir}." );
Expand Down
39 changes: 26 additions & 13 deletions src/commands/here.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@
$sub_args = args( [ 'reset' ], $args( '...' ), 0 );
$reset = $sub_args( 'reset', false );

$wp_dir = SLIC_ROOT_DIR . '/_wordpress';
$plugins_dir = SLIC_ROOT_DIR . '/_plugins';
$themes_dir = SLIC_ROOT_DIR . '/_wordpress/wp-content/themes';
$wp_dir = SLIC_ROOT_DIR . '/_wordpress';
$plugins_dir = SLIC_ROOT_DIR . '/_plugins';
$mu_plugins_dir = SLIC_ROOT_DIR . '/_wordpress/wp-content/mu-plugins';
$themes_dir = SLIC_ROOT_DIR . '/_wordpress/wp-content/themes';

if ( empty( $reset ) ) {
$here_dir = getcwd();
Expand All @@ -50,28 +51,34 @@
}

$has_wp_config = dir_has_wp_config( $here_dir );
$env_values = [];
$env_values = [
'SLIC_BUILD_SUBDIR' => 1,
'SLIC_MU_PLUGINS_DIR' => $mu_plugins_dir,
'SLIC_SITE_PLUGINS_DIR' => '',
'SLIC_SITE_THEMES_DIR' => '',
'SLIC_WP_CONTENT_CONTAINER_DIR' => '/var/www/html/wp-content',
];

if ( $has_wp_config ) {
if ( file_exists( "{$here_dir}/wp-content" ) ) {
$wp_content_dir = "{$here_dir}/wp-content";
} elseif ( file_exists( "{$here_dir}/content" ) ) {
$wp_content_dir = "{$here_dir}/content";
} else {
echo magenta( "Cannot locate the wp-content directory. If you have a custom wp-content location, you will need to set the SLIC_WP_DIR, SLIC_PLUGINS_DIR, and SLIC_THEMES_DIR manually in slic's .env.slic.run file." );
echo magenta( "Cannot locate the WordPress content directory." );
exit( 1 );
}

$env_values['SLIC_HERE_DIR'] = $here_dir;

// Support WP skeleton.
if ( file_exists( "{$here_dir}/wp" ) ) {
$here_dir .= '/wp';
}

$env_values['SLIC_WP_DIR'] = $here_dir;
$env_values['SLIC_PLUGINS_DIR'] = "{$wp_content_dir}/plugins";
$env_values['SLIC_THEMES_DIR'] = "{$wp_content_dir}/themes";
$env_values['SLIC_BUILD_SUBDIR'] = 0;
$env_values['SLIC_MU_PLUGINS_DIR'] = "{$wp_content_dir}/mu-plugins";
$env_values['SLIC_PLUGINS_DIR'] = "{$wp_content_dir}/plugins";
$env_values['SLIC_SITE_PLUGINS_DIR'] = "{$wp_content_dir}/plugins";
$env_values['SLIC_SITE_THEMES_DIR'] = "{$wp_content_dir}/themes";
$env_values['SLIC_THEMES_DIR'] = "{$wp_content_dir}/themes";
$env_values['SLIC_WP_DIR'] = $here_dir;
$env_values['SLIC_WP_CONTENT_CONTAINER_DIR'] = '/var/www/html/' . basename( $wp_content_dir );
} else {
$env_values['SLIC_HERE_DIR'] = $here_dir;
$env_values['SLIC_WP_DIR'] = $wp_dir;
Expand All @@ -92,6 +99,12 @@
}
}

foreach ( slic_target_mount_base_keys() as $key => $base_key ) {
if ( array_key_exists( $key, $env_values ) ) {
$env_values[ $base_key ] = $env_values[ $key ];
}
}

// When changing the here target, clear the currently selected project.
$env_values['SLIC_CURRENT_PROJECT'] = '';
$env_values['SLIC_CURRENT_PROJECT_CONTAINER_PATH'] = '';
Expand Down
11 changes: 0 additions & 11 deletions src/commands/run.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,6 @@
// rather than always falling back to SLIC_PLUGINS_DIR.
$root = get_project_local_path();

// Object-cache is disruptive in the context of tests; remove the object cache drop-in before running the tests.
$object_cache_dropin = slic_wp_dir( 'wp-content/object-cache.php' );
if ( file_exists( $object_cache_dropin ) ) {
echo "Removing the object cache drop-in file before tests..." . PHP_EOL;
if ( ! unlink( $object_cache_dropin ) ) {
echo magenta( "Failed to remove the {$object_cache_dropin} file." . PHP_EOL );
exit( 1 );
}
echo "Object cache drop-in file removed." . PHP_EOL;
}

/*
* Check what configuration files we've got available.
* Depending on the what we have apply them in this order: dist, local, slic.
Expand Down
51 changes: 50 additions & 1 deletion src/commands/target.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,56 @@
} while ( ! empty( $last_target ) );

$targets = array_unique( $targets );
$target_types = array_unique( array_map( __NAMESPACE__ . '\\get_target_content_type', $targets ) );

if (
slic_here_is_site()
&& site_wordpress_is_in_subdirectory( realpath( getenv( 'SLIC_HERE_DIR' ) ) )
&& count( $target_types ) > 1
) {
echo magenta( 'Site, plugin, and theme targets use different mounts and cannot be mixed in one target command.' . PHP_EOL );
exit( 1 );
}

if ( count( $targets ) > 1 ) {
$mount_keys = slic_target_mount_keys();
$original_environment = [];

foreach ( array_merge( $mount_keys, [ 'SLIC_MU_PLUGINS_DIR' ] ) as $key ) {
$original_environment[ $key ] = getenv( $key );
}

$profiles = array_map( static function ( $target ) use ( $mount_keys ) {
foreach ( slic_target_mount_base_keys() as $key => $base_key ) {
putenv( "{$key}=" . getenv( $base_key ) );
}

slic_site_target_environment( explode( '/', $target )[0] );

$profile = [];
foreach ( $mount_keys as $key ) {
$profile[ $key ] = getenv( $key );
}

$file = get_project_local_path( $target ) . '/.env.slic.local';
$profile = array_merge(
$profile,
is_file( $file ) ? array_intersect_key( read_env_file( $file ), array_flip( $mount_keys ) ) : []
);
ksort( $profile );

return serialize( $profile );
}, $targets );

foreach ( $original_environment as $key => $value ) {
false === $value ? putenv( $key ) : putenv( "{$key}={$value}" );
}

if ( count( array_unique( $profiles ) ) > 1 ) {
echo magenta( 'Targets with different bind-mount settings cannot be mixed in one target command.' . PHP_EOL );
exit( 1 );
}
}

$command_lines = [];

Expand Down Expand Up @@ -86,4 +136,3 @@
slic_switch_target( $previous_target );

exit( $status );

3 changes: 1 addition & 2 deletions src/docker.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function slic_stack_array( $filenames_only = false ) {
$base_stack = stack();
$stack_array = [ $file_prefix, $quote . $base_stack . $quote ];

if ( slic_here_is_site() ) {
if ( slic_uses_site_root() ) {
$stack_array[] = $file_prefix;
$stack_array[] = $quote . stack( '.site' ) . $quote;
}
Expand Down Expand Up @@ -260,4 +260,3 @@ function docker_compose_realtime( array $options = [] ) {
function docker_compose_bin(): string {
return (string) getenv( 'SLIC_DOCKER_COMPOSE_BIN' ) ?: 'docker compose';
}

Loading