Imagify — Engineering Deep Dive

Complete process-level reference for plugin engineers. Class hierarchies, call flows, DB schemas, hook signatures, API structures, and concurrency details.

Version 2.2.8 PHP 7.3+ · WordPress 5.3+ PSR-4 · League Container · ActionScheduler
🏗️

1. Architecture & Bootstrapping

Entry point, constants, DI container, service provider chain, and init sequence.

Entry Point — imagify.php

WordPress loads imagify.php during the plugins_loaded phase. It defines all constants and registers activation/deactivation hooks before delegating to inc/main.php.

// imagify.php — constants defined at plugin load time
define( 'IMAGIFY_VERSION',        '2.2.8' );
define( 'IMAGIFY_SLUG',           'imagify' );
define( 'IMAGIFY_FILE',           __FILE__ );
define( 'IMAGIFY_PATH',           realpath( plugin_dir_path( IMAGIFY_FILE ) ) . '/' );
define( 'IMAGIFY_URL',            plugin_dir_url( IMAGIFY_FILE ) );
define( 'IMAGIFY_ASSETS_IMG_URL', IMAGIFY_URL . 'assets/images/' );
define( 'IMAGIFY_MAX_BYTES',      5242880 );   // 5 MB hard limit per image
define( 'IMAGIFY_INT_MAX',        PHP_INT_MAX - 30 );
define( 'IMAGIFY_SITE_DOMAIN',    'https://imagify.io' );
define( 'IMAGIFY_APP_DOMAIN',     'https://app.imagify.io' );
define( 'IMAGIFY_APP_API_URL',    IMAGIFY_APP_DOMAIN . '/api/' );

Bootstrap Sequence

plugins_loaded imagify_init() vendor/autoload.php new Plugin(Container, args) Plugin::init($providers) do_action('imagify_loaded')

imagify_init() lives in inc/main.php. It skips execution if DOING_AUTOSAVE is defined. The Plugin class (classes/Plugin.php) receives a League\Container instance and the plugin path, then orchestrates the full init sequence:

// classes/Plugin.php — init sequence (abridged)
public function init( array $providers ): void {
    // 1. Register shared services
    $this->container->addShared( 'event_manager', fn() => new EventManager() );
    $this->container->addShared( 'filesystem',    fn() => new Imagify_Filesystem() );

    // 2. Include procedural files (functions/, common/, 3rd-party/)
    $this->include_files();

    // 3. Init legacy singletons
    Imagify_Auto_Optimization::get_instance()->init();
    Imagify_Options::get_instance()->init();
    Imagify_Data::get_instance()->init();
    Imagify_Folders_DB::get_instance()->init();
    Imagify_Files_DB::get_instance()->init();
    Imagify_Cron_Library_Size::get_instance()->init();
    Imagify_Cron_Rating::get_instance()->init();
    Imagify_Cron_Sync_Files::get_instance()->init();
    Imagify\Auth\Basic::get_instance()->init();
    Imagify\Job\MediaOptimization::get_instance()->init();
    Bulk::get_instance()->init();

    // 4. Admin-only classes
    if ( is_admin() ) { ... }

    // 5. Register PSR-4 service providers + subscribers
    foreach ( $providers as $service_provider ) {
        $this->container->addServiceProvider( new $service_provider() );
        $this->load_subscribers( $provider_instance );
    }

    do_action( 'imagify_loaded', $this );
}

Activation / Deactivation Hooks

HookHandlerWhat it does
register_activation_hookimagify_set_activation()Sets transient imagify_activation with current user ID (TTL 30s). On network: set_site_transient.
register_deactivation_hookimagify_deactivation()Deletes imagify_check_api_version and imagify_check_licence_1 site transients; fires imagify_deactivation action.
init (Plugin)Plugin::maybe_activate()Reads activation transient; fires imagify_activation action with user ID, then deletes transient.

Service Providers (config/providers.php)

Imagify\User\ServiceProvider

Registers User singleton; binds account/quota service.

Imagify\Admin\ServiceProvider

AdminBar, PluginFamily, AdminSubscriber.

Imagify\Avif\ServiceProvider

AVIF rewrite-rule writers for Apache/Nginx/IIS.

Imagify\CDN\ServiceProvider

CDN push integration.

Imagify\Picture\ServiceProvider

Registers Picture\Display subscriber for <picture> tag rewriting.

Imagify\Stats\ServiceProvider

Stat counters (e.g. OptimizedMediaWithoutNextGen).

Imagify\Webp\ServiceProvider

WebP rewrite-rule writers.

Imagify\ThirdParty\ServiceProvider

GravityForms, Extendify; loads all inc/3rd-party/ integrations.

Imagify\Media\ServiceProvider

Media subscribers, upload handler.

Imagify\Tools\ServiceProvider

