Introduction

The Scripts UI comes with a fully-fleshed API allowing developers to create their own script to run any kind of PHP scripts. Possibilities are unlimited:

  • Sending newsletters in batch
  • Performing maintenance on database
  • Export/Import data
  • Generate PDF files
  • etc…

Bootstrap

The Custom Script logic is similar to the native ACF Field Type API. First, you have to extends the acfe_script class and define the script settings in the initialize() method.

Then, you have to use the acfe_register_script() function to register your class. Here is a usage example:

if(!class_exists('my_acfe_script')):

class my_acfe_script extends acfe_script{
    
    /**
     * initialize
     */
    function initialize(){
        
        $this->name         = 'my_script';              // script slug
        $this->title        = 'My Script';              // script title
        $this->description  = 'My description';         // script description
        $this->recursive    = true;                     // recursive mode
        $this->category     = 'Custom';                 // script category
        $this->author       = 'Me';                     // script author
        $this->link         = 'https://www.domain.com'; // author's url
        $this->version      = '1.0';                    // script version
        $this->capability   = 'manage_options';         // required capability
        
    }
    
}

// register
acfe_register_script('my_acfe_script');

endif;

Finally, your class has to be saved in a PHP file and included within the acfe/init hook:

add_action('acfe/init', 'my_acfe_init');
function my_acfe_init(){
    
    // /wp-content/themes/my-theme/my-script.php
    require(get_stylesheet_directory() . '/my-script.php');
    
}

Recursive Mode

The recursive setting indicates if the script should spawn requests in an infinite loop until it is specifically stopped either by the user (stop button) or under specific conditions in the code.

This setting should be enabled when you expect to deal with a lot of data or posts, in order to make sure you never hit the server max execution time to finish your script.

For example, let’s say your PHP max execution time is set to 30, it means each request has 30 seconds to finish before being shutdown by the server. If you have to deal with thousands of posts, this limit will be most likely reached if you query all posts at once.

To avoid that, it is recommended to enable the recursive mode and split your query to get 10 posts. The next recursive request will take care of the next 10 posts, until all posts are treated.

The Script Flow

When the user start a script, the flow follows this logical order:

  • Start: This is the preparation phase which allows developer to gather global data and store it in a transient if needed. It is optional and can be skipped.
  • Request: This is the core of the script, where the job is actually done. The request will be run one time (if not recursive) or multiple times (if recursive).
  • Stop: This action is triggered either by the user (stop button) or by the code (if the script is finished). This phase allows developer to cleanup stored data if needed. It is also optional and can be skipped.

Non-recursive Example

Here is an example of a basic non-recursive script. The request() method will be called one time.

if(!class_exists('my_acfe_script')):

class my_acfe_script extends acfe_script{
    
    /**
     * initialize
     */
    function initialize(){
        
        $this->name  = 'my_script';
        $this->title = 'My Script';
        // script is non-recursive by default
        
    }
    
    
    /**
     * request
     */
    function request(){
    
        $posts = get_posts(array(
            'post_type'      => 'post',
            'posts_per_page' => -1, // get all posts
            'fields'         => 'ids',
        ));
        
        if($posts){
            foreach($posts as $post_id){
                
                $my_field = get_field('my_field', $post_id);
                // do something...
                
            }
        }
    
    }
    
}

// register
acfe_register_script('my_acfe_script');

endif;

Recursive Example

Here is an example of a basic recursive script. The request() method will be called in an infinite loop until the code explicitly says to stop, when all posts are treated.

We’ll split our requests to retrieve posts 10 per 10 and use an offset data to paginate our query. You can read more about WP_Query and offset here.

if(!class_exists('my_acfe_script')):

class my_acfe_script extends acfe_script{
    
    /**
     * initialize
     */
    function initialize(){
        
        $this->name      = 'my_script';
        $this->title     = 'My Script';
        $this->recursive = true;
        
        // setup a default offset
        $this->data = array(
            'offset' => 0
        );
        
    }
    
    
    /**
     * request
     */
    function request(){
    
        $posts = get_posts(array(
            'post_type'      => 'post',
            'posts_per_page' => 10,
            'fields'         => 'ids',
            'offset'         => $this->data['offset'], // retrieve offset
        ));
        
        // found posts
        if($posts){
            
            foreach($posts as $post_id){
                
                $my_field = get_field('my_field', $post_id);
                // do something...
                
            }
            
            // increment offset for next request
            $this->data['offset']++;
            
            // display a message
            // and go the next request()
            $this->send_response(array(
                'message' => '10 posts treated',
            ));
            
            // anything past this line is not executed because
            // $this->send_response() is equivalent to wp_send_json() with die()
            
        }
        
        // script finished
        // send stop event
        // this call the stop() method if it exists
        $this->send_response(array(
            'event' => 'stop',
        ));
    
    }
    
}

