Skip to content

Implement Composer#3

Open
jpjuliao wants to merge 7 commits into
mainfrom
feature/implement-composer
Open

Implement Composer#3
jpjuliao wants to merge 7 commits into
mainfrom
feature/implement-composer

Conversation

@jpjuliao
Copy link
Copy Markdown
Owner

@jpjuliao jpjuliao commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Added WordPress admin interface for managing form submissions
    • Added REST API endpoints for form data handling and submission management
    • Added database support for storing and retrieving form submissions
    • Added shortcode functionality for embedding forms on pages
  • Chores

    • Restructured plugin codebase with modern PHP standards
    • Added code quality tooling and linting configuration
    • Updated build artifacts with namespace references

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This pull request refactors the WordPress plugin architecture from class-based files with manual initialization to a Composer-based structure with PSR-4 autoloading and updated namespaces. It adds development tooling configuration (EditorConfig, VSCode settings, GitHub Actions lint workflow, PHPCodeSniffer config), migrates the plugin bootstrap, and updates webpack build artifacts with new package identifiers.

Changes

Cohort / File(s) Summary
Development Tooling Configuration
.editorconfig, .vscode/settings.json, .github/workflows/lint.yml, phpcs.xml
Introduces EditorConfig with charset and formatting rules, VSCode PHP formatter/linter settings, GitHub Actions linting workflow for PHP 8.2, and PHPCodeSniffer configuration with WordPress standards.
Package Configuration
composer.json, jpjuliao-multi-step-form-builder.php
Adds Composer configuration with PHP 8.1+ requirement, dev dependencies (PHPUnit, Code Sniffer), PSR-4 autoloading, and test/lint scripts. Updates main plugin file to use Composer autoload, fixes namespace capitalization from Wordpress to WordPress, and registers plugin instance directly on plugins_loaded hook.
PHP Source Code Refactoring (Old Structure Removed)
includes/class-plugin.php, includes/class-database.php, includes/class-admin.php, includes/class-frontend.php, includes/class-post-type.php, includes/class-shortcode.php, includes/class-rest-api.php
Removes entire legacy class files including singleton plugin bootstrap, database layer, admin UI, frontend asset loading, post type management, shortcode handlers, and REST API endpoints.
PHP Source Code Refactoring (New Structure Added)
includes/Plugin.php, includes/Database.php, includes/Admin.php, includes/Frontend.php, includes/PostType.php, includes/Shortcode.php, includes/RESTapi.php
Adds refactored classes with corrected namespace (JPJULIAO\WordPress\MultiStepFormBuilder), PSR-4 compliant naming, and identical functionality. Organizes plugin initialization, database operations, admin UI, frontend assets, post type registration, shortcodes, and REST API routing with dependency injection.
Webpack Build Artifacts
build/33.js, build/166.js, build/174.js, build/280.js, build/304.js, build/351.js, build/399.js, build/404.js, build/414.js, build/463.js, build/484.js, build/573.js, build/734.js, build/826.js, build/856.js, build/891.js, build/980.js, build/admin.js, build/frontend.js
Renames webpack chunk identifiers from webpackChunk/multi_step_form_builder to webpackChunk/jpjuliao_multi_step_form_builder and updates corresponding translation namespace references. Internal symbol renaming and module export references updated; no functional behavior changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 Hop hop, the code's been reorganized with care,
From class-files scattered to structure fair,
PSR-4 paths and Composer's embrace,
Every bundle now bears its new name and place! 🏗️✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Implement Composer' is vague and does not clearly convey the primary changes in the changeset, which include a comprehensive refactoring from class-based to PSR-4 namespaced architecture, addition of development tooling (linting, VS Code configuration), and restructuring of plugin infrastructure. Consider a more descriptive title like 'Refactor plugin to use Composer and PSR-4 autoloading' or 'Add Composer support with code standards tooling' that better reflects the scope of changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/implement-composer

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
jpjuliao-multi-step-form-builder.php (1)

1-22: ⚠️ Potential issue | 🟠 Major

Add Requires PHP: 8.1 to the plugin header.

Line 22 uses the first-class callable syntax (Plugin::get_instance(...)), which requires PHP 8.1+. Other files use union types (int|false) requiring PHP 8.0+. Without a Requires PHP header, WordPress won't prevent activation on older PHP versions, resulting in a fatal parse error.

Proposed fix
  * License: GPL2
  * Tested up to: 6.9
+ * Requires PHP: 8.1
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@jpjuliao-multi-step-form-builder.php` around lines 1 - 22, Add a "Requires
PHP: 8.1" header line to the plugin file header so WordPress prevents activation
on older PHP versions; edit the top-of-file plugin header block in
jpjuliao-multi-step-form-builder.php (the comment block containing Plugin Name,
Version, etc.) and insert the Requires PHP: 8.1 declaration, ensuring
compatibility with uses of Plugin::get_instance(...) and any 8.0+ features
elsewhere.
🧹 Nitpick comments (6)
phpcs.xml (1)

7-7: Tests directory scan is commented out — linting coverage gap.

With <!-- <file>tests</file> --> commented out, any PHP in tests/ is never linted. If tests are added alongside the new PSR-4 structure, violations there will go undetected. Enable the <file>tests</file> entry (or remove the comment) when tests are introduced.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@phpcs.xml` at line 7, Uncomment or add the <file>tests</file> entry in
phpcs.xml so the tests/ directory is included in PHP_CodeSniffer scans; locate
the commented line "<!-- <file>tests</file> -->" and remove the comment markers
(or insert an un-commented <file>tests</file> element) to ensure any PHP in
tests/ is linted alongside the PSR-4 sources.
includes/Shortcode.php (1)

28-35: Escape translated strings in HTML output.

The error messages from __() are embedded in raw HTML without esc_html(). While the source strings are hardcoded, malicious translation files could inject HTML. Wrap with esc_html() for defense-in-depth.

Same applies to lines 55 and 60.

Example fix
-		return '<p>' . __( 'Please provide a valid form ID.', 'jpjuliao-multi-step-form-builder' ) . '</p>';
+		return '<p>' . \esc_html__( 'Please provide a valid form ID.', 'jpjuliao-multi-step-form-builder' ) . '</p>';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Shortcode.php` around lines 28 - 35, The returned HTML error
messages in Shortcode.php embed translated strings via __() without escaping;
update the message returns (the checks using $form_id and $post =
\get_post($form_id) where it returns '<p>' . __(...) . '</p>') to wrap the
translated strings with esc_html() (e.g., esc_html( __(...) )) to prevent
malicious translation injection, and make the same change for the other two
message returns referenced around the checks at lines 55 and 60 so all
user-facing translated strings are escaped before concatenation into HTML.
includes/RESTapi.php (1)

96-108: Missing \ namespace prefix on wp_json_encode (line 98).

All other WordPress function calls in this file use the \ global namespace prefix (e.g., \get_post, \update_post_meta, \rest_ensure_response). Line 98 omits it for wp_json_encode. It works due to PHP's global fallback, but it's inconsistent and could mask a bug if a same-named function is ever added to the namespace.

Proposed fix
-		$form_config = wp_json_encode( $request->get_json_params() );
+		$form_config = \wp_json_encode( $request->get_json_params() );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/RESTapi.php` around lines 96 - 108, In save_form (method save_form
in this class) add the global namespace prefix to wp_json_encode by changing the
call to \wp_json_encode so it matches the other global WordPress calls (e.g.,
\get_post, \update_post_meta, \rest_ensure_response); this keeps namespace
consistency and avoids potential conflicts if a same-named function appears in
the current namespace.
includes/PostType.php (1)