Reset internal state tool, troubleshooting subscriber.

📦

2. Namespace & PSR-4 Structure

Composer autoload map, directory layout, and naming conventions.

PSR-4 Autoload Map (composer.json)

Namespace prefixDirectoryNotes
Imagify\classes/Primary PSR-4 root for all modern classes
Imagify\Deprecated\Traits\inc/deprecated/Traits/Backward compat trait shims
Imagify\ThirdParty\AS3CF\inc/3rd-party/amazon-s3-and-cloudfront/classes/S3 Offload integration
Imagify\ThirdParty\EnableMediaReplace\inc/3rd-party/enable-media-replace/classes/Enable Media Replace compat
Imagify\ThirdParty\FormidablePro\inc/3rd-party/formidable-pro/classes/Formidable Forms compat
Imagify\ThirdParty\NGG\inc/3rd-party/nextgen-gallery/classes/NextGEN Gallery integration
Imagify\ThirdParty\RegenerateThumbnails\inc/3rd-party/regenerate-thumbnails/classes/Regenerate Thumbnails compat
Imagify\ThirdParty\WPRocket\inc/3rd-party/wp-rocket/classes/WP Rocket compat

Classmap (legacy, non-PSR-4)

inc/classes/ and inc/deprecated/classes/ are loaded via Composer classmap. The convention is class-imagify-{name}.phpImagify_{Name}. Two files are explicitly excluded from the classmap: class-imagify-plugin.php and class-imagify-requirements-check.php (loaded manually before autoloader is available).

Key classes/ Sub-namespaces

Imagify\Bulk\

Bulk, BulkInterface, AbstractBulk, WP, CustomFolders, Noop

Imagify\CLI\

AbstractCommand, BulkOptimizeCommand, RestoreCommand, GenerateMissingNextgenCommand

Imagify\Context\

ContextInterface, AbstractContext, WP, CustomFolders, Noop

Imagify\Media\

MediaInterface, AbstractMedia, WP, CustomFolders, Noop

Imagify\Optimization\

File, Process\{AbstractProcess, WP, CustomFolders, Noop}, Data\{AbstractData, WP, CustomFolders, Noop}

Imagify\Picture\

Display (output buffer rewriter)

Imagify\Job\

MediaOptimization (background queue worker)

Imagify\Tools\

InternalStateList, ResetInternalState, Subscriber

Imagify\Traits\

InstanceGetterTrait (lightweight singleton), MediaRowTrait

ℹ️ InstanceGetterTrait provides static::get_instance(): static — a static singleton factory used by both PSR-4 classes and legacy Imagify_* classes. It stores the instance in static::$_instance.
⚙️

3. Optimization Process — Class Hierarchy & Call Flow

Full inheritance chain from context factory to per-file API call.

Class Inheritance Chain

ProcessInterface                          // classes/Optimization/Process/ProcessInterface.php
  └── AbstractProcess                    // classes/Optimization/Process/AbstractProcess.php (~2100 lines)
        ├── Process\WP                   // WP Media Library context
        ├── Process\CustomFolders        // Custom Folders context
        └── Process\Noop                 // No-op fallback

DataInterface                             // classes/Optimization/Data/DataInterface.php
  └── AbstractData
        ├── Data\WP                      // stores _imagify_data postmeta
        ├── Data\CustomFolders           // stores in imagify_files table
        └── Data\Noop

MediaInterface                            // classes/Media/MediaInterface.php
  └── AbstractMedia
        ├── Media\WP
        ├── Media\CustomFolders
        └── Media\Noop

ContextInterface                          // classes/Context/ContextInterface.php
  └── AbstractContext
        ├── Context\WP
        ├── Context\CustomFolders
        └── Context\Noop

Context Factory Functions

// inc/functions/common.php
imagify_get_context( string $context ): ContextInterface
imagify_get_optimization_process( int $media_id, string $context ): ProcessInterface

// Context values: 'wp' | 'custom-folders' | 'ngg' (when NGG active)
// Filterable via: imagify_context_class_name, imagify_process_class_name

AbstractProcess — Key Method Signatures