// register
acfe_register_script('my_acfe_script');

endif;

Using Start / Stop

The start() and stop() methods are always executed whenever the script starts and stops. By default, they only output a default “Start” and “Stop” message, notifying the user of the script manager status.

It is possible to use these methods to simply collect/cleanup data and leave the default message output. Or override the default message using $this->send_response() to notify more precisely the user on what is going on.

if(!class_exists('my_acfe_script')):

class my_acfe_script extends acfe_script{
    
    /**
     * initialize
     */
    function initialize(){
        
        $this->name  = 'my_script';
        $this->title = 'My Script';
        
    }
    
    
    /**
     * start
     */
    function start(){
        
        // collect data...
        
        // override default 'Start' message
        $this->send_response(array(
            'message' => 'Script started. Collecting data...',
            'status'  => 'success',
        ));
        
    }
    
    
    /**
     * request
     */
    function request(){
    
        // do something...
        
        // call the stop() method
        $this->send_response(array(
            'event' => 'stop',
        ));
    
    }
    
    
    /**
     * stop
     */
    function stop(){
        
        // cleanup data...
        
        // override default 'Stop' message
        $this->send_response(array(
            'message' => 'Script finished. Cleanup data.',
            'status'  => 'success',
        ));
        
    }
    
}

// register
acfe_register_script('my_acfe_script');

endif;

Using Shared Data

If you checked the Recursive Example, you probably saw the $this->data public variable usage. This variable allows to share data across start/request/stop events.

In order to use it, it is important to first define its structure with default values within the initialize() method. Note that $this->data is always an associative array.

/**
 * initialize
 */
function initialize(){
    
    $this->name  = 'my_script';
    $this->title = 'My Script';
    
    // setup data
    $this->data = array(
        'my_boolean' => false,   // cast boolean
        'my_int'     => 0,       // cast int
        'my_string'  => '',      // cast string
        'my_array'   => array(), // cast array
    );
    
}

It can then later be retrieved and modified in the start/request/stop methods. Note that ideally, each values should respect their initial types. So booleans should stay booleans, strings should stay strings etc…

/**
 * request
 */
function request(){
    
    // retrive my_boolean
    $my_boolean = $this->data['my_boolean'];

    // update my_boolean
    $this->data['my_boolean'] = true;

}

Using Script Statistics

The Script UI is shipped with a console-like event manager, displaying general statistics such as the number of events and the total time spent during the script execution.

Most advanced scripts will also display additional statistics such as “Items left” and “Total Items”. These provide useful information to the user in case of long-running tasks.

It is possible to interact with these statistics using the $this->stats['left'] and $this->stats['total'] public variables in any request. Note that unlike the Shared Data, these variables don’t have to be setup and can be just used out of the box.

/**
 * request
 */
function request(){
    
    // update items left stats
    $this->stats['left'] = 34;
    
    // update total items stats
    $this->stats['total'] = 200;

}

Providing these information will also automatically display a “Time Left” statistics, based on the time spent, items left and total items.

Using Custom Fields

One of the most powerful feature of the Script UI is the ability to use ACF Fields as Script Settings. These settings let the user customize the script behavior, allowing to create the most advanced scripts.

ACF Fields are registered just like with add_local_field_group(), but directly within the initialize() method.

/**
 * initialize
 */
function initialize(){
    
    $this->name  = 'my_script';
    $this->title = 'My Script';
    
    // register field groups
    $this->field_groups = array(
        
        array(
            'title'             => 'My Settings',
            'key'               => 'group_my_script_settings',
            'position'          => 'acf_after_title',
            'label_placement'   => 'top',
            'fields'            => array(
                
                array(
                    'name'        => 'my_field',
                    'type'        => 'text',
                    'placeholder' => 'Enter a text...',
                ),
    
            ),

        ),

    );
    
}

These fields values can then be retrieved in any start/request/stop method using the native get_field(), have_rows(), get_sub_field() functions.

/**
 * request
 */
function request(){
    
    // retrieve my_field value
    $my_field = get_field('my_field');

    // retrieve my_field value (unformatted)
    $my_field = get_field('my_field', false, false);

}

Here is a script example that use the previous Recursive Example, but with a setting allowing user to choose the number of posts to retrieve in each request.

