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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 7 additions & 1 deletion .github/workflows/syntax.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,10 @@ jobs:
with:
php-version: ${{ matrix.php }}
- name: Check PHP ${{ matrix.multisite }} syntax
run: find -L . -path ./vendor -prune -o -path ./tests -prune -o -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l
# lib/vendor is pruned as well as vendor: the vendored MCP adapter and
# schema packages use PHP 7.4 syntax, so they do not parse on the 7.0
# leg of this matrix. That is expected rather than a defect — Formidable
# itself still supports 7.0, and FrmMcpCompat refuses to load the
# adapter below PHP 7.4 (FrmMcpCompat::MIN_PHP_ID), so none of those
# files is ever parsed on a PHP that cannot read them.
run: find -L . -path ./vendor -prune -o -path ./lib/vendor -prune -o -path ./tests -prune -o -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l
46 changes: 45 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,52 @@ node_modules/*
npm-debug.log

# Composer
vendor
/vendor
composer.lock
/lib/composer.lock
!lib/vendor

# lib/vendor is committed because the MCP adapter has to ship inside the plugin
# zip, but only the code the plugin loads belongs here. The vendored packages'
# own CI, docs, tooling, and test suites are excluded from the release, so they
# are not tracked either.
#
# Nothing below is autoloadable: composer maps only mcp-adapter/includes,
# php-mcp-schema/src, and jetpack-autoloader/src. The jetpack autoloader is a
# require of the adapter that nothing in it calls, so it is here to keep
# composer install reproducible, never to be loaded.
#
# The depth is explicit on every rule so none of them can reach the plugin's own
# .github, docs, or tests directories.
lib/vendor/*/*/.github/
lib/vendor/*/*/docs/
lib/vendor/*/*/generator/
lib/vendor/*/*/skill/
lib/vendor/*/*/tests/

# Each vendored package's own build tooling and manifests. Composer resolves
# through lib/vendor/composer/, never these, and the zip already drops them. The
# depth keeps the plugin's own composer.json tracked, since that is what
# regenerates lib/vendor.
lib/vendor/*/*/composer.json
lib/vendor/*/*/composer.lock
lib/vendor/*/*/package.json
lib/vendor/*/*/package-lock.json
lib/vendor/*/*/phpstan.neon.dist
lib/vendor/*/*/phpunit.xml.dist
lib/vendor/*/*/.phpcs.xml.dist
lib/vendor/*/*/*.md
lib/vendor/*/*/.editorconfig
lib/vendor/*/*/.gitattributes
lib/vendor/*/*/.npmrc
lib/vendor/*/*/.nvmrc
lib/vendor/*/*/.prettierignore
lib/vendor/*/*/.prettierrc.js
lib/vendor/*/*/.wp-env.json
lib/vendor/*/*/.wp-env.test.json
lib/vendor/*/*/readme.txt
!lib/vendor/*/*/LICENSE.md
!lib/vendor/*/*/LICENSE.txt

# PHPUnit
.phpunit.result.cache
Expand Down
6 changes: 6 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Vendored third-party packages ship inside the plugin so the MCP adapter is
# available in the release zip, but their spelling is not ours to correct and
# editing them would be overwritten by the next composer install.
[files]
extend-exclude = ["lib/vendor/**"]

[type.po]
extend-glob = ["*.po"]
check-file = false
Expand Down
191 changes: 191 additions & 0 deletions classes/controllers/FrmAbilitiesController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
die( 'You are not allowed to call this page directly.' );
}

/**
* Registers the Formidable abilities for the WordPress Abilities API.
*
* Abilities let AI assistants and automation tools discover and run Formidable
* operations, over the MCP server this plugin registers or over wp-abilities/v1.
* Each domain (forms, fields, entries, styles, form actions) lives in its own
* FrmAbilities*Controller. This class registers the shared category, delegates
* to the domain controllers, and owns the gate every plugin in the family
* checks before registering anything of its own.
*
* Pro and Views register the domains that belong to their features, and the API
* add-on registers whatever is left over on a site where those plugins are too
* old to have them. All three ask owns() first, which is what keeps two plugins
* from claiming one ability name.
*
* @since x.x
*/
class FrmAbilitiesController {

/**
* Ability category every Formidable ability is registered under.
*
* @var string
*/
const CATEGORY = 'formidable-forms';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Visibility should be explicitly set for `CATEGORY` constant


Visibility (also know as Access Modifiers) can be used to define where it can be accessed. There are three access modifiers available in PHP:

  • public - The class members can be accessed from everywhere. This is default.
  • protected - The class members can be accessed within the class and by classes derived from that class.
  • private - The class members can only be accessed within the class.

The class members(properties, constants, or methods) declared without any explicit visibility keyword are by default considered as public. It is recommended to set visibility explicitly, which increases code readability. In addition, it gives the developer a mental model of where the class member would be accessible, which also leads to a better API design and makes sure that you are not making something public which isn't supposed to be.
Also, as per PSR-12: Extended Coding Style, visibility should be explicitly declared with all class properties, constants and methods.


/**
* @since x.x
*
* @return void
*/
public static function load_hooks() {
if ( ! function_exists( 'wp_register_ability' ) ) {
// The Abilities API is part of WordPress 7.0. Without it there is
// nothing to register against, on any version of any of these plugins.
return;
}

add_action( 'wp_abilities_api_categories_init', 'FrmAbilitiesController::register_categories' );
add_action( 'wp_abilities_api_init', 'FrmAbilitiesController::register_abilities' );
}

/**
* Check whether the Formidable family may register abilities at all.
*
* Pro, Views, and the API add-on all consult this before registering their
* own domains, so the MCP setting is one switch for the whole AI surface
* rather than one per plugin.
*
* @since x.x
*
* @return bool
*/
public static function is_active() {
if ( FrmMcpController::api_addon_owns_mcp() ) {
// An API add-on that predates the move registers the whole surface
// itself, including the domains that now belong to Pro and Views.
return false;
}

return FrmMcpController::is_enabled();
}

/**
* Map each ability domain to the class that owns it.
*
* Formidable owns the domains its own features cover. Pro and Views add
* theirs on the frm_ability_domains filter, which is also how the API add-on
* finds out whether a domain has an owner on this site or whether it should
* keep serving that domain itself.
*
* @since x.x
*
* @return array Domain names mapped to the controller class that registers them.
*/
public static function domains() {
$domains = array(
'forms' => 'FrmAbilitiesFormsController',
'fields' => 'FrmAbilitiesFieldsController',
'entries' => 'FrmAbilitiesEntriesController',
'styles' => 'FrmAbilitiesStylesController',
'form-actions' => 'FrmAbilitiesFormActionsController',
);

/**
* Filter the ability domains and the classes that own them.
*
* Pro adds entry-writes, styles-pro, stats, and applications. Views adds
* views and view-layouts. A plugin that adds a domain here is stating
* that it registers every ability in it, and the API add-on stops
* registering that domain in response.
*
* @since x.x
*
* @param array<string, string> $domains Domain names mapped to the controller class that registers them.
*/
return (array) apply_filters( 'frm_ability_domains', $domains );
}

/**
* Check whether one ability domain has an owner on this site.
*
* @since x.x
*
* @param string $domain Domain name, such as forms or view-layouts.
*
* @return bool
*/
public static function owns( $domain ) {
if ( ! self::is_active() ) {
return false;
}

$domains = self::domains();

return isset( $domains[ $domain ] ) && class_exists( $domains[ $domain ] );
}

/**
* Register the shared ability category.
*
* @since x.x
* @see action hook wp_abilities_api_categories_init
*
* @return void
*/
public static function register_categories() {
if ( ! self::is_active() ) {
return;
}

// Pro, Views, and the API add-on all register into this category, so
// whichever of them runs first would otherwise register it twice. The
// registry is asked directly because wp_get_ability_category() is a
// getter, not a check: it raises _doing_it_wrong for a category that is
// not there, which is the normal answer here.
if ( self::category_is_registered() ) {
return;
}

wp_register_ability_category(
self::CATEGORY,
array(
'label' => __( 'Formidable Forms', 'formidable' ),
'description' => __( 'Abilities for managing Formidable forms and entries.', 'formidable' ),
)
);
}

/**
* Check whether the shared category has already been registered.
*
* @since x.x
*
* @return bool
*/
private static function category_is_registered() {
if ( ! class_exists( 'WP_Ability_Categories_Registry' ) ) {
return false;
}

$registry = WP_Ability_Categories_Registry::get_instance();

return $registry && $registry->is_registered( self::CATEGORY );
}

/**
* Register every ability Formidable itself owns.
*
* @since x.x
* @see action hook wp_abilities_api_init
*
* @return void
*/
public static function register_abilities() {
if ( ! self::is_active() ) {
return;
}

FrmAbilitiesFormsController::register_abilities();
FrmAbilitiesFieldsController::register_abilities();
FrmAbilitiesEntriesController::register_abilities();
FrmAbilitiesStylesController::register_abilities();
FrmAbilitiesFormActionsController::register_abilities();
}
}
Loading
Loading