MethodSignatureDescription
__construct(int|WP_Post|MediaInterface $id)Accepts attachment ID, WP_Post, or MediaInterface object
optimize(?int $optimization_level, array $args = []): bool|WP_ErrorMain entry for single-media optimization; acquires lock, iterates sizes
reoptimize(?int $optimization_level, array $args = []): bool|WP_ErrorRestore then re-optimize at new level
optimize_sizes(array $sizes, ?int $level, array $args = []): bool|WP_ErrorPush sizes to background job queue
optimize_size(string $size, ?int $level): bool|WP_ErrorOptimize a single named size (e.g. 'full', 'thumbnail')
optimize_missing_thumbnails(): bool|WP_ErrorFind and optimize sizes missing from postmeta
restore(): bool|WP_ErrorRestore all sizes from backup; acquires restoring lock
delete_backup(): bool|WP_ErrorRemove backup files for this media
generate_nextgen_versions(): bool|WP_ErrorGenerate WebP/AVIF variants for all optimized sizes
delete_nextgen_files(bool $keep_full = false, bool $all_next_gen = false): voidRemove WebP/AVIF sidecar files
lock(string $action = 'optimizing'): voidSet transient lock for 10 minutes
unlock(): voidDelete lock transient
is_locked(): string|falseReturns lock action string or false
update_size_optimization_data(object $response, string $size, int $level): voidPersist API response data for a size

Optimization Call Flow — Single Media

AbstractProcess::optimize() lock('optimizing') get_sizes_to_optimize() optimize_sizes($sizes, $level) MediaOptimization::push_to_queue() optimize_size($size) File::optimize($args) upload_imagify_image() Imagify API POST /upload/ download_url(response->image) filesystem->move() update_size_optimization_data() unlock()

Per-Size Data Structure Stored

// Stored in _imagify_data['sizes'][$size_name] (WP context)
// On success:
[
    'success'        => true,
    'original_size'  => int,   // bytes before optimization
    'optimized_size' => int,   // bytes after optimization
    'percent'        => float, // savings percentage (2 decimal places)
]

// On error:
[
    'success' => false,
    'error'   => string,  // human-readable error message
]
📄

4. Optimization\File — Method Signatures

Low-level file operations: validation, resize, backup, API call, next-gen path generation.

Class: Imagify\Optimization\Fileclasses/Optimization/File.php (931 lines).

Injected with Imagify_Filesystem::get_instance(). Does not extend anything — purely compositional.

Constructor & Properties

class File {
    protected string             $path;       // absolute path to file
    protected ?bool              $is_image;   // cached result of is_image()
    protected ?object           $file_type;  // {ext, type} from wp_check_filetype()
    protected Imagify_Filesystem $filesystem;
    protected mixed              $editor;    // WP_Image_Editor_Imagick|WP_Image_Editor_GD|WP_Error
    protected array             $options;   // cached get_imagify_option() calls

    public function __construct( string $file_path ) {...}
}

Public Methods

MethodParameters → ReturnNotes
is_valid()→ boolReturns true if $path is non-empty
can_be_processed()→ true|WP_ErrorChecks: path not empty, filesystem no errors, file exists, is a file, file writable, parent dir writable
optimize(array $args)→ stdClass|WP_ErrorCalls backup(), then upload_imagify_image(), downloads result, moves to destination
resize(array $dimensions, int $max_width)→ string|WP_ErrorResizes via WP_Image_Editor; corrects EXIF orientation (cases 2–8); returns temp path
create_thumbnail(array $destination)→ bool|array|WP_ErrorCalls $editor->multi_resize(); moves to destination path
backup(?string $backup_path, ?string $backup_source)→ true|false|WP_ErrorCopies file to backup_path; also copies -scaled variant if exists
is_exceeded()→ boolReturns true if file size > IMAGIFY_MAX_BYTES (5 MB)
is_supported(array $allowed_mime_types)→ boolChecks MIME type against allow-list
is_image()→ boolMIME type starts with image/
is_pdf()→ boolMIME type is application/pdf
is_webp()→ boolRegex: @(?!^|/|\)\.webp$@i — rejects bare .webp
is_avif()→ boolSame pattern for .avif
get_path()→ stringCurrent absolute path (may change post-conversion)
get_path_to_webp()→ string|falseAppends .webp to path; false if not an image or already WebP
get_path_to_nextgen(string $format)→ string|falseAppends .webp or .avif; false if already next-gen
get_mime_type()→ stringFrom cached wp_check_filetype()
get_extension()→ string|falseFile extension without dot
get_dimensions()→ array{width:int, height:int}Returns [0,0] if not image

optimize() — Args Array

optimize( [
    'backup'             => true,    // false = skip backup regardless of user setting
    'backup_path'        => null,    // string — explicit backup destination path
    'backup_source'      => null,    // string — source to backup (WP 5.3+ original)
    'optimization_level' => 0,       // 0=normal/lossless, 1=aggressive, 2=ultra
    'convert'            => '',      // 'webp' | 'avif' | '' for original format
    'context'            => 'wp',   // sent to API for logging
    'original_size'      => 0,       // bytes, sent to API
] );
🌐

5. API Client — Endpoints & Request/Response

HTTP transport, authentication, all endpoints, response schema, error handling.

Class: Imagify (legacy classmap) — inc/classes/class-imagify.php. Singleton via InstanceGetterTrait.

Base URL: IMAGIFY_APP_API_URL = https://app.imagify.io/api/