if(!class_exists('my_acfe_script')):

class my_acfe_script extends acfe_script{
    
    /**
     * initialize
     */
    function initialize(){
        
        $this->name      = 'my_script';
        $this->title     = 'My Script';
        $this->recursive = true;
        
        // setup data
        $this->data = array(
            'offset' => 0
        );
        
        // register field groups
        $this->field_groups = array(
            array(
                'title'             => 'Script Settings',
                'key'               => 'group_my_script_settings',
                'position'          => 'side',
                'label_placement'   => 'top',
                'fields'            => array(
            
                    array(
                        'label'         => 'Posts Per Request',
                        'name'          => 'posts_per_request',
                        'type'          => 'number',
                        'instructions'  => 'Number of posts for each request',
                        'required'      => true,
                        'min'           => 1,
                        'max'           => 500,
                        'default_value' => 10,
                    ),
        
                ),
            ),
        );
        
    }
    
    
    /**
     * request
     */
    function request(){
        
        // retrieve field setting
        $posts_per_request = get_field('posts_per_request');
    
        $posts = get_posts(array(
            'post_type'      => 'post',
            'posts_per_page' => $posts_per_request, // use setting
            'fields'         => 'ids',
            'offset'         => $this->data['offset'],
        ));
        
        // found posts
        if($posts){
            
            foreach($posts as $post_id){
                
                $my_field = get_field('my_field', $post_id);
                // do something...
                
            }
            
            // increment offset for next request
            $this->data['offset']++;
            
            // send message
            $this->send_response(array(
                'message' => count($posts) . ' posts treated',
            ));
            
        }
        
        // script finished
        $this->send_response(array(
            'event' => 'stop',
        ));
    
    }
    
}

// register
acfe_register_script('my_acfe_script');

endif;

Using Pause

It is possible to pause the script programmatically from the code, allowing the user to perform some check before resuming the script via the “Play” button.

To do so we have to send a pause event with $this->send_response().

/**
 * request
 */
function request(){
    
    // pause script
    $this->send_response(array(
        'message' => 'Script paused',
        'event'   => 'pause',
        'status'  => 'warning',
    ));

}

Using Confirm

An another advanced feature is the ability to pause the script from the code and wait for the user to confirm or cancel an action before processing the next request. This allows developers to ask for user input when dealing with sensitive data.

To ask a confirmation, we have to send a confirm event thru $this->send_response(). The next request() will receive the answer in the $this->confirm public variable.

Here are the different states of $this->confirm:

  • null – default state (confirmation not asked)
  • true – user confirmed the action
  • false – user canceled the action
/**
 * request
 */
function request(){
    
    // default state
    // confirm not asked yet
    if($this->confirm === null){
        
        $this->send_response(array(
            'message' => 'Are you sure you want to proceeed?',
            'event'   => 'confirm',
            'status'  => 'warning',
        ));
        
    // confirm is either true or false
    }else{
        
        // confirmed
        if($this->confirm === true){
            // do something
        }
        
        // in all cases
        // reset confirm to default state
        $this->confirm = null;
        
    }

}

Using Debug

It is possible to send debug data alongside with a message to provide additional data to the user during the script execution. To do so we have to use the debug argument with $this->send_response().

Data will be automatically wrapped inside a modal that can be opened using a “Debug” link next to the event message.

/**
 * request
 */
function request(){
    
    // send message with debug data
    $this->send_response(array(
        'message' => 'Request performed',
        'debug'   => array(
            'posts' => array(12, 45, 84),
            'users' => array(54, 122),
        ),
    ));

}

It is also possible to send a link alongside with a message.

/**
 * request
 */
function request(){
    
    // send message with a link
    $this->send_response(array(
        'message' => 'Request performed',
        'link'   => array(
            'text' => 'https://www.domain.com'
        ),
    ));
    
    // send message with multiple links
    $this->send_response(array(
        'message' => 'Request performed',
        'link'   => array(
            'https://www.domain.com',
            'https://www.another-domain.com',
        ),
    ));
    
    // send message with multiple links
    $this->send_response(array(
        'message' => 'Request performed',
        'link'   => array(
            array(
                'text'   => 'click here',
                'href'   => 'https://www.domain.com',
                'target' => '_blank',
            ),
            array(
                'text'     => 'download file',
                'href'     => 'https://www.domain.com/file.json',
                'download' => 'file',
            ),
        ),
    ));

}

Using Index

Sometimes it might be useful to retrieve the current execution number when using the recursive mode. This information can be retrieved using the $this->index public variable in any request.