86-102: save_form_meta is a no-op after validation guards.

The method verifies the nonce, checks autosave, and confirms permissions, but never actually persists any data. If form configuration is intentionally saved only via the REST API (RESTapi::save_form), the nonce field in render_form_builder and this hook registration are dead code. Otherwise, this is an incomplete implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/PostType.php` around lines 86 - 102, The save_form_meta method
currently only runs validation and returns without persisting anything; either
remove the unused nonce field and the hook that calls save_form_meta if
persistence is handled exclusively by RESTapi::save_form (and remove or update
render_form_builder accordingly), or implement the server-side save here: after
the guards in save_form_meta retrieve and sanitize the expected $_POST payload
(e.g., form fields/config), then persist via update_post_meta($post_id, ...) or
delegate to RESTapi::save_form by calling that method with the sanitized data;
update render_form_builder and hook registration to match the chosen approach.
includes/Database.php (1)

111-127: get_client_ip trusts spoofable proxy headers.

Headers like HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP are trivially spoofable by the client. If the IP is used for rate-limiting, fraud detection, or compliance logging, this can be bypassed. If the site is not behind a trusted reverse proxy, prefer using only REMOTE_ADDR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Database.php` around lines 111 - 127, The get_client_ip function
currently trusts spoofable headers (HTTP_X_FORWARDED_FOR, HTTP_CLIENT_IP, etc.)
which allows clients to fake IPs; change it to use REMOTE_ADDR by default and
only accept proxy headers when a trusted proxy configuration is present. Update
get_client_ip to (1) check a new trusted proxy list/flag (e.g., a constant or
option like TRUSTED_PROXIES or get_option('trusted_proxies')) and if not
set/empty return $_SERVER['REMOTE_ADDR'] sanitized, and (2) when trusted proxies
are configured, validate that the immediate REMOTE_ADDR matches a trusted proxy
before parsing header values like HTTP_X_FORWARDED_FOR; keep sanitization and
FILTER_VALIDATE_IP checks as currently implemented. Ensure function name
get_client_ip remains and remove blind acceptance of headers unless the
trusted-proxy guard passes.
includes/Admin.php (1)

18-88: Duplicated script/style enqueue blocks.

Lines 23-46 and 58-87 contain nearly identical wp_enqueue_script / wp_enqueue_style / wp_localize_script calls. Extract a shared helper to reduce duplication and keep the two branches in sync when dependencies or localized data change.

