Cross Container (ECS) WordPress Session Management

Since containers are ephemeral and each instance handles requests independently, sharing session data requires using a centralized session storage such as AWS ElastiCache.

ElastiCache can be configured for Valkey (open source Reddis) or Memcache. Valkey is lower cost.

Set up ElastiCache

  • Create a Valkey server.
  • Configure a publicly accessible or VPC-limited endpoint, depending on your ECS networking setup.
  • Choose 3 subnets on same network as ECS containers
  • Choose the ECS security group

Configure The Docker Image

Add the Redis extension to PHP and enable it in the php.ini configuration. This configuration uses environment variables so the Redis server can be configured with environment variables for each container instance.

Create The Host Image Builder PHP Ini File

Review the PHP Runtime Configuration page on session settings.

Create ./Docker/Images/Files/php/docker-php-ext-redis.ini

extension=redis.so
session.save_handler = ${PHP_SESSION_SAVE_HANDLER}
session.save_path = ${PHP_SESSION_SAVE_PATH}

Update The Host Dockerfile

Update the host Dockerfile to install Redis and the libs needed to support it. Copy the php ini file into conf.d so it is loaded when PHP starts. This example is from a WordPress 6 image running PHP 8 on Apache.

Create ./Docker/Images/Dockerfile

# -- base image

FROM public.ecr.aws/docker/library/wordpress:6.4.2-php8.3-apache
LABEL authors="lancecleveland" \
      image="WordPress Multisite on Apache"

# -- ports

EXPOSE 443

# -- os utilities

RUN set -eux; \
	apt-get update; \
	apt-get install -y --no-install-recommends \
		dnsutils \
        inetutils-traceroute \
        iputils-ping \
        libz-dev \
        libssl-dev \
        libmagickwand-dev \
	; \
	rm -rf \
        /var/lib/apt/lists/* \
	    /usr/src/wordpress/wp-content/themes/* \
	    /usr/src/wordpress/wp-content/plugins/* \
	    /usr/src/wordpress/wp-config-example.php \
    ;

# -- install Redis PHP extension
RUN pecl channel-update pecl.php.net \
    && pecl install redis \
    && docker-php-ext-enable redis

# -- PHP redis
COPY ./Files/php/docker-php-ext-redis.ini /usr/local/etc/php/conf.d/docker-php-ext-redis.ini

# -- apache rewrite

RUN a2enmod ssl && a2enmod rewrite; \
    mkdir -p /etc/apache2/ssl

# -- apache SSL

COPY ./Files/ssl/*.pem /etc/apache2/ssl/
COPY ./Files/apache/sites-available/*.conf /etc/apache2/sites-available/

# -- WordPress , gets copies to apache root /var/www/html
COPY ./Files/wordpress/ /usr/src/wordpress/

# -- php xdebug

RUN pecl channel-update pecl.php.net
RUN pecl install xdebug \
    && docker-php-ext-enable xdebug

# -- Standard WordPress Env Vars

ENV WORDPRESS_DB_USER="blah_blah_user"
ENV WORDPRESS_DB_NAME="blah_blah_database"
ENV WORDPRESS_TABLE_PREFIX="wp_"
ENV WORDPRESS_DB_CHARSET="utf8"
ENV WORDPRESS_DB_COLLATE=""

Configure The Docker Container

Update Docker Composer and ECS Task Definitions

Docker Composer is for local development container setup. ECS Task definitions are for AWS Cloud Elastic Container Services.

For our local Docker Composer configuration we use a docker-compose secrets file that is not committed to our repository for setting sensitive environment variables.

In this example the PHP_SESSION_* environment variables are read by the PHP startup and substituted in the session.* variables.

./Docker/Composers/Secrets/docker-compose-secrets.yml

This configuration uses local file based session storage. This is what you’d use on a typical single-server development file.

services:
wp:
environment:
PHP_SESSION_SAVE_HANDLER: 'files'
PHP_SESSION_SAVE_PATH: ''

For a PHP connection to a cluster, like we have on our AWS fault-tolerant container clusters you and fault-tolerant ElastiCache clusters you need to set something similar in the Task Definition environment variables using the same names as above.

      PHP_SESSION_SAVE_HANDLER: 'redis'
PHP_SESSION_SAVE_PATH: 'tcp://blah-saas-staging.blah.blah.blah.amazonaws.com:6379?persistent=1&failover=1&timeout=2&read_timeout=2&serialize=php&cluster=redis'

Load Balancer Sticky Sessions Option

Configure your Application Load Balancer (or Elastic Load Balancer) to enable sticky sessions to reduce the need to share session data across containers. Sticky sessions ensure that a user is always directed to the same container instance during their session.

– Application Load Balancer: Enable Session Stickiness.
– Set a **duration-based stickiness** cookie to control how long the user remains connected to the same task/container.

**Note**: Sticky sessions are not ideal for auto-scaling environments or when maintaining container independence is critical, so this should complement, not replace, shared session storage.

Additional Considerations

1. **Security**:
– Encrypt session data in transit using TLS (especially when connecting to Redis or RDS).
– Ensure that only trusted ECS tasks and resources can access session storage by restricting permissions through IAM roles and security groups.

2. **Performance Tuning**:
– Cache session data effectively using low TTLs for Redis or Memcached.
– Monitor ElastiCache or RDS instance performance to prevent bottlenecks caused by session sharing.

3. **Scaling and Resilience**:
– Use multi-AZ configurations for Redis or RDS.
– Consider Redis Cluster for read/write scaling and high availability.

By offloading session management to centralized storage and using ECS best practices, your WordPress instances can efficiently share session information while scaling seamlessly.

Tweaking The Configuration

The cluster is not working exactly as expected.

One container will connect and appears to work properly, but the user experience will swap form a logged in page to a not logged in page mid-session. The assumption is that this is due to the user connection jumping to a different server in the container cluster.

Attempted Resolution: Set PHP session.save_handler to rediscluster

On the staging server the initial php session_save handler (set via environment variable) was set to redis.

Changing this to rediscluster did not change the session switching behavior.

Attempted Resolution: Revise the PHP session_start() call

In WordPress the session_start() was moved from the prior invocation in the WordPress init() hook to the muplugins_loaded hook which loads earlier in the process. This did not seem to have an impact on the issue. Some minor updates to deal with configurations using a Redis Cluster and not were made as well as ensuring we check if a session was already started.

Our Redis Cluster code, invoked during muplugins_loaded with a MySLP_RedisCluster::get_instance() call.

<?php
defined( 'MYSLP_VERSION' ) || exit;


/**
 *
 */
class RedisClusterSessionHandler implements SessionHandlerInterface {
	private $redis;

	public function __construct() {
		$redisClusterEndpoint = get_cfg_var( 'session.save_path' );
		if ( empty( $redisClusterEndpoint ) ) {
			throw new RuntimeException( 'No Redis Cluster endpoint configured' );
		}


		// Parse and extract host/port (handle both single node and cluster)
		$parsedUrl = parse_url( $redisClusterEndpoint );
		$redisHost = $parsedUrl['host'] ?? 'localhost';
		$redisPort = $parsedUrl['port'] ?? 6379;

		// Use an array format required by RedisCluster
		$redisClusterNodes = [ "$redisHost:$redisPort" ];

		try {
			// Initialize RedisCluster
			$this->redis = new RedisCluster( null, $redisClusterNodes, 5, 5, true );
		} catch ( RedisClusterException $e ) {
			throw new RuntimeException( 'Failed to connect to Redis Cluster: ' . $e->getMessage() );
		}

	}

	/**
	 * Initialize session
	 * @link https://php.net/manual/en/sessionhandlerinterface.open.php
	 *
	 * @param $savePath
	 * @param $sessionName
	 *
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function open( $savePath, $sessionName ): bool {
		return true; // No need to do anything here
	}

	/**
	 * Close the session
	 * @link https://php.net/manual/en/sessionhandlerinterface.close.php
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function close(): bool {
		return true; // No need to close anything explicitly
	}

	/**
	 * Read session data
	 * @link https://php.net/manual/en/sessionhandlerinterface.read.php
	 *
	 * @param $sessionId
	 *
	 * @return string <p>
	 * Returns an encoded string of the read data.
	 * If nothing was read, it must return false.
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function read( $sessionId ): string {
		$sessionData = $this->redis->get( "PHPREDIS_SESSION:$sessionId" );

		return $sessionData ?: ''; // Return session data or empty string if not found
	}

	/**
	 * Write session data
	 * @link https://php.net/manual/en/sessionhandlerinterface.write.php
	 *
	 * @param $sessionId
	 * @param string $data <p>
	 * The encoded session data. This data is the
	 * result of the PHP internally encoding
	 * the $_SESSION superglobal to a serialized
	 * string and passing it as this parameter.
	 * Please note sessions use an alternative serialization method.
	 * </p>
	 *
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function write( $sessionId, $data ): bool {
		return $this->redis->setex( "PHPREDIS_SESSION:$sessionId", 3600, $data ); // 1-hour TTL
	}

	/**
	 * Destroy a session
	 * @link https://php.net/manual/en/sessionhandlerinterface.destroy.php
	 *
	 * @param $sessionId
	 *
	 * @return bool <p>
	 * The return value (usually TRUE on success, FALSE on failure).
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function destroy( $sessionId ): bool {
		return $this->redis->del( [ "PHPREDIS_SESSION:$sessionId" ] ) > 0;
	}

	/**
	 * Cleanup old sessions
	 * @link https://php.net/manual/en/sessionhandlerinterface.gc.php
	 *
	 * @param $maxLifetime
	 *
	 * @return int|false <p>
	 * Returns the number of deleted sessions on success, or false on failure. Prior to PHP version 7.1, the function returned true on success.
	 * Note this value is returned internally to PHP for processing.
	 * </p>
	 * @since 5.4
	 */
	public function gc( $maxLifetime ): int|false {
		return true; // Redis handles expiration via TTL, so no need to do anything
	}
}

/**
 *
 */
class MySLP_RedisCluster extends MySLP_Base {
	private $redis;

	/**
	 * Catch cluster redirects (MOVED) using the built-in PHP RedisCluster lib
	 * @return void
	 * @throws RedisClusterException
	 */
	final function initialize() {
		$redisClusterEndpoint = get_cfg_var( 'session.save_path' );
		if ( class_exists( 'RedisCluster' ) && ! empty( $redisClusterEndpoint ) ) {
			try {
				$handler = new RedisClusterSessionHandler();
				session_set_save_handler( $handler, true );

			} catch ( RuntimeException $e ) {
				error_log( 'Error initializing RedisClusterSessionHandler: ' . $e->getMessage() );
			}
		}
		if ( ! session_id() && ! headers_sent() ) {
			session_start();
		}
	}
}

SaaS WP Login Processing

  • wp-login.php
    • $reauth = empty( $_REQUEST[‘reauth’] ) ? false : true; is set to false.
    • $user = wp_signon( array() , $secure_cookie “” )
      • do_action( ‘wp_login’ , $user->user_login “lcleveland” , $user “WP_User” is set)
    • if ( empty( $_COOKIE[ LOGGED_IN_COOKIE ] )) is NOT empty
      • LOGGED_IN_COOKIE is something like “wordpress_logged_in_e2ec4afff4940eebb6cd200cc8206825”
        which IS set on this session
    • $requested_redirect_to ==> ‘https://staging.storelocatorplus.com/wp-admin/”
      as set in $_REQUEST[‘redirect_to’]
    • if ( ! is_wp_error( $user ) && ! $reauth ) { // This is executing because user is set and reauth is not set.

Need to set the WP Secrets the same (keys and salts) on ALL nodes in the cluster that share login. The auth (login) cookies have salt and keys in them and with each server generating their own they will not be validated.

Docker has a method to pass these in via an ENV setting.

Image by Robert from Pixabay

Dynamic Site URL / Home URL

The SaaS platform uses internal processing hooks to auto-detect the hostname and update the WordPress data accordingly to change the site and home URL. This makes it easier to transfer the data set from production to staging and production. See the Creating A Development or Staging Database Copy post for more details on that process.

A fully qualified domain name (FQDN) example would be ‘dashboard.storelocatorplus.com’.
A uniform resource locator (URL) example would be ‘https://dashboard.storelocatorplus.com’.

Places site URL data is stored

Database Tables

  • wp_blogs
  • wp_options / wp_<site#>_options
    • option_name: siteurl set to the URL
    • option_name: home set to the URL
    • option_name: myslp_dashboard_theme_options , option_value array key ‘dashboard_site’ set to the URL
  • wp_site
    • record id: 1, domain column set to FQDN

Environment Variables

  • WP_HOME, value URL
  • WP_HOSTURL , value FQDN
  • WP_SITEURL, value URL

Dynamic URL Management

The SaaS platform uses the sunrise.php early-loading PHP code to set the domain from the web server provided $_SERVER[‘HTTP_HOST’]. It leverages the WordPress : home_url filter to set the URL for the site impacting WordPress functions such as get_rest_url() and home_url() among a dozen others.

The sunrise.php file will change home_url, site_url, and admin_url dynamically via WordPress filters.

Via MySLP Dashboard

\MySLP_Addon::updateUserBlogDomain()

Runs on the dashboard hook to WordPress : init

  • \MySLP_Addon::updateUserBlogDomain()
    • \MySLP_Addon::performUserBlogUpdates()
      • \MySLP_Addon::change_blog_urls_if_needed()

This will update the wp_<site#>_options entries for siteurl and home.

Via MySLP Dashboard Theme

The MySLP Dashboard theme contains some default URLs that are used to create links, including logout, recover password, etc. These options are stored in the wp_options table in the myslp_dashboard_theme_options option key under a subkey in option_value named dashboard_site.

User Profile (MySLP_User class)

Account Creation

\MySLP_DSRA::add_account()

Calls \MySLP_Recurring_Payments::add_payment_data()

Params include the user_id and the new payment data array:

Array
(
    [customer_details] => Array
        (
            [id] => cus_RcHfOPMYNHRgo7
            [object] => customer
            [address] => 
            [balance] => 0
            [created] => 1737310780
            [currency] => 
            [default_source] => 
            [delinquent] => 
            [description] => 
            [discount] => 
            [email] => test14@lancecleveland.com
            [invoice_prefix] => 2FDC81BB
            [invoice_settings] => Array
                (
                    [custom_fields] => 
                    [default_payment_method] => 
                    [footer] => 
                    [rendering_options] => 
                )

            [livemode] => 
            [metadata] => Array
                (
                )

            [name] => Lance Cleveland
            [next_invoice_sequence] => 1
            [phone] => 
            [preferred_locales] => Array
                (
                )

            [shipping] => 
            [tax_exempt] => none
            [test_clock] => 
        )

    [latest_response_code] => 200
    [coupon_code] => 
    [PROCESSOR] => stripe
    [PROFILEID] => sub_1Qj360BvHKfBw2LG0LkOaHbg
    [STATUS] => active
    [PROFILESTARTDATE] => 19 January 2025 18:19:40
    [NEXTBILLINGDATE] => 19 February 2025 18:19:40
    [LASTPAYMENTDATE] => 19 January 2025 18:19:40
    [LASTPAYMENTAMT] => 35
    [DESC] => Professional plan billed $35.00 per month
    [PLAN] => Professional
    [AMT] => 35
    [PERIOD] => month
    [IS_GOOD] => 1
    [status] => succeeded
    [referring_site] => 
)

Subscription Creation

\stripe\MySLP_Stripe_Payments::create_subscription()

Creates a Stripe Customer.
Creates a Stripe Subscription.

Returns a subscription data array

Array
(
    [customer_details] => Array
        (
            [id] => cus_RcHfOPMYNHRgo7
            [object] => customer
            [address] => 
            [balance] => 0
            [created] => 1737310780
            [currency] => 
            [default_source] => 
            [delinquent] => 
            [description] => 
            [discount] => 
            [email] => test14@lancecleveland.com
            [invoice_prefix] => 2FDC81BB
            [invoice_settings] => Array
                (
                    [custom_fields] => 
                    [default_payment_method] => 
                    [footer] => 
                    [rendering_options] => 
                )

            [livemode] => 
            [metadata] => Array
                (
                )

            [name] => Lance Cleveland
            [next_invoice_sequence] => 1
            [phone] => 
            [preferred_locales] => Array
                (
                )

            [shipping] => 
            [tax_exempt] => none
            [test_clock] => 
        )

    [latest_response_code] => 200
    [coupon_code] => 
    [PROCESSOR] => stripe
    [PROFILEID] => sub_1Qj360BvHKfBw2LG0LkOaHbg
    [STATUS] => active
    [PROFILESTARTDATE] => 19 January 2025 18:19:40
    [NEXTBILLINGDATE] => 19 February 2025 18:19:40
    [LASTPAYMENTDATE] => 19 January 2025 18:19:40
    [LASTPAYMENTAMT] => 35
    [DESC] => Professional plan billed $35.00 per month
    [PLAN] => Professional
    [AMT] => 35
    [PERIOD] => month
    [IS_GOOD] => 1
    [status] => succeeded
)

Subscription Details

Map Views

These are coming from $myslp->User->mapview_count.

This is coming from \MySLP_User::mapview_count which is managed with magic method setters and getters. The data is stored in the user_meta object within \MySLP_User in a key names “mapview_count”.

Incrementing Map View Counts

This count is updated whenever \MySLP_REST_API::get_map_options() is called (theoretically/assumed to be whenever the map is rendered).

Resetting Map View Count

This is reset to 0 via \myslp_extend_plan() within the myslp-dashboard-helpers module.

Called By
  • \MySLP_Dashboard_Controller::check_subscription()
    Runs when a subscription status is checked, has expired, and is renewed automatically.
    • \myslp_extend_plan()
  • \MySLP_Recurring_Payments::initialize()
    ONLY if payment type is PayPal…
    • \MySLP_Recurring_Payments::get_paypal_checkout_details()
      • \myslp_extend_plan()
  • WPSLP Hook personal_options_update or WPSLP Hook : edit_user_profile_update
    • \MySLP_Customer_Maintenance::update_myslp_profile_management()
      • \myslp_extend_plan()

Payment Info : \MySLP_User::recurring_payments

Your Sites

These are coming from the user meta “user_subscription_status” key as a subarray named “referer_urls”.

\MySLP_User::log_referer()

This adds to the list of referring URLs any time a map URL is requested.

If you look through the documentation (or code) for the slp_rest_geocode_invalid_referer hook, you will see that this is only called when running a geocoding request.

This means that up to the current 2501.XX.YY release of MySLP, the list of sites only shows sites where a geocoding request was called from. This is NOT necessarily all the sites that have requested that a map be displayed.

Used By

\SLP_BaseClass_Admin::enqueue_admin_javascript()

	/**
	 * Enqueues Admin JavaScript for include/admin.js and js/admin.js and js/admin-<tabname>-tab.js (minified if present)
	 *
	 * @used-by \SLP_BaseClass_Admin::add_hooks_and_filters     for WP Hook: admin_enqueue_scripts [10]
	 *
	 * @see https://docs.storelocatorplus.com/development/2022/07/13/debugging-add-locations-w-power-uncaught-syntaxerror-redeclaration-of-const-wp_data/
	 *
	 * @param string $hook
	 */

Generic admin.js script loading

Admin Tab Specific script loading

This loads up javascript files , if they exist, for specific tabs such as “info” or “experience”.

The tab name is simplified by stripping SLP_ADMIN_PAGEPRE . ‘slp_’ from the hook name for the page.

SLP_ADMIN_PAGEPRE is set in the main Store Locator Plus plugin loader.php file as

defined( 'SLP_ADMIN_PAGEPRE' ) || define( 'SLP_ADMIN_PAGEPRE', 'store-locator-plus_page_' );

As of 2501.06.01 this will also check and strip “toplevel_page_slp_” from the hook name to decide it is should enqueue a tab-specific script.

\SLP_BaseClass_Admin::ok_to_enqueue_admin_js()

	/**
	 * Check if it is OK to enqueue the admin JavaScript, assume so if the hook starts with our prefix.
	 *
	 * @param $hook
	 *
	 * @return boolean
	 */

Loops over the \SLP_Admin_UI::menu_items[] array which is an array of arrays where the key is the page key and the subarray contains the dislpay class, the hook name, label, etc. like this:

Called By

\SLP_BaseClass_Admin::enqueue_admin_javascript()

Saving/Changing Style ID

Saving/changing the Style ID (Smart Option: style_id) is what applies the pre-made styles from the Style Server (running on the main SLP site at the moment).

\SLP_SmartOptions::view_appearance()

This method defines the Smart Option for style_id.
It is a hidden field (changed with JS on the old Settings | View interface).
When changed it calls \SLP_SmartOptions::change_style_id() which directly calls \SLP_Style_Manager->change_style() to load the style changes from the style server.

This also sets up related style Smart Option which displays the “style_vision_list” vue based listing.

The now-defunct theme Smart Option is also set, but this is for legacy support and no longer used.

			$smart_options['theme']    = array(
				'type'    => 'hidden',
				'default' => 'a_gallery_style',
			);
			$smart_options['style_id'] = array(
				'type'              => 'hidden',
				'call_when_changed' => array( $this, 'change_style_id' ),
			);
			$smart_options['style']    = array(
				'type'       => 'style_vision_list',
				'show_label' => true,
				'wrapper'    => false,
				'vue'        => true,
			);
			$smart_

\SLP_Style_Manager::change_style_id()

Call stack when clicking “select style” on the legacy “style vision list”:

SLP_SmartOptions.php:320, SLP_SmartOptions->change_style_id()
SLP_SmartOptions.php:539, SLP_SmartOptions->execute_change_callbacks()
SLP_Admin_Settings.php:409, SLP_Admin_Settings->save_options()
SLP_Admin_UI.php:538, SLP_Admin_UI->setup_admin_screen()
class-wp-hook.php:324, WP_Hook->apply_filters()
class-wp-hook.php:348, WP_Hook->do_action()
plugin.php:517, do_action()
class-wp-screen.php:424, WP_Screen->set_current_screen()
screen.php:243, set_current_screen()
admin.php:212, {main}()

Call stack when clicking “activate” on the new style manager (React based) interface:

SLP_SmartOptions.php:320, SLP_SmartOptions->change_style_id()
SLP_SmartOptions.php:539, SLP_SmartOptions->execute_change_callbacks()
SLP_REST_Handler.php:719, SLP_REST_Handler->update_smart_option()
MySLP.php:266, MySLP->rest_dispatch_request_filter()
class-wp-hook.php:324, WP_Hook->apply_filters()
plugin.php:205, apply_filters()
class-wp-rest-server.php:1187, WP_REST_Server->respond_to_request()
class-wp-rest-server.php:1041, WP_REST_Server->dispatch()
class-wp-rest-server.php:431, WP_REST_Server->serve_request()
rest-api.php:424, rest_api_loaded()
class-wp-hook.php:324, WP_Hook->apply_filters()
class-wp-hook.php:348, WP_Hook->do_action()
plugin.php:565, do_action_ref_array()
class-wp.php:418, WP->parse_request()
class-wp.php:813, WP->main()
functions.php:1336, wp()
wp-blog-header.php:16, require()
index.php:17, {main}()

Style Caching

Only the Style Vision list currently caches style info by setting a transient via \SLP_Style_Manager::cache_style(), it does this as it renders each style response returned from the style server while rendering the vision list.

The cache is kept for one week.

The current style comes from a transient that caches the style server responses.

An example entry:

stdClass Object
(
    [id] => 67
    [date] => 2023-02-21T13:38:51
    [date_gmt] => 2023-02-21T18:38:51
    [guid] => stdClass Object
        (
            [rendered] => http://3.211.25.112/?post_type=slp_style_gallery&#038;p=67
        )

    [modified] => 2024-12-21T13:25:44
    [modified_gmt] => 2024-12-21T18:25:44
    [slug] => basic
    [status] => publish
    [type] => slp_style_gallery
    [link] => https://storelocatorplus.com/slp_style_gallery/basic/
    ...

\SLP_SmartOptions::style

This is the style vision list rendering.

This is also used to cache the style settings from the server by comparing \SLP_Style_Manager::current_style->slug versus the \SLP_SmartOptions::style value

Used By

  • \SLP_Style_Manager::style_matches_slug()
    • which is used by \SLP_Style_Manager::set_active_style()
      • which is used by \SLP_Style_Manager::apply_style()

\SLP_SmartOptions::style_id

The actual selected style_id.

Locator Style Architecture

This setting is stored in the options_nojs[‘style’] option setting.

It is managed by an instantiation of the SLP_Settings_style_vision_list object.

  • data_field: options_nojs[style]
  • label: ‘Locator Style’
  • type: ‘style_vision_list’

This is a “smart option” managed by SLP_SmartOptions.
It is defined in \SLP_SmartOptions:view_appearance()
It has a ‘vue’ object type for rendering assistance.

Stack Trace : SLP | Settings

Triggered selecting – Menu : Store Locator Plus® | Settings

Most recent to oldest…

  • SLP_Settings_card_list.php:42, SLP_Settings_card_list->display()
    • SLP_Settings_Group.php:88, SLP_Settings_Group->display()
      • SLP_Settings_Section.php:94, SLP_Settings_Section->display()
        • SLP_Settings.php:384, SLP_Settings->render_settings_page()
          • SLP_Admin_Settings.php:289, SLP_Admin_Settings->display()
  • SLP_Settings_style_vision_list.php:15, SLP_Settings_style_vision_list->get_items()
    • SLP_Settings_card_list.php:28, SLP_Settings_card_list->at_startup()
      • SLP_Setting.php:48, SLP_Setting->initialize()
        • SLP_Admin_Settings.php:213, SLP_Admin_Settings->add_view()

Showing The Currently Selected Style

\SLP_Settings_card_list::display()

Renders the list of styles.

When $this->value === $item->clean_title it marks it as selected.

$item is from the list of locator styles fetched from the SLP locator style service (see \SLP_Settings_style_vision_list->items below).

Stack Trace : setting the initial value of the ‘style’ property (option)

\SLP_Setting->set_value() appears to have value already set to “”.

Track down the SLP_SmartOptions and how it loads the default values from options/options_nojs into the Smart Option properties.

  • SLP_SmartOptions.php:1784, SLP_SmartOptions->set_valid_option()
    • SLP_SmartOptions.php:1832, SLP_SmartOptions->set_valid_options_nojs()
      • SLP_SmartOptions.php:1661, array_walk()
        • SLP_SmartOptions.php:1661, SLP_SmartOptions->slp_specific_setup()
          • SLP_SmartOptions.php:1513, SLP_SmartOptions->initialize_after_plugins_loaded() SLPlus.php:386
            • SLPlus->initialize_after_plugins_loaded()

Class : SLP_Setting

Methods

set_value()

Sets the value of a setting.

Fetches the value of a setting from \WPOption_Manager::get_wp_option() if necessary.

Class : SLP_Settings_card_list

Extends SLP_Setting.

SLP_Setting type: “style_vision_list”

Class: SLP_Settings_style_vision_list

Extends SLP_Settings_card_list.

Properties

items : SLP_Setting_item[]

Contains the array of styles retrieved from the SLP Locator Styles service (REST call to external service).

value

Contains the currently set/selected style for the user.

SaaS Hooks Calls

See Lance Cleveland’s WordPress Hooks Order Of Precedence.

Processing Modes

Base Loop

Generic WordPress + MySLP processing of a page request.

  • WordPress : muplugins_loaded
  • WordPress : registered_taxonomy
  • WordPress : registered_post_type
  • WordPress : plugins_loaded
  • WordPress : set_current_user
  • WordPress : init
    • SLP : slp_init_complete
      • SLP: start_slp_specific_setup
      • SLP: finish_slp_specific_setup
  • WordPress : parse_request
  • WordPress : rest_api_init
  • WordPress : send_headers

WordPress : plugins_loaded

Fires once activated plugins have loaded. The hook is generally used for immediate filter setup, or plugin overrides. The plugins_loaded action hook fires early, and precedes the setup_theme, after_setup_theme, init and wp_loaded action hooks.

Used By

  • MySLP_Dashboard_Controller :: load_hooks()
    • add_action( ‘plugins_loaded’, array( CLASS, ‘plugins_loaded’ ) );

WordPress : set_current_user

WordPress : init

Fires after WordPress has finished loading but before any headers are sent. Most of WP is loaded at this stage, and the user is authenticated.

Used By

  • Site_Architect::__construct()
    • add_action( ‘init’, array( $this, ‘sa_start_session’ ) );
  • SLP_Actions:initialize()
    • add_action( ‘init’, array( $this, ‘init’ ), 11 );

      Sets up the slplus->database object with slplus->database = SLP_Data::get_instance().
      This needs to happen AFTER the WoredPress:set_current_user is called so MySLP has the proper user meta to set the database attributes.
  • MySLP::add_hooks()
    • add_action( ‘init’, array( $this, ‘perform_init_actions’ ) );

User Logged In Traced Calls

  • Site_Architect::sa_start_session()
  • SLP_Actions::init()
  • MySLP::perform_init_actions()

SLP : slp_init_complete

Executed during WordPress : init hook via SLP_Actions::init() after all of the main SLP plugin init stuff has been executed.

Used By

  • \MySLP_Dashboard_Controller::load_hooks()
    • add_action( 'slp_init_complete', array( __CLASS__, 'slp_init_complete' ) );
  • SLP_BaseClass_Addon :: initialize()
    • add_action( ‘slp_init_complete’, array( $this, ‘slp_init’ ) );
  • SLP_Power_Pages :: add_hooks_and_filters()
    • add_action( ‘slp_init_complete’ , array( $this , ‘do_when_slp_ready’ ) , 11 ); // 11 ensures the base slp_init has run for the addon

WordPress : parse_request

Used By

  • WordPress : rest_api_loaded
    in wordpress/wp-includes/rest-api.php
    this eventually calls the REST custom endpoint permission_callback

WordPress : rest_api_init

Called when processing a REST API call.

slp_ajax_response

Used By

\SLP_Premier_AJAX::add_ajax_hooks()

Invoked By

\SLP_AJAX::renderJSON_Response()

slp_ajaxsql_fullquery

Used By

\SLP_Experience_AJAX::add_ajax_hooks()
\SLP_Premier_AJAX::add_ajax_hooks()

Invoked By

\SLP_AJAX::execute_location_query()

slp_rest_geocode_invalid_referer

Invoked By

\SLP_REST_Handler::valid_referer() for any site that is not coming from get_site_url() or get_home_url(). This is run via the WordPress REST API handler via the register_rest_route() for the readable request for geocoding at the wp-json/store-locator-plus/v2/geocode/<address> endpoint. This happens via the permission_callback property.

SmartOptions

Smart Options via the SLP_SmartOptions class (include/module/smartoptions/SLP_SmartOptions.php) handles nearly all of the settings (options in WordPress parlance) for the Store Locator Plus® plugin.

Methods

get_ValueOf( <property:string> )

Get the value of a Smart Option.

		
Check if a given property exists and return the value.		
@param string $property The property to check.
@return bool Returns true if the property exists and is not null, and its 'is_true' property is true. Otherwise, returns false.

is_true( <property:string)

Check if a given property is true.
@param string $property The property to check.
@return bool Returns true if the property exists and is not null, and its ‘is_true’ property is true. Otherwise, returns false.

if ( SLP_SmartOptions::get_instance()->is_true( 'use_territory_bounds' ) ) {
    $this->add_territory_bounds();
}

Architecture: Update Plugin

FOR INTERNAL USE ONLY

The process that happens when a plugin is updated, especially the conversion of pre-existing options to SLPlus options / options_nojs serialized JSON objects.

In older releases , including some vestiges that still existing in 2209.X versions, options were stored in a WordPress option_name that match the plugin name. Such as option_name ‘slp-power’ for the power add on. Each add on had its own set of options.

When Smart Options were introduced into the SLP architecture there were two options that stored “all settings”. csl-slplus-options (aka “options”) stores settings that were not being localized and sent to JavaScript. csl-slplus-options_nojs (aka “options_nojs”) are the settings that are not localized. (In theory… both of these will be replaced with a single monolithic store in csl-slplus-options eventually and specific settings retrieved from JavaScript via a REST call).

The idea was to eventually (soon?) convert all separate plugin-specific option settings like option_name = ‘slp-power’ into a standard csl-slplus-options string. Below is the flowchart that describes how that conversion happens.

Store Locator Plus® JavaScript Pub/Sub

Store Locator Plus® was updated with a “pub/sub” model a few years ago. This feature is a managed interface for jQuery Callbacks that allows disparate Javascript functions to interact with each other. It works much like hooks and filters in WordPress, but solely within the JavaScript runtime space.

The basic premise is any JavaScript function can call a “publish” method that fires off a stack of callbacks.

slp_Filter('slp_map_ready').publish(cslmap);

What is run by those callbacks is set by having a JavaScript module call a “subscribe” method.

slp_Filter( 'slp_map_ready' ).subscribe( this.initialize );