Authentication

// Headers set in __construct() using stored API key
$this->all_headers['Accept']        = 'Accept: application/json';
$this->all_headers['Content-Type']  = 'Content-Type: application/json';
$this->all_headers['Authorization'] = 'Authorization: token ' . $this->api_key;

// upload_image() sends only Authorization header (multipart/form-data via cURL)
// All other endpoints send all three headers

Transport Strategy

The private http_call() method auto-selects transport: if $args['post_data']['image'] is set, it routes to curl_http_call() (direct cURL for multipart file uploads); otherwise uses WordPress wp_remote_request(). A pre_imagify_request filter allows short-circuiting the cURL path.

All Endpoints

MethodEndpointHTTPBody / Response
get_user()users/me/GETJSON: {id, email, plan_id, plan_label, quota, extra_quota, extra_quota_consumed, consumed_current_month_quota, next_date_update, is_active, is_monthly}
create_user($data)users/POSTJSON body; no auth header
update_user($data)users/me/PUTJSON body with all headers
get_status($data)status/{$data}/GETCached in static array per type
get_api_version()version/GET5s timeout; cached in site transient
get_public_info()public-infoGETMarketing/public plan info
upload_image($data)upload/POST (cURL multipart)$data = ['image' => $path, 'data' => json_encode($opts)]. Response: {image: $url, ...}
fetch_image($data)fetch/POST (JSON)Optimize image from URL; same response shape as upload
get_plans_prices()pricing/plan/GETPlan pricing objects
get_all_prices()pricing/all/GETAll pricing including packs
check_coupon_code($coupon)coupons/{$coupon}/GETCoupon validity response
check_discount()pricing/discount/GETActive discount info

Upload Request Body (multipart via cURL)

// $data array passed to upload_image()
[
    'image' => '/absolute/path/to/image.jpg',  // CURLFile in cURL transport
    'data'  => json_encode([
        'normal'        => true/false,  // level === 0
        'aggressive'    => true/false,  // level === 1
        'ultra'         => true/false,  // level === 2
        'keep_exif'     => true,
        'original_size' => int,
        'context'       => string,     // 'wp' | 'custom-folders' | 'ngg'
        'convert'       => string,     // 'webp' | 'avif' — only when converting
    ]),
]

API Response Shape (upload/fetch)

// Success — stdClass
{
    "image": "https://app.imagify.io/...temp_url...",  // URL for download_url()
    "original_size": 123456,
    "new_size": 98765,
    "percent": 19.87,
    // "message" key present when conversion was not possible (falls back to original)
}

// Error — WP_Error with code 'error {http_code}'
// HTTP 401 → invalid API key
// HTTP 413 → file too large
// HTTP 4xx/5xx → $response->detail or $response->image error array

HTTP Response Handling

private function handle_response( string $response, int $http_code, string $error = '' ) {
    $response = json_decode( $response );          // stdClass or null
    if ( 200 !== $http_code && !empty( $response->code ) ) {
        // $response->detail → WP_Error message
        // $response->image  → array of field errors
        return new WP_Error( 'error ' . $http_code, ... );
    }
    if ( ! is_object( $response ) ) {
        return new WP_Error( 'not_valid_json', ... );
    }
    return $response;
}
⚠️ Timeout defaults: get_user() and get_status() use 10s. get_api_version() uses 5s. All other calls default to 45s. Filterable via imagify_api_http_request_timeout.
🗄️

6. WordPress Postmeta Keys & Data Structures

Every key written to wp_postmeta by Imagify, with types and full schemas.

Primary Metadata Keys (WP Media Library)

All stored on the attachment post (post_type = attachment). Managed by Imagify\Optimization\Data\WP.

Meta KeyTypeValues / Schema
_imagify_data Serialized array ['sizes' => [...], 'stats' => [...], 'message' => string]
See schema below.
_imagify_status string 'success' | 'already_optimized' | error string | '' (not optimized)
_imagify_optimization_level int (stored as string) 0 = normal/lossless, 1 = aggressive, 2 = ultra/smart

_imagify_data Full Schema

// _imagify_data serialized array structure
[
    'sizes'  => [
        'full'      => [ 'success' => true, 'original_size' => int, 'optimized_size' => int, 'percent' => float ],
        'thumbnail' => [ 'success' => true, ... ],
        'medium'    => [ 'success' => false, 'error' => string ],
        // ... one entry per registered image size + any custom sizes
    ],
    'stats'  => [
        'original_size'  => int,    // sum across all successful sizes
        'optimized_size' => int,    // sum across all successful sizes
        'percent'        => float,  // aggregate % (2 decimal places)
    ],
    'message' => string,           // optional message from API (e.g. already optimized)
]
ℹ️ _imagify_status and _imagify_optimization_level are written only when the 'full' size is updated. They act as top-level fast-access keys mirroring _imagify_data['sizes']['full'].

Standard WordPress Keys (also modified by Imagify)

Meta KeyModified When
_wp_attachment_metadataAfter resize (adds/removes sizes array entries); after thumbnail generation (updates width/height); after WP 5.3 original file handling
_wp_attached_fileNot directly modified; read to resolve absolute paths
⚙️

7. Settings — Option Keys, Types & Defaults

All values stored under a single serialized option. Class: Imagify_Options (inc/classes/class-imagify-options.php).

Option name: imagify_settings (single site) or imagify_settings stored via get_site_option (network). Set via get_imagify_option($key) / update_imagify_option($key, $value).

KeyTypeDefaultReset ValueDescription
api_keystring''Imagify API key. Overridable via PHP constant IMAGIFY_API_KEY.
optimization_levelint220=lossless, 1=aggressive, 2=ultra
losslessint (bool)0Force level 0 for all optimizations
auto_optimizeint (bool)01Auto-optimize on upload
backupint (bool)01Keep backup of originals
resize_largerint (bool)01 (if WP 5.3+)Resize images larger than threshold
resize_larger_wint0From big_image_size_threshold filter (default 2560)Max width in pixels for resize
display_nextgenint (bool)0Enable next-gen format delivery
display_nextgen_methodstring'picture''picture' = HTML rewrite; 'rewrite' = server-side rules
display_webpint (bool)0Legacy WebP delivery toggle
display_webp_methodstring'picture'Legacy WebP method selector
cdn_urlstring''CDN base URL for URL→path resolution
disallowed-sizesarray[]Size names excluded from optimization
admin_bar_menuint (bool)11Show Imagify in admin bar
partner_linksint (bool)01Show partner links in plugin UI
convert_to_avifint (bool)0Generate AVIF sidecar files
convert_to_webpint (bool)0Generate WebP sidecar files
optimization_formatstring'webp''webp' | 'avif' | 'off'
ℹ️ The reset_values array in Imagify_Options contains only keys that differ from defaults; it is applied on first install or explicit reset. The option is a single serialized blob — never stored as individual keys.
🗃️

8. Database Schemas

Complete DDL for all three custom tables: imagify_folders, imagify_files, and ngg_imagify_data.

imagify_folders

Class: Imagify_Folders_DB (inc/classes/class-imagify-folders-db.php). Global table in multisite ($wpdb->base_prefix). Table version: 100.

CREATE TABLE `{prefix}imagify_folders` (
    `folder_id`  bigint(20) unsigned NOT NULL auto_increment,
    `path`       varchar(191)        NOT NULL default '',
    `active`     tinyint(1) unsigned NOT NULL default 0,
    PRIMARY KEY  (folder_id),
    UNIQUE KEY   path (path),
    KEY          active (active)
);
ColumnTypeDescription
folder_idbigint unsigned PKAuto-increment primary key
pathvarchar(191) UNIQUEAbsolute path with placeholder: {{ROOT}}/wp-content/uploads/gallery/. Uses {{ROOT}} and {{ABSPATH}} tokens for portability.
activetinyint(1)1 = selected in settings; 0 = deactivated. Indexed for fast active-folder queries.

imagify_files

Class: Imagify_Files_DB (inc/classes/class-imagify-files-db.php). Global table in multisite. Table version: 102.

CREATE TABLE `{prefix}imagify_files` (
    `file_id`            bigint(20) unsigned  NOT NULL auto_increment,
    `folder_id`          bigint(20) unsigned  NOT NULL default 0,
    `file_date`          datetime             NOT NULL default '0000-00-00 00:00:00',
    `path`               varchar(191)         NOT NULL default '',
    `hash`               varchar(32)          NOT NULL default '',   -- MD5 of file
    `mime_type`          varchar(100)         NOT NULL default '',
    `modified`           tinyint(1) unsigned  NOT NULL default 0,
    `width`              smallint(2) unsigned NOT NULL default 0,
    `height`             smallint(2) unsigned NOT NULL default 0,
    `original_size`      int(4) unsigned      NOT NULL default 0,
    `optimized_size`     int(4) unsigned      default NULL,
    `percent`            smallint(2) unsigned default NULL,
    `optimization_level` tinyint(1) unsigned  default NULL,
    `status`             varchar(20)          default NULL,
    `error`              varchar(255)         default NULL,
    `data`               longtext             default NULL,   -- serialized, see below
    PRIMARY KEY  (file_id),
    UNIQUE KEY   path (path),
    KEY          folder_id (folder_id),
    KEY          optimization_level (optimization_level),
    KEY          status (status),
    KEY          modified (modified)
);
ColumnNotes
folder_idFK reference to imagify_folders.folder_id (not enforced at DB level)
pathAbsolute path using same {{ROOT}} tokens as folders table
hashMD5 hash of file contents — used by refresh_file() to detect modifications
modified1 when file has changed since last optimization (hash mismatch)
status'success' | 'already_optimized' | 'error' | NULL (not yet processed)
dataSerialized array — same shape as _imagify_data (sizes + stats)