Sketch
+	private function enqueue_msf_admin_scripts( int $form_id ): void {
+		\wp_enqueue_script(
+			'msf-admin',
+			\plugin_dir_url( __DIR__ ) . 'build/admin.js',
+			array( 'wp-element', 'wp-components', 'wp-i18n', 'wp-api-fetch', 'wp-data' ),
+			\filemtime( \plugin_dir_path( __DIR__ ) . 'build/admin.js' ),
+			true
+		);
+
+		\wp_enqueue_style(
+			'msf-admin',
+			\plugin_dir_url( __DIR__ ) . 'build/admin.css',
+			array( 'wp-components' ),
+			\filemtime( \plugin_dir_path( __DIR__ ) . 'build/admin.css' )
+		);
+
+		\wp_localize_script(
+			'msf-admin',
+			'msfAdmin',
+			array(
+				'formId' => $form_id,
+				'apiUrl' => \rest_url( 'msf/v1' ),
+				'nonce'  => \wp_create_nonce( 'wp_rest' ),
+			)
+		);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Admin.php` around lines 18 - 88, The enqueue_admin_assets method
duplicates the wp_enqueue_script/wp_enqueue_style/wp_localize_script block in
two branches; refactor by extracting a private helper (e.g., ensure_admin_assets
or enqueue_admin_assets_block) that accepts the dynamic values (formId) and
performs the wp_enqueue_script, wp_enqueue_style and wp_localize_script work,
then call that helper from both places inside enqueue_admin_assets (one call
with $post->ID and one with $form_id) so dependencies and asset paths remain
centralized and synchronized; keep the script handle 'msf-admin' and preserve
rest_url and nonce generation inside the helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.editorconfig:
- Around line 9-11: .editorconfig currently sets PHP files to 2-space
indentation which conflicts with WordPress Coding Standards used by phpcs.xml;
update the [*.php] section by changing indent_style from "space" to "tab" and
remove or unset indent_size so PHP files use tabs (i.e., modify the settings
under the [*.php] header where indent_style and indent_size are declared).

In @.github/workflows/lint.yml:
- Around line 28-32: Update the "Install Composer dependencies" step to run
composer install with the --no-interaction flag to prevent CI hangs (modify the
step named "Install Composer dependencies"); also ensure composer.lock is
committed to the repo so the cache key using hashFiles('**/composer.lock')
produces a stable hash instead of an empty value — commit the lockfile and then
leave the existing cache key logic intact so each commit gets its own cache
slot.

In @.vscode/settings.json:
- Line 8: The "phpsab.standard" setting currently hardcodes "PSR12" which
conflicts with the repo's PHP coding standard defined in phpcs.xml (WordPress);
update the setting value from "PSR12" to "WordPress" and ensure
"phpsab.autoRulesetSearch" remains enabled so the workspace can still detect
phpcs.xml, i.e., change the phpsab.standard entry to "WordPress" to align VS
Code defaults with the repository standard.

In `@composer.json`:
- Line 16: The PSR-4 namespace in composer.json is cased incorrectly: change the
autoload and autoload-dev mapping keys from
"JPJULIAO\\Wordpress\\MultiStepFormBuilder\\" to exactly match the declared PHP
namespace "JPJULIAO\\WordPress\\MultiStepFormBuilder\\" (capital "S" in
WordPress) so PSR-4 resolution will succeed; update both the autoload and
autoload-dev entries to use the corrected casing and then run composer
dump-autoload to verify.

In `@includes/Database.php`:
- Around line 100-109: Change delete_submission to return int|false instead of
bool: call $wpdb->delete(...) and return its raw result (the affected-rows int
or false) so callers can distinguish 0 (no row) from false (DB error). Update
the signature of delete_submission and adjust RESTapi::delete_submission to
treat 0 as a 404 (not found) and false as a 500 (DB error) when handling the
return value from Database::delete_submission.
- Around line 50-61: The insert currently passes user_id with a '%d' format
which coerces null to 0; change the insert to omit the 'user_id' key when
$user_id is null so SQL NULL (or the column default) is used instead.
Concretely, build the data array and the formats array before calling
$wpdb->insert: include 'user_id' => $user_id and a '%d' format only when
$user_id !== null, otherwise do not add that key/format; keep the other keys
('form_id','submission_data','ip_address','user_agent') unchanged and call
$wpdb->insert($this->table_name, $data, $formats).

In `@includes/Frontend.php`:
- Around line 25-48: The code calls get_modal_forms() twice causing duplicate DB
queries; capture its result once and reuse it: in the Frontend class method
where get_modal_forms() is first called to set $should_enqueue, assign the
result to a local variable (e.g., $modal_forms = $this->get_modal_forms()) and
use that same $modal_forms variable later when building the localized payload
instead of calling $this->get_modal_forms() again; ensure the logic that flips
$should_enqueue still uses the cached $modal_forms and remove the second
invocation to avoid repeated get_posts/get_post_meta calls.

In `@includes/Plugin.php`:
- Around line 29-34: The activation/deactivation hooks registered in the
Plugin::__construct are using the wrong filename ("multi-step-form-builder.php")
so activate()/deactivate() will never run; update the two calls to
register_activation_hook and register_deactivation_hook in the __construct
method to reference the real main plugin file name
("jpjuliao-multi-step-form-builder.php") (or, alternatively, derive the correct
main plugin path dynamically) so WordPress can match and invoke the
Plugin::activate and Plugin::deactivate callbacks.

In `@includes/PostType.php`:
- Line 80: In the echo that renders the Copy Shortcode button in
includes/PostType.php (the HTML button with the onclick handler using
navigator.clipboard.writeText), remove the extra closing parenthesis so the
onclick calls navigator.clipboard.writeText(...) only once; ensure the
single-quoted string and concatenated esc_attr($post->ID) remain correctly
escaped and the final onclick attribute ends with a single closing quote and
angle bracket rather than an extra ')'.

In `@includes/RESTapi.php`:
- Around line 158-178: In get_submissions, clamp and validate the computed
$per_page after casting to int to avoid zero/negative or absurd values: cast
$request->get_param('per_page') to int, then if $per_page <= 0 set it to a safe
default (e.g. 50), and optionally cap it with a max (e.g. 1000); use this
validated $per_page when computing $offset and total_pages so LIMIT and ceil(
$total / $per_page ) never receive zero/negative values. Ensure the change
touches the $per_page computation and any uses in offset and total_pages in
get_submissions.
- Around line 200-221: The loop over $form_config['steps'] accesses
$field['name'], $field['label'], and $field['type'] without guards; add
defensive checks at the start of the inner loop (inside the foreach (
$step['fields'] as $field ) block) to verify isset($field['name']) and
isset($field['label']) (and optionally isset($field['type'])); if any required
key is missing, skip the field (continue) and optionally log or collect an error
for malformed config; then use the guarded $field_name, $field['label'] and
$field['type'] for the existing required/email/url validations (keeping the
existing checks that call is_email and filter_var).

In `@phpcs.xml`:
- Around line 16-19: Remove the four invalid PHPCS exclusion entries from
phpcs.xml by deleting the <exclude> elements with names
"WordPress.Files.ClassStructure.MissingNamespace",
"WordPress.Files.FileHeader.Missing",
"WordPress.Files.ClassStructure.MissingClassDocBlock", and
"WordPress.Files.FunctionUse.MultipleUseOnly" (they are not valid WPCS 3.3.0
sniff codes and are silently ignored); keep the valid "WordPress.Files.FileName"
exclusion intact and ensure no other valid exclusions are accidentally removed.

---

Outside diff comments:
In `@jpjuliao-multi-step-form-builder.php`:
- Around line 1-22: Add a "Requires PHP: 8.1" header line to the plugin file
header so WordPress prevents activation on older PHP versions; edit the
top-of-file plugin header block in jpjuliao-multi-step-form-builder.php (the
comment block containing Plugin Name, Version, etc.) and insert the Requires
PHP: 8.1 declaration, ensuring compatibility with uses of
Plugin::get_instance(...) and any 8.0+ features elsewhere.

---

Nitpick comments:
In `@includes/Admin.php`:
- Around line 18-88: The enqueue_admin_assets method duplicates the
wp_enqueue_script/wp_enqueue_style/wp_localize_script block in two branches;
refactor by extracting a private helper (e.g., ensure_admin_assets or
enqueue_admin_assets_block) that accepts the dynamic values (formId) and
performs the wp_enqueue_script, wp_enqueue_style and wp_localize_script work,
then call that helper from both places inside enqueue_admin_assets (one call
with $post->ID and one with $form_id) so dependencies and asset paths remain
centralized and synchronized; keep the script handle 'msf-admin' and preserve
rest_url and nonce generation inside the helper.

In `@includes/Database.php`:
- Around line 111-127: The get_client_ip function currently trusts spoofable
headers (HTTP_X_FORWARDED_FOR, HTTP_CLIENT_IP, etc.) which allows clients to
fake IPs; change it to use REMOTE_ADDR by default and only accept proxy headers
when a trusted proxy configuration is present. Update get_client_ip to (1) check
a new trusted proxy list/flag (e.g., a constant or option like TRUSTED_PROXIES
or get_option('trusted_proxies')) and if not set/empty return
$_SERVER['REMOTE_ADDR'] sanitized, and (2) when trusted proxies are configured,
validate that the immediate REMOTE_ADDR matches a trusted proxy before parsing
header values like HTTP_X_FORWARDED_FOR; keep sanitization and
FILTER_VALIDATE_IP checks as currently implemented. Ensure function name
get_client_ip remains and remove blind acceptance of headers unless the
trusted-proxy guard passes.

In `@includes/PostType.php`:
- Around line 86-102: The save_form_meta method currently only runs validation
and returns without persisting anything; either remove the unused nonce field
and the hook that calls save_form_meta if persistence is handled exclusively by
RESTapi::save_form (and remove or update render_form_builder accordingly), or
implement the server-side save here: after the guards in save_form_meta retrieve
and sanitize the expected $_POST payload (e.g., form fields/config), then
persist via update_post_meta($post_id, ...) or delegate to RESTapi::save_form by
calling that method with the sanitized data; update render_form_builder and hook
registration to match the chosen approach.

In `@includes/RESTapi.php`:
- Around line 96-108: In save_form (method save_form in this class) add the
global namespace prefix to wp_json_encode by changing the call to
\wp_json_encode so it matches the other global WordPress calls (e.g., \get_post,
\update_post_meta, \rest_ensure_response); this keeps namespace consistency and
avoids potential conflicts if a same-named function appears in the current
namespace.

In `@includes/Shortcode.php`:
- Around line 28-35: The returned HTML error messages in Shortcode.php embed
translated strings via __() without escaping; update the message returns (the
checks using $form_id and $post = \get_post($form_id) where it returns '<p>' .
__(...) . '</p>') to wrap the translated strings with esc_html() (e.g.,
esc_html( __(...) )) to prevent malicious translation injection, and make the
same change for the other two message returns referenced around the checks at
lines 55 and 60 so all user-facing translated strings are escaped before
concatenation into HTML.

In `@phpcs.xml`:
- Line 7: Uncomment or add the <file>tests</file> entry in phpcs.xml so the
tests/ directory is included in PHP_CodeSniffer scans; locate the commented line
"<!-- <file>tests</file> -->" and remove the comment markers (or insert an
un-commented <file>tests</file> element) to ensure any PHP in tests/ is linted
alongside the PSR-4 sources.

Comment thread .editorconfig
Comment on lines +9 to +11
[*.php]
indent_style = space
indent_size = 2
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine phpcs.xml
fd phpcs.xml --exec cat {}

Repository: jpjuliao/multi-step-form-builder

Length of output: 1217


🏁 Script executed:

# Also check for any other PHPCS or linting configurations
fd -e xml -e json -e yml -e yaml | grep -E '(phpcs|phpstan|coding|standards)' | head -20

Repository: jpjuliao/multi-step-form-builder

Length of output: 85


🏁 Script executed:

# Get context on .editorconfig file itself
cat -n .editorconfig

Repository: jpjuliao/multi-step-form-builder

Length of output: 544


PHP indentation in .editorconfig conflicts with WordPress Coding Standards — CI lint will fail.

phpcs.xml uses WordPress Coding Standards, which mandate tab indentation for PHP files. However, .editorconfig specifies 2-space indentation for PHP. Any code written by contributors following the editor config will fail composer lint in CI.

Change PHP indentation in .editorconfig to tabs:

🔧 Fix
[*.php]
-indent_style = space
-indent_size = 2
+indent_style = tab
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[*.php]
indent_style = space
indent_size = 2
[*.php]
indent_style = tab
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.editorconfig around lines 9 - 11, .editorconfig currently sets PHP files to
2-space indentation which conflicts with WordPress Coding Standards used by
phpcs.xml; update the [*.php] section by changing indent_style from "space" to
"tab" and remove or unset indent_size so PHP files use tabs (i.e., modify the
settings under the [*.php] header where indent_style and indent_size are
declared).

Comment on lines +28 to +32
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-

- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add --no-interaction to composer install and commit composer.lock.

Two related CI reliability issues:

  1. Without --no-interaction, Composer can prompt for input and hang the CI runner if an unexpected interactive question arises.
  2. hashFiles('**/composer.lock') produces an empty hash when composer.lock is not committed, making the cache key degenerate to the restore-key prefix — all CI runs will share the same cache slot and dependency versions will silently drift between runs. For a plugin (application), composer.lock should be committed.
🔧 Proposed fix
-        run: composer install --prefer-dist --no-progress
+        run: composer install --prefer-dist --no-progress --no-interaction
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/lint.yml around lines 28 - 32, Update the "Install
Composer dependencies" step to run composer install with the --no-interaction
flag to prevent CI hangs (modify the step named "Install Composer
dependencies"); also ensure composer.lock is committed to the repo so the cache
key using hashFiles('**/composer.lock') produces a stable hash instead of an
empty value — commit the lockfile and then leave the existing cache key logic
intact so each commit gets its own cache slot.

Comment thread .vscode/settings.json
},
"phpsab.executablePathCBF": "./vendor/bin/phpcbf",
"phpsab.executablePathCS": "./vendor/bin/phpcs",
"phpsab.standard": "PSR12",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

phpsab.standard is set to PSR12 but the repo enforces WordPress coding standards via phpcs.xml.

While phpsab.autoRulesetSearch: true will find phpcs.xml when VS Code is opened from the repo root, the explicit "PSR12" acts as a fallback for any other workspace root and is actively misleading to contributors reading this file. Set it to WordPress to match the repo standard:

🔧 Proposed fix
-  "phpsab.standard": "PSR12",
+  "phpsab.standard": "WordPress",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"phpsab.standard": "PSR12",
"phpsab.standard": "WordPress",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.vscode/settings.json at line 8, The "phpsab.standard" setting currently
hardcodes "PSR12" which conflicts with the repo's PHP coding standard defined in
phpcs.xml (WordPress); update the setting value from "PSR12" to "WordPress" and
ensure "phpsab.autoRulesetSearch" remains enabled so the workspace can still
detect phpcs.xml, i.e., change the phpsab.standard entry to "WordPress" to align
VS Code defaults with the repository standard.

Comment thread composer.json
},
"autoload": {
"psr-4": {
"JPJULIAO\\Wordpress\\MultiStepFormBuilder\\": "includes/"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the namespace declared in PHP source files under includes/
rg -rn "^namespace JPJULIAO" --include="*.php" includes/ tests/ 2>/dev/null | head -30

Repository: jpjuliao/multi-step-form-builder

Length of output: 58


🏁 Script executed:

#!/bin/bash
# Check if directories exist and what files are in them
echo "=== Checking includes/ directory ==="
ls -la includes/ 2>/dev/null || echo "includes/ does not exist"

echo -e "\n=== Checking tests/ directory ==="
ls -la tests/ 2>/dev/null || echo "tests/ does not exist"

echo -e "\n=== Full directory structure ==="
find . -type f -name "*.php" | head -20

echo -e "\n=== Checking composer.json content ==="
cat -n composer.json

Repository: jpjuliao/multi-step-form-builder

Length of output: 2010


🏁 Script executed:

#!/bin/bash
# Check namespace declarations in each PHP file
for file in ./includes/*.php; do
  echo "=== $file ==="
  grep -n "^namespace" "$file"
done

Repository: jpjuliao/multi-step-form-builder

Length of output: 665


PSR-4 namespace casing mismatch will break autoloading on Linux.

All PHP files in includes/ declare namespace JPJULIAO\WordPress\MultiStepFormBuilder; (capital S), but composer.json maps JPJULIAO\\Wordpress\\MultiStepFormBuilder\\ (lowercase s). PSR-4 resolution is case-sensitive on Linux/Ubuntu (including the CI runner), so the autoloader will fail to resolve these classes at runtime, causing fatal errors.

The same mismatch exists on line 21 in autoload-dev.

Fix
-      "JPJULIAO\\Wordpress\\MultiStepFormBuilder\\": "includes/"
+      "JPJULIAO\\WordPress\\MultiStepFormBuilder\\": "includes/"
-      "JPJULIAO\\Wordpress\\MultiStepFormBuilder\\Tests\\": "tests/"
+      "JPJULIAO\\WordPress\\MultiStepFormBuilder\\Tests\\": "tests/"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"JPJULIAO\\Wordpress\\MultiStepFormBuilder\\": "includes/"
"JPJULIAO\\WordPress\\MultiStepFormBuilder\\": "includes/"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composer.json` at line 16, The PSR-4 namespace in composer.json is cased
incorrectly: change the autoload and autoload-dev mapping keys from
"JPJULIAO\\Wordpress\\MultiStepFormBuilder\\" to exactly match the declared PHP
namespace "JPJULIAO\\WordPress\\MultiStepFormBuilder\\" (capital "S" in
WordPress) so PSR-4 resolution will succeed; update both the autoload and
autoload-dev entries to use the corrected casing and then run composer
dump-autoload to verify.

Comment thread includes/Database.php
Comment on lines +50 to +61
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Using $wpdb->insert for custom plugin table writes.
$result = $wpdb->insert(
$this->table_name,
array(
'form_id' => $form_id,
'user_id' => $user_id ? $user_id : null,
'submission_data' => \wp_json_encode( $data ),
'ip_address' => $ip_address,
'user_agent' => $user_agent,
),
array( '%d', '%d', '%s', '%s', '%s' )
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

user_id = null is silently converted to 0 by the %d format specifier.

$wpdb->prepare coerces null to 0 when the format is %d, so anonymous (logged-out) submissions will be stored with user_id = 0 instead of NULL. This breaks any query that relies on IS NULL to identify guest submissions and contradicts the schema's DEFAULT NULL.

Handle the null case separately so wpdb receives the correct SQL NULL.

Proposed fix
-		$result = $wpdb->insert(
-			$this->table_name,
-			array(
-				'form_id'         => $form_id,
-				'user_id'         => $user_id ? $user_id : null,
-				'submission_data' => \wp_json_encode( $data ),
-				'ip_address'      => $ip_address,
-				'user_agent'      => $user_agent,
-			),
-			array( '%d', '%d', '%s', '%s', '%s' )
-		);
+		$insert_data = array(
+			'form_id'         => $form_id,
+			'submission_data' => \wp_json_encode( $data ),
+			'ip_address'      => $ip_address,
+			'user_agent'      => $user_agent,
+		);
+		$insert_format = array( '%d', '%s', '%s', '%s' );
+
+		if ( $user_id ) {
+			$insert_data['user_id'] = $user_id;
+			$insert_format[]        = '%d';
+		}
+
+		$result = $wpdb->insert( $this->table_name, $insert_data, $insert_format );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Using $wpdb->insert for custom plugin table writes.
$result = $wpdb->insert(
$this->table_name,
array(
'form_id' => $form_id,
'user_id' => $user_id ? $user_id : null,
'submission_data' => \wp_json_encode( $data ),
'ip_address' => $ip_address,
'user_agent' => $user_agent,
),
array( '%d', '%d', '%s', '%s', '%s' )
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Using $wpdb->insert for custom plugin table writes.
$insert_data = array(
'form_id' => $form_id,
'submission_data' => \wp_json_encode( $data ),
'ip_address' => $ip_address,
'user_agent' => $user_agent,
);
$insert_format = array( '%d', '%s', '%s', '%s' );
if ( $user_id ) {
$insert_data['user_id'] = $user_id;
$insert_format[] = '%d';
}
$result = $wpdb->insert( $this->table_name, $insert_data, $insert_format );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Database.php` around lines 50 - 61, The insert currently passes
user_id with a '%d' format which coerces null to 0; change the insert to omit
the 'user_id' key when $user_id is null so SQL NULL (or the column default) is
used instead. Concretely, build the data array and the formats array before
calling $wpdb->insert: include 'user_id' => $user_id and a '%d' format only when
$user_id !== null, otherwise do not add that key/format; keep the other keys
('form_id','submission_data','ip_address','user_agent') unchanged and call
$wpdb->insert($this->table_name, $data, $formats).

Comment thread includes/Plugin.php
Comment on lines +29 to +34
private function __construct() {
$this->init_components();

\register_activation_hook( dirname( __DIR__ ) . '/multi-step-form-builder.php', array( $this, 'activate' ) );
\register_deactivation_hook( dirname( __DIR__ ) . '/multi-step-form-builder.php', array( $this, 'deactivate' ) );
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find PHP files in the repository root that match the plugin filename pattern
fd --max-depth 1 --type f -e php

Repository: jpjuliao/multi-step-form-builder

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Read the Plugin.php file to verify the exact code in lines 29-34
head -40 includes/Plugin.php | tail -15

Repository: jpjuliao/multi-step-form-builder

Length of output: 589


Activation/deactivation hooks reference the wrong filename — they will never fire.

The paths in register_activation_hook() and register_deactivation_hook() reference multi-step-form-builder.php, but the actual main plugin file is jpjuliao-multi-step-form-builder.php. Since WordPress matches these hooks against the exact main plugin file path, the activate() and deactivate() callbacks will silently never execute.

Proposed fix
-		\register_activation_hook( dirname( __DIR__ ) . '/multi-step-form-builder.php', array( $this, 'activate' ) );
-		\register_deactivation_hook( dirname( __DIR__ ) . '/multi-step-form-builder.php', array( $this, 'deactivate' ) );
+		\register_activation_hook( dirname( __DIR__ ) . '/jpjuliao-multi-step-form-builder.php', array( $this, 'activate' ) );
+		\register_deactivation_hook( dirname( __DIR__ ) . '/jpjuliao-multi-step-form-builder.php', array( $this, 'deactivate' ) );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private function __construct() {
$this->init_components();
\register_activation_hook( dirname( __DIR__ ) . '/multi-step-form-builder.php', array( $this, 'activate' ) );
\register_deactivation_hook( dirname( __DIR__ ) . '/multi-step-form-builder.php', array( $this, 'deactivate' ) );
}
private function __construct() {
$this->init_components();
\register_activation_hook( dirname( __DIR__ ) . '/jpjuliao-multi-step-form-builder.php', array( $this, 'activate' ) );
\register_deactivation_hook( dirname( __DIR__ ) . '/jpjuliao-multi-step-form-builder.php', array( $this, 'deactivate' ) );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Plugin.php` around lines 29 - 34, The activation/deactivation hooks
registered in the Plugin::__construct are using the wrong filename
("multi-step-form-builder.php") so activate()/deactivate() will never run;
update the two calls to register_activation_hook and register_deactivation_hook
in the __construct method to reference the real main plugin file name
("jpjuliao-multi-step-form-builder.php") (or, alternatively, derive the correct
main plugin path dynamically) so WordPress can match and invoke the
Plugin::activate and Plugin::deactivate callbacks.

Comment thread includes/PostType.php
if ( $post->ID ) {
echo '<p>' . esc_html__( 'Use this shortcode to display the form:', 'jpjuliao-multi-step-form-builder' ) . '</p>';
echo '<code>[multi_step_form id="' . esc_attr( $post->ID ) . '"]</code>';
echo '<p><button type="button" class="button button-small" onclick="navigator.clipboard.writeText(\'[multi_step_form id=&quot;' . esc_attr( $post->ID ) . '&quot;]\'))">Copy Shortcode</button></p>';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

JavaScript syntax error — extra closing parenthesis breaks the copy button.

The onclick handler has a mismatched parenthesis: writeText(\'...\')) has two ) but only one ( in writeText(. This will throw a SyntaxError in the browser, making the button non-functional.

Proposed fix
-			echo '<p><button type="button" class="button button-small" onclick="navigator.clipboard.writeText(\'[multi_step_form id=&quot;' . esc_attr( $post->ID ) . '&quot;]\'))">Copy Shortcode</button></p>';
+			echo '<p><button type="button" class="button button-small" onclick="navigator.clipboard.writeText(\'[multi_step_form id=&quot;' . esc_attr( $post->ID ) . '&quot;]\')">Copy Shortcode</button></p>';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/PostType.php` at line 80, In the echo that renders the Copy
Shortcode button in includes/PostType.php (the HTML button with the onclick
handler using navigator.clipboard.writeText), remove the extra closing
parenthesis so the onclick calls navigator.clipboard.writeText(...) only once;
ensure the single-quoted string and concatenated esc_attr($post->ID) remain
correctly escaped and the final onclick attribute ends with a single closing
quote and angle bracket rather than an extra ')'.

Comment thread includes/RESTapi.php
Comment on lines +158 to +178
public function get_submissions( \WP_REST_Request $request ): \WP_REST_Response {
$form_id = $request->get_param( 'id' );
$page = $request->get_param( 'page' )
? (int) $request->get_param( 'page' ) : 1;
$per_page = $request->get_param( 'per_page' )
? (int) $request->get_param( 'per_page' ) : 50;

$offset = ( $page - 1 ) * $per_page;

$submissions = $this->database->get_submissions( $form_id, $per_page, $offset );
$total = $this->database->get_submission_count( $form_id );

return \rest_ensure_response(
array(
'submissions' => $submissions,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => ceil( $total / $per_page ),
)
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Division-by-zero when per_page=0.

$request->get_param('per_page') can be "0", which is falsy so it falls through to the default of 50. Actually wait — (int) "0" is 0, and 0 is falsy, so the ternary at line 162-163 evaluates to 50. So the default path is safe.

However, if someone passes per_page=-1, (int) "-1" is -1 which is truthy, so $per_page becomes -1. $offset becomes (page - 1) * -1, and the SQL LIMIT -1 OFFSET ... is invalid in MySQL, which would produce an error.

Consider clamping per_page to a positive range.

Proposed fix
 		$per_page = $request->get_param( 'per_page' )
 			? (int) $request->get_param( 'per_page' ) : 50;
+		$per_page = max( 1, min( $per_page, 100 ) );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function get_submissions( \WP_REST_Request $request ): \WP_REST_Response {
$form_id = $request->get_param( 'id' );
$page = $request->get_param( 'page' )
? (int) $request->get_param( 'page' ) : 1;
$per_page = $request->get_param( 'per_page' )
? (int) $request->get_param( 'per_page' ) : 50;
$offset = ( $page - 1 ) * $per_page;
$submissions = $this->database->get_submissions( $form_id, $per_page, $offset );
$total = $this->database->get_submission_count( $form_id );
return \rest_ensure_response(
array(
'submissions' => $submissions,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => ceil( $total / $per_page ),
)
);
public function get_submissions( \WP_REST_Request $request ): \WP_REST_Response {
$form_id = $request->get_param( 'id' );
$page = $request->get_param( 'page' )
? (int) $request->get_param( 'page' ) : 1;
$per_page = $request->get_param( 'per_page' )
? (int) $request->get_param( 'per_page' ) : 50;
$per_page = max( 1, min( $per_page, 100 ) );
$offset = ( $page - 1 ) * $per_page;
$submissions = $this->database->get_submissions( $form_id, $per_page, $offset );
$total = $this->database->get_submission_count( $form_id );
return \rest_ensure_response(
array(
'submissions' => $submissions,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => ceil( $total / $per_page ),
)
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/RESTapi.php` around lines 158 - 178, In get_submissions, clamp and
validate the computed $per_page after casting to int to avoid zero/negative or
absurd values: cast $request->get_param('per_page') to int, then if $per_page <=
0 set it to a safe default (e.g. 50), and optionally cap it with a max (e.g.
1000); use this validated $per_page when computing $offset and total_pages so
LIMIT and ceil( $total / $per_page ) never receive zero/negative values. Ensure
the change touches the $per_page computation and any uses in offset and
total_pages in get_submissions.

Comment thread includes/RESTapi.php
Comment on lines +200 to +221
foreach ( $form_config['steps'] as $step ) {
if ( empty( $step['fields'] ) ) {
continue;
}

foreach ( $step['fields'] as $field ) {
$field_name = $field['name'];
$field_value = isset( $data[ $field_name ] ) ? $data[ $field_name ] : '';

if ( ! empty( $field['required'] ) && empty( $field_value ) ) {
$errors[ $field_name ] = $field['label'] . ' is required';
continue;
}

if ( 'email' === $field['type'] && ! empty( $field_value ) && ! \is_email( $field_value ) ) {
$errors[ $field_name ] = 'Please enter a valid email address';
}

if ( 'url' === $field['type'] && ! empty( $field_value ) && ! filter_var( $field_value, FILTER_VALIDATE_URL ) ) {
$errors[ $field_name ] = 'Please enter a valid URL';
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Accessing $field['name'], $field['label'], $field['type'] without isset guards.

If any field in the form config is malformed (missing name, label, or type keys), this will emit PHP warnings and potentially produce incorrect validation results. Defensive checks or early continue on missing keys would harden this against corrupt config data.

Proposed fix
 		foreach ( $step['fields'] as $field ) {
+			if ( empty( $field['name'] ) ) {
+				continue;
+			}
 			$field_name  = $field['name'];
 			$field_value = isset( $data[ $field_name ] ) ? $data[ $field_name ] : '';
 
 			if ( ! empty( $field['required'] ) && empty( $field_value ) ) {
-				$errors[ $field_name ] = $field['label'] . ' is required';
+				$errors[ $field_name ] = ( $field['label'] ?? $field_name ) . ' is required';
 				continue;
 			}
 
-			if ( 'email' === $field['type'] && ! empty( $field_value ) && ! \is_email( $field_value ) ) {
+			if ( isset( $field['type'] ) && 'email' === $field['type'] && ! empty( $field_value ) && ! \is_email( $field_value ) ) {
 				$errors[ $field_name ] = 'Please enter a valid email address';
 			}
 
-			if ( 'url' === $field['type'] && ! empty( $field_value ) && ! filter_var( $field_value, FILTER_VALIDATE_URL ) ) {
+			if ( isset( $field['type'] ) && 'url' === $field['type'] && ! empty( $field_value ) && ! filter_var( $field_value, FILTER_VALIDATE_URL ) ) {
 				$errors[ $field_name ] = 'Please enter a valid URL';
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
foreach ( $form_config['steps'] as $step ) {
if ( empty( $step['fields'] ) ) {
continue;
}
foreach ( $step['fields'] as $field ) {
$field_name = $field['name'];
$field_value = isset( $data[ $field_name ] ) ? $data[ $field_name ] : '';
if ( ! empty( $field['required'] ) && empty( $field_value ) ) {
$errors[ $field_name ] = $field['label'] . ' is required';
continue;
}
if ( 'email' === $field['type'] && ! empty( $field_value ) && ! \is_email( $field_value ) ) {
$errors[ $field_name ] = 'Please enter a valid email address';
}
if ( 'url' === $field['type'] && ! empty( $field_value ) && ! filter_var( $field_value, FILTER_VALIDATE_URL ) ) {
$errors[ $field_name ] = 'Please enter a valid URL';
}
}
foreach ( $form_config['steps'] as $step ) {
if ( empty( $step['fields'] ) ) {
continue;
}
foreach ( $step['fields'] as $field ) {
if ( empty( $field['name'] ) ) {
continue;
}
$field_name = $field['name'];
$field_value = isset( $data[ $field_name ] ) ? $data[ $field_name ] : '';
if ( ! empty( $field['required'] ) && empty( $field_value ) ) {
$errors[ $field_name ] = ( $field['label'] ?? $field_name ) . ' is required';
continue;
}
if ( isset( $field['type'] ) && 'email' === $field['type'] && ! empty( $field_value ) && ! \is_email( $field_value ) ) {
$errors[ $field_name ] = 'Please enter a valid email address';
}
if ( isset( $field['type'] ) && 'url' === $field['type'] && ! empty( $field_value ) && ! filter_var( $field_value, FILTER_VALIDATE_URL ) ) {
$errors[ $field_name ] = 'Please enter a valid URL';
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/RESTapi.php` around lines 200 - 221, The loop over
$form_config['steps'] accesses $field['name'], $field['label'], and
$field['type'] without guards; add defensive checks at the start of the inner
loop (inside the foreach ( $step['fields'] as $field ) block) to verify
isset($field['name']) and isset($field['label']) (and optionally
isset($field['type'])); if any required key is missing, skip the field
(continue) and optionally log or collect an error for malformed config; then use
the guarded $field_name, $field['label'] and $field['type'] for the existing
required/email/url validations (keeping the existing checks that call is_email
and filter_var).

Comment thread phpcs.xml
Comment on lines +16 to +19
<exclude name="WordPress.Files.ClassStructure.MissingNamespace"/>
<exclude name="WordPress.Files.FileHeader.Missing"/>
<exclude name="WordPress.Files.ClassStructure.MissingClassDocBlock"/>
<exclude name="WordPress.Files.FunctionUse.MultipleUseOnly"/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and examine the phpcs.xml file
find . -name "phpcs.xml" -type f | head -5

Repository: jpjuliao/multi-step-form-builder

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Check if phpcs is available and list available sniffs
which phpcs || echo "phpcs not in PATH, checking vendor/bin..."
PHPCS=$(find . -path "*/vendor/bin/phpcs" -type f 2>/dev/null | head -1)
if [ -n "$PHPCS" ]; then
  echo "Found phpcs at: $PHPCS"
  echo ""
  echo "=== All WordPress.Files sniffs ==="
  "$PHPCS" -e --standard=WordPress 2>/dev/null | grep -i "wordpress.files" || echo "No WordPress.Files sniffs found"
else
  echo "phpcs not found in vendor directories"
  find . -path "*/vendor*" -name "phpcs" -o -path "*/vendor*" -name "phpcs.phar" 2>/dev/null | head -5
fi

Repository: jpjuliao/multi-step-form-builder

Length of output: 216


🏁 Script executed:

#!/bin/bash
# Read the phpcs.xml file
cat -n ./phpcs.xml

Repository: jpjuliao/multi-step-form-builder

Length of output: 1406


🏁 Script executed:

#!/bin/bash
# Check composer files for WPCS version information
ls -la | grep composer
echo "=== composer.json ==="
cat composer.json 2>/dev/null | head -50 || echo "No composer.json found"
echo ""
echo "=== composer.lock snippet ==="
cat composer.lock 2>/dev/null | grep -A 5 "wpcs\|wordpress-coding-standards" | head -30 || echo "No composer.lock found"

Repository: jpjuliao/multi-step-form-builder

Length of output: 1336


🏁 Script executed:

#!/bin/bash
# Search for any WPCS documentation or rule references in the codebase
find . -type f \( -name "*.md" -o -name "*.txt" \) -exec grep -l "WordPress.*Files\|WPCS\|sniff" {} \; 2>/dev/null | head -10

Repository: jpjuliao/multi-step-form-builder

Length of output: 58


🌐 Web query:

WPCS 3.3.0 WordPress.Files sniff codes rules list

💡 Result:

In WPCS 3.3.0, the WordPress.Files standard includes (at least) this sniff:

  • WordPress.Files.FileName (raw.githubusercontent.com)
    Error codes / rules it enforces (as of 3.3.0):
    • WordPress.Files.FileName.NotHyphenatedLowercase (file names should be lowercase with hyphens) (raw.githubusercontent.com)
    • WordPress.Files.FileName.InvalidClassFileName (class files should use a class- prefix and reflect the class name) (raw.githubusercontent.com)
    • WordPress.Files.FileName.InvalidTemplateTagFileName (template-tag files should have a -template suffix in specific cases) (raw.githubusercontent.com)

If you want to generate the authoritative list from your local WPCS 3.3.0 install (including any additional WordPress.Files.* sniffs pulled in via the ruleset), run:

vendor/bin/phpcs -e --standard=WordPress | grep -E '^WordPress\.Files\.'
``` ([make.wordpress.org](https://make.wordpress.org/core/2023/08/21/wordpresscs-3-0-0-is-now-available/?utm_source=openai))

Citations:

- 1: https://raw.githubusercontent.com/WordPress/WordPress-Coding-Standards/3.3.0/WordPress/Sniffs/Files/FileNameSniff.php
- 2: https://raw.githubusercontent.com/WordPress/WordPress-Coding-Standards/3.3.0/WordPress/Sniffs/Files/FileNameSniff.php
- 3: https://raw.githubusercontent.com/WordPress/WordPress-Coding-Standards/3.3.0/WordPress/Sniffs/Files/FileNameSniff.php
- 4: https://raw.githubusercontent.com/WordPress/WordPress-Coding-Standards/3.3.0/WordPress/Sniffs/Files/FileNameSniff.php
- 5: https://make.wordpress.org/core/2023/08/21/wordpresscs-3-0-0-is-now-available/?utm_source=openai

---

🌐 Web query:

`WordPress Coding Standards ClassStructure sniff codes MissingNamespace`

💡 Result:

In PHPCS/WPCS terminology, **`MissingNamespace` is an “error code”** emitted by a sniff, and the fully-qualified “sniff code” looks like:

- `WordPress.Classes.ClassStructure.MissingNamespace`

To **see the exact sniff + code** that’s triggering in your project, run PHPCS with `-s` (shows sniff codes in reports):  
`phpcs -s ...` ([github.com](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage?utm_source=openai))

To **see which sniffs exist in a standard/ruleset**, use:  
`phpcs -e --standard=WordPress` ([github.com](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage?utm_source=openai))

To **fix** `...MissingNamespace`, add a namespace declaration near the top of the file (after `<?php` and `declare(...)`, before `use` / class declaration), e.g.:
```php
<?php
namespace Vendor\Package;

class My_Class {}

If you don’t want to enforce this rule, exclude just that code in your project’s .phpcs.xml / phpcs.xml.dist (WPCS supports custom rulesets and exclusions): (github.com)

<rule ref="WordPress">
  <exclude name="WordPress.Classes.ClassStructure.MissingNamespace"/>
</rule>

Citations:


🌐 Web query:

WPCS WordPress.Files.FileHeader sniff codes available

💡 Result:

To see which message “source codes” (a.k.a. PHPCS error codes) exist for WordPress.Files.FileHeader, the practical way is to let PHPCS print them for real violations:

  1. Run the sniff and show sources (codes) in the report
vendor/bin/phpcs -s --standard=WordPress --sniffs=WordPress.Files.FileHeader path/to/file-or-dir

The -s flag makes PHPCS include the source for each message (i.e. the code you can use in <exclude name="..."/>). This is the same -s shown in the WPCS usage examples. [1]

  1. If you need a “full list” of codes (even ones you don’t currently trigger)
    There isn’t a separate official published list per sniff; you generate it by inspecting the sniff implementation in your installed WPCS package and collecting the $code strings passed to addError()/addWarning() in that sniff file (then the usable exclude name will be WordPress.Files.FileHeader.<ThatCode>). [1]

Tip: if your goal is “what can I exclude?”, step (1) is usually sufficient because it outputs exactly the codes you’re hitting.

[1] (feicode.com)

Citations:


🌐 Web query:

WPCS 3.3.0 WordPress.Classes.ClassStructure.MissingNamespace sniff

💡 Result:

I wasn’t able to find any public documentation or changelog entries for the specific WPCS 3.3.0 sniff WordPress.Classes.ClassStructure.MissingNamespace via web search (the search returned no results), so I can’t cite an authoritative source for its exact behavior.

What that sniff name typically means in PHPCS terms:

  • It flags class/interface/trait/enum declarations that are not inside a PHP namespace ...;, i.e., “global namespace” code.

How to address it

Option A — Add a namespace (the “fix”)

At the top of the file (after <?php and any declare(...)), add a namespace and (usually) update class references/autoloading:

<?php
declare(strict_types=1);

namespace Vendor\Plugin;

class My_Class {}

Option B — If your project intentionally doesn’t use namespaces, exclude the sniff

In phpcs.xml / phpcs.xml.dist:

<ruleset>
  <rule ref="WordPress">
    <exclude name="WordPress.Classes.ClassStructure.MissingNamespace"/>
  </rule>
</ruleset>

Or via CLI:

phpcs --exclude=WordPress.Classes.ClassStructure.MissingNamespace .

If you paste your PHPCS output + the exact file/class it flags

…I can tell you whether it’s being triggered by a genuine missing namespace, an unexpected file pattern (tests/tools), or a ruleset that’s pulling in a stricter WPCS subset than you intended.


🌐 Web query:

site:github.com WordPress/WordPress-Coding-Standards 3.3.0 ClassStructure sniff

💡 Result:

ClassStructure is not a sniff in WordPress/WordPress-Coding-Standards (WPCS) v3.3.0—it’s the sniff SlevomatCodingStandard.Classes.ClassStructure from the slevomat/coding-standard project. The WPCS 3.3.0 release notes don’t list any ClassStructure sniff (they cover other additions/changes like updates to EscapeOutput, PHP version requirements, etc.). (github.com)

Citations:


🌐 Web query:

WPCS WordPress.Files FunctionUse sniff exists

💡 Result:

No — WPCS does not ship a WordPress.Files.FunctionUse sniff.

  • Trying to load WordPress/Sniffs/Files/FunctionUseSniff.php from the WPCS repo returns 404 Not Found, i.e. that sniff file doesn’t exist in WPCS. [1]
  • WPCS does have file-related sniffs under WordPress.Files.* (for example, WordPress.Files.FileName). [2]
  • For “function use” restrictions/recommendations (including filesystem-related ones like file_get_contents()), WPCS uses WordPress.WP.AlternativeFunctions. [3][4]

If you’re seeing WPCS.WordPress.Files.FunctionUse referenced somewhere, it’s likely a typo or an outdated/custom sniff name; in practice you typically want WordPress.WP.AlternativeFunctions (and possibly other specific WordPress.WP.* sniffs depending on what you’re trying to ban/allow). [3][4]

Sources
[1] https://raw.githubusercontent.com/WordPress/WordPress-Coding-Standards/develop/WordPress/Sniffs/Files/FunctionUseSniff.php
[2] https://raw.githubusercontent.com/WordPress/WordPress-Coding-Standards/develop/WordPress/Sniffs/Files/FileNameSniff.php
[3] https://raw.githubusercontent.com/WordPress/WordPress-Coding-Standards/develop/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php
[4] https://github.com/WordPress/WordPress-Coding-Standards/releases


🌐 Web query:

WPCS WordPress.Files.FileHeader sniff message codes MissingPackageTag

💡 Result:

MissingPackageTag is the PHPCS message code for the “missing @package tag in the file docblock” check in the WPCS WordPress.Files.FileHeader sniff. To confirm the exact code your install reports, run PHPCS with -s (it prints the sniff/message codes next to each violation). [1]

To ignore just that message code in your project ruleset (e.g. phpcs.xml / .phpcs.xml.dist), exclude the message by name: [1]

<rule ref="WordPress.Files.FileHeader">
  <exclude name="WordPress.Files.FileHeader.MissingPackageTag"/>
</rule>

If you only want to ignore it in a specific spot, you can also use an inline ignore comment (e.g. phpcs:ignore ...) rather than changing the whole ruleset. [2]

Sources: [1] [2]


🌐 Web query:

site:github.com/WordPress/WordPress-Coding-Standards WordPress/Sniffs/Files/FileHeaderSniff.php

💡 Result:

WordPress/Sniffs/Files/FileHeaderSniff.php does not exist in the current WordPress/WordPress-Coding-Standards repository (both the master and develop blob URLs return 404 Not Found). ()

In recent WordPressCS versions, file-header checks are handled via the upstream PHPCS PSR12.Files.FileHeader sniff being included (select error codes) in the WordPress-Core ruleset. (github.com)

Citations:


🌐 Web query:

WPCS 3.3.0 release notes sniff list WordPress.Files

💡 Result:

WPCS 3.3.0 release notes (tag 3.3.0, released 2025-11-25) do not mention any additions/changes/removals specifically in the WordPress.Files sniff category; the items called out are mainly in WordPress.Security.EscapeOutput, ruleset changes, dependency/min-version bumps, and various WP-up-to-6.9 updates. [1]

From the 3.3.0 code, the WordPress.Files sniff explicitly visible/confirmed is:

  • WordPress.Files.FileName (error codes include NotHyphenatedLowercase, InvalidClassFileName, InvalidTemplateTagFileName) [2]

Sources:
[1] WPCS 3.3.0 GitHub release notes — WordPress/WordPress-Coding-Standards releases
[2] WordPress/Sniffs/Files/FileNameSniff.php in tag 3.3.0 (raw file)


🌐 Web query:

WPCS 3.3.0 all available sniff codes complete list

💡 Result:

WPCS doesn’t publish a single static “complete sniff-code list” in the release notes; the authoritative way to get the full, exact list for WPCS 3.3.0 is to have that version installed and let PHP_CodeSniffer enumerate the sniffs per ruleset.

1) Install exactly WPCS 3.3.0

composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev wp-coding-standards/wpcs:3.3.0

(WPCS is intended to be installed via Composer.) (github.com)

2) See which WPCS standards (rulesets) are available

WPCS ships these rulesets: WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra. (github.com)

Confirm what PHPCS sees on your machine:

vendor/bin/phpcs -i

3) Print the complete sniff-code list (per ruleset)

vendor/bin/phpcs -e --standard=WordPress
vendor/bin/phpcs -e --standard=WordPress-Core
vendor/bin/phpcs -e --standard=WordPress-Docs
vendor/bin/phpcs -e --standard=WordPress-Extra

(Using WordPress gives the “complete set” of sniffs in the project.) (github.com)

Optional: save to files

vendor/bin/phpcs -e --standard=WordPress       > wpcs-3.3.0-sniffs-wordpress.txt
vendor/bin/phpcs -e --standard=WordPress-Core  > wpcs-3.3.0-sniffs-core.txt
vendor/bin/phpcs -e --standard=WordPress-Docs  > wpcs-3.3.0-sniffs-docs.txt
vendor/bin/phpcs -e --standard=WordPress-Extra > wpcs-3.3.0-sniffs-extra.txt

Note (what changed in 3.3.0)

In WPCS 3.3.0, WordPress-Extra includes (at least) these newly-added sniff codes: Universal.Attributes.BracketSpacing and Universal.Attributes.DisallowAttributeParentheses. (github.com)

Citations:


Remove the four invalid PHPCS rule exclusions (lines 16–19) from phpcs.xml; they don't exist in WPCS 3.3.0 and are silently ignored.

These exclusion names do not correspond to actual WPCS 3.3.0 sniff codes and will have no effect:

Line Entry Why it's invalid
16 WordPress.Files.ClassStructure.MissingNamespace ClassStructure sniff does not exist in WPCS; it comes from the separate SlevomatCodingStandard package. WPCS has no WordPress.Files.ClassStructure.* sniffs.
17 WordPress.Files.FileHeader.Missing No Missing code exists under FileHeader. Valid codes are MissingPackageTag, WrongStyle, etc.
18 WordPress.Files.ClassStructure.MissingClassDocBlock ClassStructure is not a WPCS sniff (see line 16). Class doc-block enforcement is via Squiz.Commenting.ClassComment (already excluded on line 22).
19 WordPress.Files.FunctionUse.MultipleUseOnly No FunctionUse sniff exists under WordPress.Files; function-use rules are in WordPress.WP.AlternativeFunctions.

Delete lines 16–19 entirely. Line 15 (WordPress.Files.FileName) is valid and should be retained.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@phpcs.xml` around lines 16 - 19, Remove the four invalid PHPCS exclusion
entries from phpcs.xml by deleting the <exclude> elements with names
"WordPress.Files.ClassStructure.MissingNamespace",
"WordPress.Files.FileHeader.Missing",
"WordPress.Files.ClassStructure.MissingClassDocBlock", and
"WordPress.Files.FunctionUse.MultipleUseOnly" (they are not valid WPCS 3.3.0
sniff codes and are silently ignored); keep the valid "WordPress.Files.FileName"
exclusion intact and ensure no other valid exclusions are accidentally removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant