Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/hardiksolanki90/acf/archive/refs/heads/main.zipReadme
RudraACF
Production-grade WordPress custom fields framework — ACF Pro–compatible alternative built on PHP 8.1+, PSR-4, repository pattern, and object-cache-first design.
Requirements
| Dependency | Minimum |
|---|---|
| PHP | 8.1 |
| WordPress | 6.3 |
| Composer | 2.x |
Installation
# 1. Copy plugin folder into wp-content/plugins/
cp -r myacf /path/to/wp-content/plugins/
# 2. Install PHP dependencies
cd /path/to/wp-content/plugins/myacf
composer install --no-dev --optimize-autoloader
# 3. Activate via WP Admin → Plugins, or WP-CLI:
wp plugin activate myacf
Activation runs database migrations automatically (creates wp_myacf_field_groups, wp_myacf_fields, wp_myacf_field_values).
Architecture Overview
myacf/
├── src/
│ ├── Admin/ # Admin pages, meta boxes, AJAX handlers
│ ├── Api/ # FieldApi service + global helper functions
│ ├── Compatibility/ # PHP/WP version + conflict detection
│ ├── Database/ # Models, Repositories, Migrations
│ ├── Fields/ # FieldInterface, BaseField, 16 field types
│ ├── ImportExport/ # JSON Exporter + Importer
│ ├── Licensing/ # LicenseManager + WP UpdateChecker
│ ├── OptionsPages/ # Options page registry + manager
│ ├── Performance/ # BulkLoader (cache pre-warming)
│ ├── Pro/ # ConditionalLogic
│ ├── Rest/ # REST API (myacf/v1)
│ ├── Rules/ # Rules Engine + matchers
│ ├── Sdk/ # Third-party extension SDK
│ ├── Services/ # Container, Capability, Sanitizer, Escaper, Nonce
│ └── Plugin.php # Bootstrap + DI wiring
├── tests/Unit/ # 265 PHPUnit tests
└── myacf.php # Plugin entry point
Key patterns:
- Repository pattern — all DB access via
FieldGroupRepository,FieldRepository,FieldValueRepository; never raw$wpdbcalls outside repos - Object cache — every repo read is cache-first (
wp_cache_get/set);CACHE_GROUPconstants per repo; TTL 3600 s - DI Container — custom
Container::singleton()/make()with no Reflection; all services resolved lazily - FieldInterface contract —
getType / getLabel / render / renderInput / validate / save / load
Field Types
| Type | Class | Notes |
|---|---|---|
text |
TextField | Single-line |
textarea |
TextareaField | Multi-line |
number |
NumberField | min/max/step settings |
email |
EmailField | Validates format |
url |
UrlField | |
password |
PasswordField | Never echoed in output |
select |
SelectField | choices array; multiple support |
checkbox |
CheckboxField | choices array; returns string[] |
radio |
RadioField | choices array |
toggle |
ToggleField | Stores '1'/'' |
image |
ImageField | Attachment ID; return setting: id\|url\|array |
file |
FileField | Attachment ID; array includes filename/mime_type |
post_object |
PostObjectField | WP_Query select; multiple support |
user |
UserField | get_users with role__in filter |
taxonomy |
TaxonomyField | get_terms; returns term ID(s) |
repeater |
RepeaterField | Rows stored as JSON; sub-fields via parent_id |
flexible_content |
FlexibleContentField | Layouts + sub-fields; stored as JSON array |
clone |
CloneField | Inlines fields from source group(s); prefix_label/prefix_name settings |
Developer API
Get / Display Values
// Get a field value
$value = myacf_get_field('field_name', $post_id);
$value = myacf_get_field('field_name', $post_id, 'post', $default);
// Echo value (HTML-escaped)
myacf_the_field('field_name', $post_id);
// Check if value exists
if (myacf_has_field('field_name', $post_id)) { ... }
// Get all fields for an object
$all = myacf_get_fields($post_id);
// User fields
$bio = myacf_get_field('bio', $user_id, 'user');
// Term fields
$desc = myacf_get_field('extra_desc', $term_id, 'term');
// Options page fields
$val = myacf_get_field('site_logo', 0, 'option');
Save / Delete
myacf_update_field('field_name', $value, $post_id);
myacf_update_field('field_name', $value, $user_id, 'user');
myacf_delete_field('field_name', $post_id);
Performance: Bulk Cache Pre-warm
// Call in pre_get_posts or the_post to batch-load before template loop
add_action('pre_get_posts', function($query) {
if (!$query->is_main_query()) return;
$ids = get_posts(['fields' => 'ids', 'posts_per_page' => 10]);
myacf_prime_cache($ids, 'post');
});
Field Group Registration (programmatic)
Field groups can be created via WP Admin → RudraACF → Add New, or programmatically via the FieldGroupRepository:
add_action('plugins_loaded', function() {
$repo = RudraACF\Plugin::getInstance()
->getContainer()
->make(RudraACF\Database\Repositories\FieldGroupRepository::class);
$groupId = $repo->insert(
title: 'Article Meta',
key: 'group_article_meta',
settings: [
'rules' => [[
['param' => 'post_type', 'operator' => '==', 'value' => 'post'],
]],
'position' => 'normal',
],
);
});
Location Rules
Field groups are shown based on location rules evaluated by the Rules Engine.
param |
Evaluates | Operators |
|---|---|---|
post_type |
Current post type | == != |
taxonomy |
Current taxonomy (term edit screen) | == != |
user_role |
Current user's roles | == != |
page_template |
Active page template filename | == != |
post_status |
Post status | == != |
Rules use OR across groups, AND within each group (same as ACF):
// Show on 'post' OR 'page' post types
'rules' => [
[['param' => 'post_type', 'operator' => '==', 'value' => 'post']],
[['param' => 'post_type', 'operator' => '==', 'value' => 'page']],
]
Register a custom rule matcher
add_action('myacf/register_rule_matchers', function($engine) {
$engine->registerMatcher(new MyCustomRule());
});
// MyCustomRule implements RudraACF\Rules\RuleInterface:
// getParam(): string
// evaluate(string $operator, mixed $value, array $context): bool
Conditional Logic (Pro)
Show/hide individual fields based on other field values. Set in field settings:
'settings' => [
'conditional_logic' => [
// OR across outer array, AND within each group
[
['field' => 'enable_hero', 'operator' => '==', 'value' => '1'],
['field' => 'layout', 'operator' => '!=', 'value' => 'minimal'],
],
],
],
Available operators: == != < <= > >= contains not_contains empty not_empty
Apply in code:
$logic = new RudraACF\Pro\ConditionalLogic();
$values = myacf_get_fields($post_id);
$fields = $logic->filterFields($fieldDefinitions, $values);
Options Pages
// Register at plugins_loaded (before admin_menu)
add_action('plugins_loaded', function() {
myacf_register_options_page([
'slug' => 'theme-settings',
'title' => 'Theme Settings',
'menu_title' => 'Theme Options',
'capability' => 'manage_options',
'icon' => 'dashicons-art',
'position' => 60,
]);
// Sub-page
myacf_register_options_page([
'slug' => 'theme-colors',
'title' => 'Color Settings',
'parent' => 'theme-settings',
]);
});
// Read options page fields (object_id = 0, type = 'option')
$logo = myacf_get_field('logo', 0, 'option');
REST API
Base namespace: myacf/v1
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /field-groups |
manage_options |
List all field groups |
| GET | /field-groups/{id} |
manage_options |
Single group + fields |
| GET | /objects/{type}/{id} |
per-object cap | All field values |
| GET | /objects/{type}/{id}/{field} |
per-object cap | Single field value |
| POST | /objects/{type}/{id}/{field} |
per-object cap | Update field value |
{type} must be one of: post user term option
Per-object capability check (IDOR protection):
| type | read cap | write cap |
|---|---|---|
| post | read_post |
edit_post |
| user | edit_user |
edit_user |
| term | read |
edit_term |
| option | manage_options |
manage_options |
# Example
curl -X GET https://site.com/wp-json/myacf/v1/objects/post/42 \
-H "Authorization: Bearer TOKEN"
curl -X POST https://site.com/wp-json/myacf/v1/objects/post/42/hero_title \
-H "Authorization: Bearer TOKEN" \
-d '{"value": "New Title"}'
Import / Export
Via Admin UI
WP Admin → RudraACF → Import / Export
- Export: downloads all (or selected) field groups as JSON
- Import: upload JSON; checkbox "Update Existing" overwrites groups with matching key
Programmatic
$container = RudraACF\Plugin::getInstance()->getContainer();
// Export
$exporter = $container->make(RudraACF\ImportExport\Exporter::class);
$json = $exporter->export(); // all groups
$json = $exporter->export([1, 3]); // specific group IDs
// Import
$importer = $container->make(RudraACF\ImportExport\Importer::class);
$stats = $importer->import($json, updateExisting: true);
// $stats = ['imported' => 2, 'skipped' => 0, 'errors' => []]
Third-Party SDK
Hook into myacf/sdk/ready to register extensions after all services boot:
add_action('myacf/sdk/ready', function(RudraACF\Sdk\Sdk $sdk) {
// Register a custom field type
$sdk->registerFieldType(new MyPlugin\Fields\ColorPickerField());
// Register a custom rule matcher
$sdk->registerRuleMatcher(new MyPlugin\Rules\MembershipRule());
// Register an options page
$sdk->registerOptionsPage([
'slug' => 'my-plugin-settings',
'title' => 'My Plugin Settings',
]);
// Raw container access (escape hatch)
$container = $sdk->getContainer();
});
Custom Field Type
use RudraACF\Fields\BaseField;
final class ColorPickerField extends BaseField
{
public function getType(): string { return 'color_picker'; }
public function getLabel(): string { return 'Color Picker'; }
public function renderInput(array $field, mixed $value): void
{
$name = $this->inputName($field);
$id = $this->inputId($field);
echo '<input type="color" name="' . esc_attr($name) . '" '
. 'id="' . esc_attr($id) . '" '
. 'value="' . esc_attr((string) $value) . '">';
}
public function validate(mixed $value, array $field): true|string
{
if (!empty($field['settings']['required']) && empty($value)) {
return 'This field is required.';
}
if ($value !== '' && !preg_match('/^#[0-9a-fA-F]{3,6}$/', (string) $value)) {
return 'Must be a valid hex color (e.g. #ff0000).';
}
return true;
}
public function save(mixed $value, array $field): mixed { return sanitize_hex_color($value); }
public function load(mixed $value, array $field): mixed { return $value; }
}
Licensing (Pro)
WP Admin → RudraACF → License
Enter license key to activate. Key is validated against MYACF_LICENSE_SERVER_URL (define in wp-config.php for Pro builds):
// wp-config.php
define('MYACF_LICENSE_SERVER_URL', 'https://licenses.yoursite.com/api');
Programmatic:
$license = $container->make(RudraACF\Licensing\LicenseManager::class);
$result = $license->activate('YOUR-KEY-HERE');
// ['success' => true, 'message' => 'License activated successfully.']
$license->isActive(); // bool
$license->getLicenseKey(); // string
$license->check(); // bool — re-validates against server (24h cache)
$license->deactivate(); // frees the activation slot
The UpdateChecker automatically injects plugin update info into WP's update system when an active license is present.
Running Tests
cd myacf
composer install
./vendor/bin/phpunit --testsuite Unit
265 tests, 409 assertions. No real WordPress or database needed — full stub environment in tests/bootstrap.php.
Compatibility
On plugin activation, CompatibilityChecker blocks activation and shows an error if:
- PHP < 8.1
- WordPress < 6.3
- Advanced Custom Fields (free or Pro) is active — field name conflicts
Storage Schema
-- Field groups
wp_myacf_field_groups (id, title, key, settings JSON, created_at, updated_at)
-- Field definitions
wp_myacf_fields (id, group_id, parent_id, type, name, label, instructions, settings JSON, sort_order)
-- Field values
wp_myacf_field_values (id, object_type, object_id, field_id, value LONGTEXT)
parent_id links sub-fields to their Repeater/FlexContent/Clone parent.
object_type is one of post user term option. For options pages object_id = 0.
Hooks Reference
| Hook | Type | Description |
|---|---|---|
myacf/sdk/ready |
action | Fires after all services boot; receives Sdk instance |
myacf/register_field_types |
action | Fires inside FieldTypeRegistry build; receives FieldTypeRegistry |
myacf/register_rule_matchers |
action | Fires inside RulesEngine build; receives RulesEngine |
myacf/rules/context |
filter | Filter the rules context array before evaluation |