ngg_imagify_data (NextGEN Gallery)

Class: Imagify\ThirdParty\NGG\DB (inc/3rd-party/nextgen-gallery/classes/DB.php). Per-site table ($wpdb->prefix). Table version: 100.

CREATE TABLE `{prefix}ngg_imagify_data` (
    `data_id`            bigint(20) unsigned NOT NULL auto_increment,
    `pid`                bigint(20) unsigned NOT NULL default 0,
    `optimization_level` varchar(1)          NOT NULL default '',
    `status`             varchar(30)         NOT NULL default '',
    `data`               longtext            default NULL,
    PRIMARY KEY  (data_id),
    KEY          pid (pid)
);

pid is the NextGEN picture ID. data is serialized the same way as _imagify_data.

Abstract DB Base Class

All three DB classes extend Imagify_Abstract_DB which implements Imagify\DB\DBInterface. It provides:

Table Management

  • maybe_upgrade_table() — create/upgrade on plugin init
  • create_table() — issues dbDelta()
  • can_operate(): bool — true when table is ready
  • Version stored in option: {option_prefix}_db_version

CRUD Methods

  • get($id), get_by($col, $val), get_in($col, $vals)
  • get_var($col, $where), get_column_in($col, $ids)
  • insert($data), update($data, $where), delete($id)
  • Auto-serialize array columns before insert/update via serialize_columns()
  • Auto-cast results via cast_row() based on column type map
🚀

9. Bulk Optimization — ActionScheduler Integration

How bulk jobs are enqueued, tracked, and completed via ActionScheduler async actions.

Class: Imagify\Bulk\Bulk (classes/Bulk/Bulk.php). Singleton. Registered hooks in init().

Bulk Run Flow

AJAX: imagify_bulk_optimize bulk_optimize_callback() run_optimize($context, $level) get_unoptimized_media_ids() as_enqueue_async_action() ×N set_transient 'running' ActionScheduler fires 'imagify_optimize_media' optimize_media($id, $ctx, $lvl) check_optimization_status()

ActionScheduler Job Enqueue

// Bulk::run_optimize() — one as_enqueue_async_action() per media
as_enqueue_async_action(
    'imagify_optimize_media',
    [
        'id'      => (int) $media_id,
        'context' => (string) $context,      // 'wp' | 'custom-folders'
        'level'   => (int) $optimization_level,
    ],
    "imagify-{$context}-optimize-media"   // group name — allows cancellation per context
);

// Next-gen generation uses a separate hook
as_enqueue_async_action(
    'imagify_convert_next_gen',
    [ 'id' => $media_id, 'context' => $context ],
    "imagify-{$context}-convert-nextgen"
);

Progress Tracking Transients

TransientSet WhenShapeTTL
imagify_wp_optimize_runningStart of WP library bulk run['total' => int, 'remaining' => int]DAY_IN_SECONDS
imagify_custom-folders_optimize_runningStart of custom-folders bulk run['total' => int, 'remaining' => int]DAY_IN_SECONDS
imagify_bulk_optimization_resultAfter each successful optimization['total' => int, 'original_size' => int, 'optimized_size' => int]DAY_IN_SECONDS
imagify_bulk_optimization_completeWhen remaining reaches 01DAY_IN_SECONDS
imagify_missing_next_gen_totalStart of next-gen generation runint (total count)HOUR_IN_SECONDS
imagify_bulk_optimization_infosUser dismisses info popup1WEEK_IN_SECONDS

ActionScheduler Job Lifecycle

ActionScheduler is bundled at inc/Dependencies/ActionScheduler/action-scheduler.php. Jobs go through these states:

pending in-progress complete

On failure: failed. On cancel: canceled. The check_optimization_status() hook fires on imagify_after_optimize and decrements the running counter, deleting the transient and setting the complete transient when all jobs finish.

Multisite Context Routing

// Bulk::get_contexts() — determines which contexts appear on bulk page
if ( ! is_network_admin() ) {
    $types['library|wp'] = 1;                // library only in site admin
}
if ( imagify_is_active_for_network() && is_network_admin() ) {
    $types['custom-folders|custom-folders'] = 1;  // custom folders in network admin
} elseif ( ! imagify_is_active_for_network() ) {
    $types['custom-folders|custom-folders'] = 1;  // custom folders in site admin
}
🔒

10. Concurrency & Locking Mechanisms

Transient-based per-media locks that prevent duplicate concurrent optimization or restore jobs.

Per-Media Process Lock

Defined in AbstractProcess. Each lock is a transient named after the context and media ID.

// Transient name pattern (LOCK_NAME constant)
const LOCK_NAME = 'imagify_%1$s_%2$s_process_locked';
// Example: 'imagify_wp_42_process_locked'
// Example: 'imagify_custom-folders_7_process_locked'

// Network-aware: uses set_site_transient when context is_network_wide()
public function lock( string $action = 'optimizing' ): void {
    $name     = $this->get_lock_name();           // sprintf(LOCK_NAME, ctx, id)
    $callback = $media->get_context_instance()->is_network_wide()
        ? 'set_site_transient' : 'set_transient';
    call_user_func( $callback, $name, $action, 10 * MINUTE_IN_SECONDS );
}

public function is_locked(): string|false {
    // Returns 'optimizing' | 'restoring' | false
    $callback = ...'get_site_transient' or 'get_transient'...;
    $action   = call_user_func( $callback, $name );
    return $this->validate_lock_action( $action );  // normalizes 'restore' → 'restoring'
}

public function unlock(): void {
    $callback = ...'delete_site_transient' or 'delete_transient'...;
    call_user_func( $callback, $name );
}

Lock Actions

ValueSet ByCleared By
'optimizing'optimize() before iterating sizesoptimize() after all sizes complete
'restoring'restore() at startrestore() at end (success or error)

Transient Name Summary for Cleanup

Managed by Imagify\Tools\InternalStateList::get_locked_transient_patterns(). Used by ResetInternalState for SQL LIKE deletion:

'_transient_%imagify-auto-optimize-%'      // Legacy (deprecated)
'_transient_%imagify_rpc_%'                // Legacy (deprecated)
'_transient_imagify_%_process_locked'      // Active single-site locks
'_site_transient_imagify_%_process_lock%'  // Active network-wide locks
⚠️ TTL is 10 minutes. If a PHP process dies mid-optimization, the lock expires automatically. The Reset Internal State admin tool can force-clear all locks immediately via direct SQL DELETE.
🖼️

11. Picture\Display — Output Buffer HTML Rewrite

How <img> tags are rewritten to <picture> tags at the HTTP response level.

Class: Imagify\Picture\Display (classes/Picture/Display.php). Implements SubscriberInterface.

Subscribed Events

public static function get_subscribed_events(): array {
    return [
        'template_redirect'            => 'start_content_process',
        'imagify_process_webp_content' => 'process_content',
    ];
}

Full Rewrite Pipeline

template_redirect start_content_process() ob_start([this, 'maybe_process_buffer']) PHP renders full page HTML maybe_process_buffer($buffer) is_html() check (must contain </html> and be >255 chars) process_content($buffer) remove_picture_tags() — strip existing <picture> wrappers get_images() — regex extract all <img> tags process_image() per tag filesystem->exists() check for .webp/.avif sidecars build_picture_tag() → str_replace() in buffer

Guards in start_content_process()

Lazy-Load Support

The parser checks these src attributes in priority order: data-lazy-srcdata-srcsrc. Likewise for srcset: data-lazy-srcsetdata-srcsetsrcset. The generated <source> tag mirrors whichever attribute was active.

Generated HTML Structure

<!-- Input -->
<img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="...">

<!-- Output (when both AVIF and WebP exist) -->
<picture>
  <source type="image/avif" srcset="/uploads/photo.jpg.avif, /uploads/photo-300.jpg.avif 300w" sizes="...">
  <source type="image/webp" srcset="/uploads/photo.jpg.webp, /uploads/photo-300.jpg.webp 300w" sizes="...">
  <img src="/uploads/photo.jpg" srcset="/uploads/photo-300.jpg 300w" sizes="..." alt="...">
</picture>

URL → Path Resolution

url_to_path() converts image URLs to filesystem paths for existence checks. It handles: uploads URL, site root URL, CDN URL (via imagify_cdn_source_url filter), and protocol-relative URLs. Static caches are maintained per request.

Filters on the Rewrite Path

FilterSignaturePurpose
imagify_allow_picture_tags_for_nextgen(bool $allow): boolGlobal on/off switch for the rewriter
imagify_webp_picture_images_to_display(array $images, string $content): arrayFilter/add/remove images before rewriting
imagify_webp_picture_process_image(array $data, string $img_tag): array|falsePer-image data manipulation (used by S3 Offload integration)
imagify_picture_attributes(array $attributes, array $data): arrayAttributes on the <picture> element
imagify_picture_source_attributes(array $attributes, array $data): arrayAttributes on each <source> element
imagify_picture_img_attributes(array $attributes, array $data): arrayAttributes on the fallback <img>
imagify_additional_source_tags(string $html, array $data): stringInject extra <source> elements before the generated ones
imagify_buffer(string $buffer): stringFinal buffer after all replacements
imagify_cdn_source_url(string $url): stringCDN base URL for URL-to-path mapping
🔐

12. AJAX & Admin-Post — Full Security Table

Every wp_ajax_* and admin_post_* action with its nonce name and capability requirement.

Security is enforced by imagify_check_nonce($action, $query_arg) which wraps check_ajax_referer() and calls imagify_die() on failure. Capability checks use imagify_get_context($ctx)->current_user_can($capability, $media_id).

wp_ajax_* + admin_post_* (both)

These are registered for both AJAX and form POST. Handler class: Imagify_Admin_Ajax_Post.

ActionNonce NameCapabilityDescription
imagify_manual_optimizeimagify-optimize-{id}-{ctx}manual-optimizeOptimize single attachment
imagify_manual_reoptimizeimagify-manual-reoptimize-{id}-{ctx}manual-optimizeRe-optimize at different level
imagify_optimize_missing_sizesimagify-optimize-missing-sizes-{id}-{ctx}manual-optimizeGenerate missing thumbnail sizes
imagify_generate_nextgen_versionsimagify-generate-nextgen-versions-{id}-{ctx}manual-optimizeGenerate WebP/AVIF for one attachment
imagify_delete_nextgen_versionsimagify-delete-nextgen-versions-{id}-{ctx}manual-restoreRemove WebP/AVIF sidecar files
imagify_restoreimagify-restore-{id}-{ctx}manual-restoreRestore attachment from backup
imagify_optimize_fileimagify_optimize_filemanual-optimize (custom-folders ctx)Optimize custom folder file
imagify_reoptimize_fileimagify_reoptimize_filemanual-optimize (custom-folders ctx)Re-optimize custom folder file
imagify_restore_fileimagify_restore_filemanual-restore (custom-folders ctx)Restore custom folder file from backup
imagify_refresh_file_modifiedimagify_refresh_file_modifiedmanual-optimize (custom-folders ctx)Refresh file hash/modified status

wp_ajax_* only

ActionNonce NameCapability / CheckDescription
imagify_bulk_optimizeimagify-bulk-optimizebulk-optimizeLaunch ActionScheduler bulk job
imagify_missing_nextgen_generationimagify-bulk-optimizebulk-optimize per contextGenerate all missing next-gen files
imagify_get_folder_type_dataimagify-bulk-optimizebulk-optimizeStats for one folder type on bulk page
imagify_bulk_info_seenimagify-bulk-optimizebulk-optimizeSet imagify_bulk_optimization_infos transient
imagify_bulk_get_statsimagify-bulk-optimizebulk-optimize per folder typeAggregate bulk page statistics
imagify_reset_internal_stateimagify_reset_internal_statemanage (wp ctx)Clear all locks, transients, AS jobs
imagify_check_backup_dir_is_writableimagify_check_backup_dir_is_writablemanage (wp ctx)Test backup directory writability
imagify_get_files_treeget-files-treemanage (custom-folders ctx)Filesystem tree for folder picker
imagify_signupimagify-signup (imagifysignupnonce)manage (wp ctx)Create Imagify account
imagify_check_api_key_validityimagify-check-api-key (imagifycheckapikeynonce)manageValidate API key against API
imagify_get_pricesimagify_get_pricing_{user_id} (imagifynonce)manageFetch plan prices
imagify_check_couponimagify_get_pricing_{user_id} (imagifynonce)manageValidate coupon code
imagify_get_discountimagify_get_pricing_{user_id} (imagifynonce)manageCheck active discount
imagify_get_images_countsimagify_get_pricing_{user_id} (imagifynonce)manageCount images per status
imagify_update_estimate_sizesupdate_estimate_sizesmanageRecalculate size estimates
imagify_get_user_dataimagify_get_user_datamanageFetch fresh account data from API
imagify_delete_user_data_cacheimagify_delete_user_data_cachemanagePurge cached user data transient
nopriv_imagify_rpcimagify_rpc_{rpc_id} (imagify_rpc_nonce)None (nonce only)Internal RPC dispatch — re-fires as wp_ajax_{action}

admin_post_* only

ActionNonce NameCapability
imagify_scan_custom_foldersimagify_scan_custom_foldersoptimize (custom-folders ctx)
imagify_dismiss_adimagify-dismiss-admanage (wp ctx)
imagify_dismiss_noticeimagify-dismiss-noticeVaries per notice
imagify_deactivate_pluginimagify-deactivate-pluginVaries per notice
imagify_rollbackimagify_rollbackmanage_options
ℹ️ Nonces with {id} and {ctx} are unique per media item and context (e.g. imagify-optimize-42-wp). This prevents CSRF replay across different media items.