├── .editorconfig ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── acf.php ├── resources └── views │ ├── alpine-support.blade.php │ ├── block-editor-filters.blade.php │ └── field-usage.blade.php └── src ├── AcfComposer.php ├── Block.php ├── Builder.php ├── Builder ├── AccordionBuilder.php ├── ChoiceFieldBuilder.php ├── Concerns │ └── HasParentContext.php ├── FieldBuilder.php ├── FlexibleContentBuilder.php ├── GroupBuilder.php ├── RepeaterBuilder.php └── TabBuilder.php ├── Composer.php ├── Concerns ├── HasCollection.php ├── InteractsWithBlade.php └── InteractsWithPartial.php ├── Console ├── BlockMakeCommand.php ├── CacheCommand.php ├── ClearCommand.php ├── FieldMakeCommand.php ├── IdeHelpersCommand.php ├── MakeCommand.php ├── OptionsMakeCommand.php ├── PartialMakeCommand.php ├── StubPublishCommand.php ├── UpgradeCommand.php ├── UsageCommand.php ├── WidgetMakeCommand.php └── stubs │ ├── block.localized.stub │ ├── block.stub │ ├── field.stub │ ├── options.full.stub │ ├── options.stub │ ├── partial.stub │ ├── views │ ├── block.stub │ └── widget.stub │ └── widget.stub ├── Contracts ├── Block.php ├── Composer.php └── Widget.php ├── Exceptions ├── DuplicateKeyException.php └── InvalidFieldsException.php ├── Field.php ├── Manifest.php ├── Options.php ├── Partial.php ├── Providers └── AcfComposerServiceProvider.php └── Widget.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.php] 15 | indent_size = 4 16 | 17 | [src/Console/stubs/*.stub] 18 | indent_size = 4 19 | 20 | [src/Console/stubs/views/*.stub] 21 | indent_size = 2 22 | 23 | [*.blade.php] 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | _ide-helpers.php 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Brandon Nifong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACF Composer 2 | 3 | ![Latest Stable Version](https://img.shields.io/packagist/v/log1x/acf-composer.svg?style=flat-square) 4 | ![Total Downloads](https://img.shields.io/packagist/dt/log1x/acf-composer.svg?style=flat-square) 5 | ![Build Status](https://img.shields.io/github/actions/workflow/status/log1x/acf-composer/main.yml?branch=master&style=flat-square) 6 | 7 | ACF Composer is the ultimate tool for creating fields, blocks, widgets, and option pages using [ACF Builder](https://github.com/stoutlogic/acf-builder) alongside [Sage 10](https://github.com/roots/sage). 8 | 9 | ![Screenshot](https://i.imgur.com/7e7w3U9.png) 10 | 11 | ## Features 12 | 13 | - 🔧 Encourages clean structuring for creating fields with Sage 10 and ACF. 14 | - 🚀 Instantly generate working fields, blocks, widgets, partials, and option pages using CLI. Batteries included. 15 | - 🖼️ Fully rendered blocks and widgets using Blade with a native Sage 10 feel for passing view data. 16 | - ⚡ Seamlessly [cache](#caching-blocks--fields) blocks to `block.json` and field groups to a manifest. 17 | - 📦 Automatically hooks legacy widgets with `WP_Widget` making them instantly ready to use. 18 | - 🛠️ Automatically sets field location on blocks, widgets, and option pages. 19 | - 🌐 Globally define default field type and field group settings. No more repeating `['ui' => 1]` on every select field. 20 | 21 | ## Requirements 22 | 23 | - [Sage](https://github.com/roots/sage) >= 10.0 24 | - [Acorn](https://github.com/roots/acorn) >= 3.0 25 | - [ACF Pro](https://www.advancedcustomfields.com/) >= 5.8.0 26 | 27 | ## Installation 28 | 29 | Install via Composer: 30 | 31 | ```bash 32 | $ composer require log1x/acf-composer 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### Getting Started 38 | 39 | Start by publishing the `config/acf.php` configuration file using Acorn: 40 | 41 | ```bash 42 | $ wp acorn vendor:publish --tag="acf-composer" 43 | ``` 44 | 45 | ### Generating a Field Group 46 | 47 | To create your first field group, start by running the following generator command from your theme directory: 48 | 49 | ```bash 50 | $ wp acorn acf:field ExampleField 51 | ``` 52 | 53 | This will create `src/Fields/ExampleField.php` which is where you will create and manage your first field group. 54 | 55 | Taking a glance at the generated `ExampleField.php` stub, you will notice that it has a simple list configured. 56 | 57 | ```php 58 | setLocation('post_type', '==', 'post'); 78 | 79 | $fields 80 | ->addRepeater('items') 81 | ->addText('item') 82 | ->endRepeater(); 83 | 84 | return $fields->build(); 85 | } 86 | } 87 | ``` 88 | 89 | Proceed by checking the `Add Post` for the field to ensure things are working as intended. 90 | 91 | ### Field Builder Usage 92 | 93 | To assist with development, ACF Composer comes with a `usage` command that will let you search for registered field types. This will provide basic information about the field type as well as a usage example containing all possible options. 94 | 95 | ```sh 96 | $ wp acorn acf:usage 97 | ``` 98 | 99 | For additional usage, you may consult the [ACF Builder Cheatsheet](https://github.com/Log1x/acf-builder-cheatsheet). 100 | 101 | ### Generating a Field Partial 102 | 103 | A field partial consists of a field group that can be re-used and/or added to existing field groups. 104 | 105 | To start, let's generate a partial called _ListItems_ that we can use in the _Example_ field we generated above. 106 | 107 | ```bash 108 | $ wp acorn acf:partial ListItems 109 | ``` 110 | 111 | ```php 112 | addRepeater('items') 132 | ->addText('item') 133 | ->endRepeater(); 134 | 135 | return $fields; 136 | } 137 | } 138 | ``` 139 | 140 | Looking at `ListItems.php`, you will see out of the box it consists of an identical list repeater as seen in your generated field. 141 | 142 | A key difference to note compared to an ordinary field is the omitting of `->build()` instead returning the `FieldsBuilder` instance itself. 143 | 144 | This can be utilized in our _Example_ field by passing the `::class` constant to `->addPartial()`: 145 | 146 | ```php 147 | setLocation('post_type', '==', 'post'); 168 | 169 | $fields 170 | ->addPartial(ListItems::class); 171 | 172 | return $fields->build(); 173 | } 174 | } 175 | ``` 176 | 177 | ### Generating a Block 178 | 179 | Generating a block is generally the same as generating a field as seen above. 180 | 181 | Start by creating the block field using Acorn: 182 | 183 | ```bash 184 | $ wp acorn acf:block ExampleBlock 185 | ``` 186 | 187 | ```php 188 | $this->items(), 234 | ]; 235 | } 236 | 237 | /** 238 | * The block field group. 239 | * 240 | * @return array 241 | */ 242 | public function fields() 243 | { 244 | $fields = Builder::make('example_block'); 245 | 246 | $fields 247 | ->addRepeater('items') 248 | ->addText('item') 249 | ->endRepeater(); 250 | 251 | return $fields->build(); 252 | } 253 | 254 | /** 255 | * Return the items field. 256 | * 257 | * @return array 258 | */ 259 | public function items() 260 | { 261 | return get_field('items') ?: []; 262 | } 263 | } 264 | ``` 265 | 266 | You may also pass `--localize` to the command above to generate a block stub with the name and description ready for translation. 267 | 268 | ```bash 269 | $ wp acorn acf:block Example --localize 270 | ``` 271 | 272 | When running the block generator, one difference to a generic field is an accompanied `View` is generated in the `resources/views/blocks` directory. 273 | 274 | ```php 275 | @if ($items) 276 | 281 | @else 282 |

No items found!

283 | @endif 284 | 285 |
286 | 287 |
288 | ``` 289 | 290 | Like the field generator, the example block contains a simple list repeater and is working out of the box. 291 | 292 | #### Block Preview View 293 | 294 | While `$block->preview` is an option for conditionally modifying your block when shown in the editor, you may also render your block using a seperate view. 295 | 296 | Simply duplicate your existing view prefixing it with `preview-` (e.g. `preview-example.blade.php`). 297 | 298 | ### Generating a Widget 299 | 300 | > [!IMPORTANT] 301 | > With WordPress 5.8, Blocks can now be used as widgets making this feature somewhat deprecated as you would just make a block instead. 302 | > 303 | > If you are on the latest WordPress and would like to use the classic widget functionality from ACF Composer, you will need to [opt-out of the widget block editor](https://developer.wordpress.org/block-editor/how-to-guides/widgets/opting-out/). 304 | 305 | Creating a sidebar widget using ACF Composer is extremely easy. Widgets are automatically loaded and rendered with Blade, as well as registered with `WP_Widget` which is usually rather annoying. 306 | 307 | Start by creating a widget using Acorn: 308 | 309 | ```bash 310 | $ wp acorn acf:widget ExampleWidget 311 | ``` 312 | 313 | ```php 314 | $this->items(), 346 | ]; 347 | } 348 | 349 | /** 350 | * The widget title. 351 | * 352 | * @return string 353 | */ 354 | public function title() { 355 | return get_field('title', $this->widget->id); 356 | } 357 | 358 | /** 359 | * The widget field group. 360 | * 361 | * @return array 362 | */ 363 | public function fields() 364 | { 365 | $fields = Builder::make('example_widget'); 366 | 367 | $fields 368 | ->addText('title'); 369 | 370 | $fields 371 | ->addRepeater('items') 372 | ->addText('item') 373 | ->endRepeater(); 374 | 375 | return $fields->build(); 376 | } 377 | 378 | /** 379 | * Return the items field. 380 | * 381 | * @return array 382 | */ 383 | public function items() 384 | { 385 | return get_field('items', $this->widget->id) ?: []; 386 | } 387 | } 388 | ``` 389 | 390 | Similar to blocks, widgets are also accompanied by a view generated in `resources/views/widgets`. 391 | 392 | ```php 393 | @if ($items) 394 | 399 | @else 400 |

No items found!

401 | @endif 402 | ``` 403 | 404 | Out of the box, the Example widget is ready to go and should appear in the backend. 405 | 406 | ### Generating an Options Page 407 | 408 | Creating an options page is similar to creating a regular field group in additional to a few configuration options available to customize the page (most of which, are optional.) 409 | 410 | Start by creating an option page using Acorn: 411 | 412 | ```bash 413 | $ wp acorn acf:options ExampleOptions 414 | ``` 415 | 416 | ```php 417 | addRepeater('items') 451 | ->addText('item') 452 | ->endRepeater(); 453 | 454 | return $fields->build(); 455 | } 456 | } 457 | ``` 458 | 459 | Optionally, you may pass `--full` to the command above to generate a stub that contains additional configuration examples. 460 | 461 | ```bash 462 | $ wp acorn acf:options Options --full 463 | ``` 464 | 465 | Once finished, you should see an Options page appear in the backend. 466 | 467 | All fields registered will have their location automatically set to this page. 468 | 469 | ## Caching Blocks & Fields 470 | 471 | As of v3, ACF Composer now has the ability to cache registered blocks to native `block.json` files and field groups to a flat file JSON manifest automatically using CLI. 472 | 473 | This can lead to a **dramatic increase** in performance in projects both small and large, especially when loading a post in the editor containing multiple custom blocks. 474 | 475 | > [!NOTE] 476 | > Making changes to blocks or fields once cached will not take effect until cleared or re-cached. 477 | 478 | The best time to do this is during deployment and can be done using the `acf:cache` command: 479 | 480 | ```bash 481 | $ wp acorn acf:cache [--status] 482 | ``` 483 | 484 | Cache can then be cleared using the `acf:clear` command: 485 | 486 | ```bash 487 | $ wp acorn acf:clear 488 | ``` 489 | 490 | The ACF Composer cache status can be found using `--status` on `acf:cache` as seen above or by running `wp acorn about`. 491 | 492 | ## Custom Stub Generation 493 | 494 | To customize the stubs generated by ACF Composer, you can easily publish the `stubs` directory using Acorn: 495 | 496 | ```bash 497 | $ wp acorn acf:stubs 498 | ``` 499 | 500 | The publish command generates all available stubs by default. However, each stub file is optional. When a stub file doesn't exist in the `stubs/acf-composer` directory, the default stub provided by the package will be used. 501 | 502 | ## Default Field Settings 503 | 504 | A useful feature unique to ACF Composer is the ability to set field type as well as field group defaults. Any globally set default can of course be over-ridden by simply setting it on the individual field. 505 | 506 | ### Global 507 | 508 | Taking a look at `config/acf.php`, you will see a few pre-configured defaults: 509 | 510 | ```php 511 | 'defaults' => [ 512 | 'trueFalse' => ['ui' => 1], 513 | 'select' => ['ui' => 1], 514 | ], 515 | ``` 516 | 517 | When setting `trueFalse` and `select` to have their `ui` set to `1` by default, it is no longer necessary to repeatedly set `'ui' => 1` on your fields. This takes effect globally and can be overridden by simply setting a different value on a field. 518 | 519 | ### Field Group 520 | 521 | It is also possible to define defaults on individual field groups. This is done by simply defining `$defaults` in your field class. 522 | 523 | ```php 524 | /** 525 | * Default field type settings. 526 | * 527 | * @return array 528 | */ 529 | protected $defaults = ['ui' => 0]; 530 | ``` 531 | 532 | ### My Defaults 533 | 534 | Here are a couple defaults I personally use. Any prefixed with `acfe_` are related to [ACF Extended](https://www.acf-extended.com/). 535 | 536 | ```php 537 | 'defaults' => [ 538 | 'fieldGroup' => ['instruction_placement' => 'acfe_instructions_tooltip'], 539 | 'repeater' => ['layout' => 'block', 'acfe_repeater_stylised_button' => 1], 540 | 'trueFalse' => ['ui' => 1], 541 | 'select' => ['ui' => 1], 542 | 'postObject' => ['ui' => 1, 'return_format' => 'object'], 543 | 'accordion' => ['multi_expand' => 1], 544 | 'group' => ['layout' => 'table', 'acfe_group_modal' => 1], 545 | 'tab' => ['placement' => 'left'], 546 | 'sidebar_selector' => ['default_value' => 'sidebar-primary', 'allow_null' => 1] 547 | ], 548 | ``` 549 | 550 | ## Bug Reports 551 | 552 | If you discover a bug in ACF Composer, please [open an issue](https://github.com/log1x/acf-composer/issues). 553 | 554 | ## Contributing 555 | 556 | Contributing whether it be through PRs, reporting an issue, or suggesting an idea is encouraged and appreciated. 557 | 558 | ## License 559 | 560 | ACF Composer is provided under the [MIT License](LICENSE.md). 561 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "log1x/acf-composer", 3 | "type": "package", 4 | "description": "Create fields, blocks, option pages, and widgets using ACF Builder and Sage 10", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Brandon Nifong", 9 | "email": "brandon@tendency.me" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Log1x\\AcfComposer\\": "src/" 15 | } 16 | }, 17 | "require": { 18 | "php": "^8.0", 19 | "stoutlogic/acf-builder": "^1.11", 20 | "laravel/prompts": "*" 21 | }, 22 | "require-dev": { 23 | "laravel/pint": "^1.14", 24 | "roots/acorn": "^3.0|^4.0" 25 | }, 26 | "extra": { 27 | "acorn": { 28 | "providers": [ 29 | "Log1x\\AcfComposer\\Providers\\AcfComposerServiceProvider" 30 | ] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/acf.php: -------------------------------------------------------------------------------- 1 | [ 20 | // 'trueFalse' => ['ui' => 1], 21 | // 'select' => ['ui' => 1], 22 | ], 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Custom Field Types 27 | |-------------------------------------------------------------------------- 28 | | 29 | | Here you can define custom field types that are not included with ACF 30 | | out of the box. This allows you to use the fluent builder pattern with 31 | | custom field types such as `addEditorPalette()`. 32 | | 33 | */ 34 | 35 | 'types' => [ 36 | // 'editorPalette' => 'editor_palette', 37 | // 'phoneNumber' => 'phone_number', 38 | ], 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Default Block Settings 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Here you may define default settings to merge with your block definition 46 | | during composition. Any settings defined on the block will take 47 | | precedence over these defaults. 48 | | 49 | */ 50 | 51 | 'blocks' => [ 52 | 'apiVersion' => 2, 53 | ], 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Generators 58 | |-------------------------------------------------------------------------- 59 | | 60 | | Here you may specify defaults used when generating Composer classes in 61 | | your application. 62 | | 63 | */ 64 | 65 | 'generators' => [ 66 | 'supports' => ['align', 'mode', 'multiple', 'jsx'], 67 | ], 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Cache Manifest Path 72 | |-------------------------------------------------------------------------- 73 | | 74 | | Here you can define the cache manifest path. Fields are typically cached 75 | | when running the `acf:cache` command. This will cache the built field 76 | | groups and potentially improve performance in complex applications. 77 | | 78 | */ 79 | 80 | 'manifest' => storage_path('framework/cache'), 81 | 82 | ]; 83 | -------------------------------------------------------------------------------- /resources/views/alpine-support.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/block-editor-filters.blade.php: -------------------------------------------------------------------------------- 1 | @verbatim 2 | wp.hooks.addFilter( 3 | 'blocks.getBlockDefaultClassName', 4 | 'acf-composer/block-slug-classname', 5 | (className, blockName) => { 6 | if (! blockName.startsWith('acf/')) { 7 | return className; 8 | } 9 | 10 | const list = (className || '').split(/\\s+/); 11 | 12 | const classes = list.reduce((acc, current) => { 13 | acc.push(current); 14 | 15 | if (current.startsWith('wp-block-acf-')) { 16 | acc.push( 17 | current.replace('wp-block-acf-', 'wp-block-') 18 | ); 19 | } 20 | 21 | return acc; 22 | }, []); 23 | 24 | return classes.join(' '); 25 | } 26 | ); 27 | @endverbatim 28 | -------------------------------------------------------------------------------- /resources/views/field-usage.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'name', 3 | 'label', 4 | 'options' => '', 5 | 'native' => null, 6 | ]) 7 | 8 | @php($field = $native ? "{$native}('{$label}'" : "addField('{$name}', '{$label}'") 9 | 10 | $fields 11 | ->{!! $field !!}, [ 12 | {!! $options !!} 13 | ]); 14 | -------------------------------------------------------------------------------- /src/AcfComposer.php: -------------------------------------------------------------------------------- 1 | manifest = Manifest::make($this); 73 | } 74 | 75 | /** 76 | * Make a new Composer instance. 77 | */ 78 | public static function make(Application $app): self 79 | { 80 | return new static($app); 81 | } 82 | 83 | /** 84 | * Boot the registered Composers. 85 | */ 86 | public function boot(): void 87 | { 88 | if ($this->booted()) { 89 | return; 90 | } 91 | 92 | $this->registerDefaultPath(); 93 | 94 | $this->handleBlocks(); 95 | $this->handleWidgets(); 96 | 97 | add_filter('acf/init', fn () => $this->handleComposers(), config('acf.hookPriority', 100)); 98 | 99 | add_filter('acf/input/admin_footer', function () { 100 | echo view('acf-composer::alpine-support')->render(); 101 | }); 102 | 103 | $this->booted = true; 104 | } 105 | 106 | /** 107 | * Handle the Composer registration. 108 | */ 109 | public function handleComposers(): void 110 | { 111 | foreach ($this->composers as $namespace => $composers) { 112 | foreach ($composers as $i => $composer) { 113 | if (! is_subclass_of($composer, Options::class)) { 114 | $this->pendingComposers[$namespace][] = $composer; 115 | 116 | unset($this->composers[$namespace][$i]); 117 | 118 | continue; 119 | } 120 | 121 | $composer = $composer::make($this); 122 | 123 | if (! is_null($composer->parent)) { 124 | $this->deferredOptions[$namespace][] = $composer; 125 | 126 | unset($this->composers[$namespace][$i]); 127 | 128 | continue; 129 | } 130 | 131 | $this->composers[$namespace][$i] = $composer->handle(); 132 | } 133 | } 134 | 135 | foreach ($this->deferredOptions as $namespace => $composers) { 136 | foreach ($composers as $index => $composer) { 137 | $this->composers[$namespace][] = $composer->handle(); 138 | } 139 | } 140 | 141 | foreach ($this->pendingComposers as $namespace => $composers) { 142 | foreach ($composers as $composer) { 143 | $this->composers[$namespace][] = $composer::make($this)->handle(); 144 | } 145 | } 146 | 147 | $this->deferredOptions = []; 148 | $this->pendingComposers = []; 149 | 150 | foreach ($this->composers as $namespace => $composers) { 151 | $names = []; 152 | 153 | foreach ($composers as $composer) { 154 | $group = $composer->getFields(); 155 | 156 | $key = $group['key'] ?? $group[0]['key'] ?? null; 157 | 158 | if (! $key) { 159 | continue; 160 | } 161 | 162 | if (isset($names[$key])) { 163 | $class = $composer::class; 164 | 165 | throw new DuplicateKeyException("Duplicate ACF field group key [{$key}] found in [{$class}] and [{$names[$key]}]."); 166 | } 167 | 168 | $names[$key] = $composer::class; 169 | } 170 | 171 | $this->composers[$namespace] = array_values($composers); 172 | } 173 | } 174 | 175 | /** 176 | * Handle the Widget Composer registration. 177 | */ 178 | public function handleWidgets(): void 179 | { 180 | foreach ($this->legacyWidgets as $namespace => $composers) { 181 | foreach ($composers as $composer) { 182 | $composer = $composer::make($this); 183 | 184 | $this->composers[$namespace][] = $composer->handle(); 185 | } 186 | } 187 | 188 | $this->legacyWidgets = []; 189 | } 190 | 191 | /** 192 | * Handle the block rendering. 193 | */ 194 | protected function handleBlocks(): void 195 | { 196 | if (is_admin()) { 197 | add_action('enqueue_block_assets', function () { 198 | foreach ($this->composers() as $composers) { 199 | foreach ($composers as $composer) { 200 | if (! is_a($composer, Block::class)) { 201 | continue; 202 | } 203 | 204 | method_exists($composer, 'assets') && $composer->assets((array) $composer->block ?? []); 205 | } 206 | } 207 | }); 208 | } 209 | 210 | add_action('enqueue_block_editor_assets', function () { 211 | wp_add_inline_script('wp-blocks', view('acf-composer::block-editor-filters')->render()); 212 | }); 213 | 214 | add_action('acf_block_render_template', function ($block, $content, $is_preview, $post_id, $wp_block, $context) { 215 | if (! class_exists($composer = $block['render_template'] ?? '')) { 216 | return; 217 | } 218 | 219 | if (! $composer = app('AcfComposer')->getComposer($composer)) { 220 | return; 221 | } 222 | 223 | add_filter('acf/blocks/template_not_found_message', fn () => ''); 224 | 225 | echo $composer->render($block, $content, $is_preview, $post_id, $wp_block, $context); 226 | }, 9, 6); 227 | } 228 | 229 | /** 230 | * Register the default application path. 231 | */ 232 | public function registerDefaultPath(): void 233 | { 234 | $this->registerPath($this->app->path()); 235 | } 236 | 237 | /** 238 | * Register the specified path with ACF Composer. 239 | */ 240 | public function registerPath(string $path, ?string $namespace = null): array 241 | { 242 | $paths = $this->collect(File::directories($path)) 243 | ->filter(fn ($item) => Str::contains($item, $this->classes)); 244 | 245 | if ($paths->isEmpty()) { 246 | return []; 247 | } 248 | 249 | if (empty($namespace)) { 250 | $namespace = $this->app->getNamespace(); 251 | } 252 | 253 | foreach ((new Finder)->in($paths->toArray())->files()->sortByName() as $file) { 254 | $relativePath = str_replace( 255 | Str::finish($path, DIRECTORY_SEPARATOR), 256 | '', 257 | $file->getPathname() 258 | ); 259 | 260 | $folders = Str::beforeLast( 261 | $relativePath, 262 | DIRECTORY_SEPARATOR 263 | ).DIRECTORY_SEPARATOR; 264 | 265 | $className = Str::after($relativePath, $folders); 266 | 267 | $composer = $namespace.str_replace( 268 | ['/', '.php'], 269 | ['\\', ''], 270 | $folders.$className 271 | ); 272 | 273 | $this->paths[$path][] = $composer; 274 | 275 | $this->register($composer, $namespace); 276 | } 277 | 278 | return $this->paths; 279 | } 280 | 281 | /** 282 | * Register a Composer with ACF Composer. 283 | */ 284 | public function register(string $composer, string $namespace): bool 285 | { 286 | if ( 287 | ! is_subclass_of($composer, Composer::class) || 288 | is_subclass_of($composer, Partial::class) || 289 | (new ReflectionClass($composer))->isAbstract() 290 | ) { 291 | return false; 292 | } 293 | 294 | if (is_subclass_of($composer, Widget::class)) { 295 | $this->legacyWidgets[$namespace][] = $composer; 296 | 297 | return true; 298 | } 299 | 300 | $this->composers[$namespace][] = $composer; 301 | 302 | return true; 303 | } 304 | 305 | /** 306 | * Register an ACF Composer plugin with the container. 307 | */ 308 | public function registerPlugin(string $path, string $namespace): void 309 | { 310 | $namespace = str_replace('Providers', '', $namespace); 311 | 312 | $this->registerPath($path, $namespace); 313 | 314 | $this->plugins[$namespace] = $path; 315 | } 316 | 317 | /** 318 | * Retrieve the registered composers. 319 | */ 320 | public function composers(): array 321 | { 322 | return $this->composers; 323 | } 324 | 325 | /** 326 | * Retrieve a Composer instance by class name. 327 | */ 328 | public function getComposer(string $class): ?Composer 329 | { 330 | foreach ($this->composers as $composers) { 331 | foreach ($composers as $composer) { 332 | if ($composer::class === $class) { 333 | return $composer; 334 | } 335 | } 336 | } 337 | 338 | return null; 339 | } 340 | 341 | /** 342 | * Retrieve the registered paths. 343 | */ 344 | public function paths(): array 345 | { 346 | return array_unique($this->paths); 347 | } 348 | 349 | /** 350 | * Retrieve the registered plugins. 351 | */ 352 | public function plugins(): array 353 | { 354 | return $this->plugins; 355 | } 356 | 357 | /** 358 | * Retrieve the cache manifest. 359 | */ 360 | public function manifest(): Manifest 361 | { 362 | return $this->manifest; 363 | } 364 | 365 | /** 366 | * Determine if ACF Composer is booted. 367 | */ 368 | public function booted(): bool 369 | { 370 | return $this->booted; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/Block.php: -------------------------------------------------------------------------------- 1 | null, 207 | 'margin' => null, 208 | ]; 209 | 210 | /** 211 | * The supported block features. 212 | * 213 | * @var array 214 | */ 215 | public $supports = []; 216 | 217 | /** 218 | * The block styles. 219 | * 220 | * @var array 221 | */ 222 | public $styles = []; 223 | 224 | /** 225 | * The current block style. 226 | * 227 | * @var string 228 | */ 229 | public $style; 230 | 231 | /** 232 | * The block dimensions. 233 | * 234 | * @var string 235 | */ 236 | public $inlineStyle; 237 | 238 | /** 239 | * Context values inherited by the block. 240 | * 241 | * @var string[] 242 | */ 243 | public $uses_context = []; 244 | 245 | /** 246 | * Context provided by the block. 247 | * 248 | * @var string[] 249 | */ 250 | public $provides_context = []; 251 | 252 | /** 253 | * The block preview example data. 254 | * 255 | * @var array 256 | */ 257 | public $example = []; 258 | 259 | /** 260 | * The block template. 261 | * 262 | * @var array|string 263 | */ 264 | public $template = []; 265 | 266 | /** 267 | * Determine whether to save the block's data as post meta. 268 | * 269 | * @var bool 270 | */ 271 | public $usePostMeta = false; 272 | 273 | /** 274 | * The block API version. 275 | * 276 | * @var int|null 277 | */ 278 | public $apiVersion = null; 279 | 280 | /** 281 | * The internal ACF block version. 282 | * 283 | * @var int 284 | */ 285 | public $blockVersion = 2; 286 | 287 | /** 288 | * Validate block fields as per the field group configuration. 289 | * 290 | * @var bool 291 | */ 292 | public $validate = true; 293 | 294 | /** 295 | * The block attributes. 296 | */ 297 | public function attributes(): array 298 | { 299 | return []; 300 | } 301 | 302 | /** 303 | * Set the attributes to the block properties. 304 | */ 305 | public function mergeAttributes(): void 306 | { 307 | foreach ($this->attributes() as $key => $value) { 308 | if (! property_exists($this, $key)) { 309 | continue; 310 | } 311 | 312 | $this->{$key} = $value; 313 | } 314 | 315 | $defaults = config('acf.blocks', []); 316 | 317 | foreach ($defaults as $key => $value) { 318 | if (! property_exists($this, $key) || filled($this->{$key})) { 319 | continue; 320 | } 321 | 322 | $this->{$key} = $value; 323 | } 324 | } 325 | 326 | /** 327 | * Retrieve the block name. 328 | */ 329 | public function getName(): string 330 | { 331 | return $this->name; 332 | } 333 | 334 | /** 335 | * Retrieve the block description. 336 | */ 337 | public function getDescription(): string 338 | { 339 | return $this->description; 340 | } 341 | 342 | /** 343 | * Retrieve the active block style. 344 | */ 345 | public function getStyle(): ?string 346 | { 347 | return Str::of($this->block->className ?? null) 348 | ->matchAll('/is-style-(\S+)/') 349 | ->get(0) ?? 350 | Arr::get($this->getDefaultStyle(), 'name'); 351 | } 352 | 353 | /** 354 | * Retrieve the default style. 355 | */ 356 | public function getDefaultStyle(): array 357 | { 358 | return $this->collect($this->getStyles())->firstWhere('isDefault') ?? []; 359 | } 360 | 361 | /** 362 | * Retrieve the block styles. 363 | */ 364 | public function getStyles(): array 365 | { 366 | $styles = $this->collect($this->styles)->map(function ($value, $key) { 367 | if (is_array($value)) { 368 | return $value; 369 | } 370 | 371 | $name = is_bool($value) ? $key : $value; 372 | $default = is_bool($value) ? $value : false; 373 | 374 | return [ 375 | 'name' => $name, 376 | 'label' => Str::headline($name), 377 | 'isDefault' => $default, 378 | ]; 379 | }); 380 | 381 | if (! $styles->where('isDefault', true)->count()) { 382 | $styles = $styles->map(fn ($style, $key) => $key === 0 383 | ? Arr::set($style, 'isDefault', true) 384 | : $style 385 | ); 386 | } 387 | 388 | return $styles->all(); 389 | } 390 | 391 | /** 392 | * Retrieve the block supports. 393 | */ 394 | public function getSupports(): array 395 | { 396 | $supports = $this->collect($this->supports) 397 | ->mapWithKeys(fn ($value, $key) => [Str::camel($key) => $value]) 398 | ->merge($this->supports); 399 | 400 | $typography = $supports->get('typography', []); 401 | 402 | if ($supports->has('alignText')) { 403 | $typography['textAlign'] = $supports->get('alignText'); 404 | 405 | $supports->forget(['alignText', 'align_text']); 406 | } 407 | 408 | if ($typography) { 409 | $supports->put('typography', $typography); 410 | } 411 | 412 | return $supports->all(); 413 | } 414 | 415 | /** 416 | * Retrieve the block support attributes. 417 | */ 418 | public function getSupportAttributes(): array 419 | { 420 | $attributes = []; 421 | 422 | if ($this->align) { 423 | $attributes['align'] = [ 424 | 'type' => 'string', 425 | 'default' => $this->align, 426 | ]; 427 | } 428 | 429 | if ($this->align_content) { 430 | $attributes['alignContent'] = [ 431 | 'type' => 'string', 432 | 'default' => $this->align_content, 433 | ]; 434 | } 435 | 436 | $styles = []; 437 | 438 | if ($this->align_text) { 439 | $styles['typography']['textAlign'] = $this->align_text; 440 | } 441 | 442 | $spacing = array_filter($this->spacing); 443 | 444 | if ($spacing) { 445 | $styles['spacing'] = $spacing; 446 | } 447 | 448 | if ($styles) { 449 | $attributes['style'] = [ 450 | 'type' => 'object', 451 | 'default' => $styles, 452 | ]; 453 | } 454 | 455 | return $attributes; 456 | } 457 | 458 | /** 459 | * Retrieve the block HTML attributes. 460 | */ 461 | public function getHtmlAttributes(): array 462 | { 463 | if ($this->preview) { 464 | return []; 465 | } 466 | 467 | return WP_Block_Supports::get_instance()?->apply_block_supports() ?? []; 468 | } 469 | 470 | /** 471 | * Retrieve the inline block styles. 472 | */ 473 | public function getInlineStyle(): string 474 | { 475 | $supports = $this->getHtmlAttributes(); 476 | 477 | return $supports['style'] ?? ''; 478 | } 479 | 480 | /** 481 | * Retrieve the block classes. 482 | */ 483 | public function getClasses(): string 484 | { 485 | $supports = $this->getHtmlAttributes(); 486 | 487 | $class = $supports['class'] ?? ''; 488 | 489 | if ($alignContent = $this->block->alignContent ?? $this->block->align_content ?? null) { 490 | $class = "{$class} is-position-{$alignContent}"; 491 | } 492 | 493 | if ($this->block->fullHeight ?? $this->block->full_height ?? null) { 494 | $class = "{$class} full-height"; 495 | } 496 | 497 | return str_replace( 498 | acf_slugify($this->namespace), 499 | $this->slug, 500 | trim($class) 501 | ); 502 | } 503 | 504 | /** 505 | * Retrieve the block API version. 506 | */ 507 | public function getApiVersion(): int 508 | { 509 | return $this->apiVersion ?? 2; 510 | } 511 | 512 | /** 513 | * Retrieve the block text domain. 514 | */ 515 | public function getTextDomain(): string 516 | { 517 | return $this->textDomain 518 | ?? wp_get_theme()?->get('TextDomain') 519 | ?? 'acf-composer'; 520 | } 521 | 522 | /** 523 | * Retrieve the block icon. 524 | */ 525 | public function getIcon(): string|array 526 | { 527 | if (is_array($this->icon)) { 528 | return $this->icon; 529 | } 530 | 531 | if (Str::startsWith($this->icon, 'asset:')) { 532 | $asset = Str::of($this->icon) 533 | ->after('asset:') 534 | ->before('.svg') 535 | ->replace('.', '/') 536 | ->finish('.svg'); 537 | 538 | return asset($asset)->contents(); 539 | } 540 | 541 | return $this->icon; 542 | } 543 | 544 | /** 545 | * Handle the block template. 546 | */ 547 | public function handleTemplate(array $template = []): Collection 548 | { 549 | return $this->collect($template)->map(function ($block, $key) { 550 | $name = is_numeric($key) 551 | ? array_key_first((array) $block) 552 | : $key; 553 | 554 | $value = is_numeric($key) 555 | ? ($block[$name] ?? []) 556 | : $block; 557 | 558 | if (is_array($value) && isset($value['innerBlocks'])) { 559 | $innerBlocks = $this->handleTemplate($value['innerBlocks'])->all(); 560 | 561 | unset($value['innerBlocks']); 562 | 563 | return [$name, $value, $innerBlocks]; 564 | } 565 | 566 | return [$name, $value]; 567 | })->values(); 568 | } 569 | 570 | /** 571 | * Compose the fields and register the block. 572 | */ 573 | public function compose(): ?self 574 | { 575 | $this->mergeAttributes(); 576 | 577 | if (blank($this->getName())) { 578 | return null; 579 | } 580 | 581 | $this->slug = $this->slug ?: Str::slug(Str::kebab($this->getName())); 582 | $this->view = $this->view ?: Str::start($this->slug, 'blocks.'); 583 | $this->namespace = $this->namespace ?? Str::start($this->slug, $this->prefix); 584 | 585 | if (! Arr::has($this->fields, 'location.0.0')) { 586 | Arr::set($this->fields, 'location.0.0', [ 587 | 'param' => 'block', 588 | 'operator' => '==', 589 | 'value' => $this->namespace, 590 | ]); 591 | } 592 | 593 | $this->register(fn () => $this->hasJson() 594 | ? register_block_type($this->jsonPath()) 595 | : $this->registerBlockType() 596 | ); 597 | 598 | return $this; 599 | } 600 | 601 | /** 602 | * Register the block type. 603 | */ 604 | public function registerBlockType(): void 605 | { 606 | $block = acf_validate_block_type($this->settings()->all()); 607 | $block = apply_filters('acf/register_block_type_args', $block); 608 | 609 | if (acf_has_block_type($block['name'])) { 610 | throw new Exception("Block type [{$block['name']}] is already registered."); 611 | } 612 | 613 | $block['attributes'] = array_merge( 614 | acf_get_block_type_default_attributes($block), 615 | $block['attributes'] ?? [] 616 | ); 617 | 618 | acf_get_store('block-types')->set($block['name'], $block); 619 | 620 | $block['render_callback'] = 'acf_render_block_callback'; 621 | 622 | register_block_type( 623 | $block['name'], 624 | $block 625 | ); 626 | 627 | add_action('enqueue_block_editor_assets', 'acf_enqueue_block_assets'); 628 | } 629 | 630 | /** 631 | * Retrieve the block settings. 632 | */ 633 | public function settings(): Collection 634 | { 635 | if ($this->settings) { 636 | return $this->settings; 637 | } 638 | 639 | $settings = Collection::make([ 640 | 'name' => $this->slug, 641 | 'title' => $this->getName(), 642 | 'description' => $this->getDescription(), 643 | 'category' => $this->category, 644 | 'icon' => $this->getIcon(), 645 | 'keywords' => $this->keywords, 646 | 'post_types' => $this->post_types, 647 | 'mode' => $this->mode, 648 | 'align' => $this->align, 649 | 'attributes' => $this->getSupportAttributes(), 650 | 'alignText' => $this->align_text ?? $this->align, 651 | 'alignContent' => $this->align_content, 652 | 'styles' => $this->getStyles(), 653 | 'supports' => $this->getSupports(), 654 | 'textdomain' => $this->getTextDomain(), 655 | 'acf_block_version' => $this->blockVersion, 656 | 'api_version' => $this->getApiVersion(), 657 | 'validate' => $this->validate, 658 | 'use_post_meta' => $this->usePostMeta, 659 | 'render_callback' => function ( 660 | $block, 661 | $content = '', 662 | $preview = false, 663 | $post_id = 0, 664 | $wp_block = false, 665 | $context = false 666 | ) { 667 | echo $this->render($block, $content, $preview, $post_id, $wp_block, $context); 668 | }, 669 | ]); 670 | 671 | if (filled($this->parent)) { 672 | $settings = $settings->put('parent', $this->parent); 673 | } 674 | 675 | if (filled($this->ancestor)) { 676 | $settings = $settings->put('ancestor', $this->ancestor); 677 | } 678 | 679 | if ($this->example !== false) { 680 | if (method_exists($this, 'example') && is_array($example = $this->example())) { 681 | $this->example = array_merge($this->example, $example); 682 | } 683 | 684 | $settings = $settings->put('example', [ 685 | 'attributes' => [ 686 | 'mode' => 'preview', 687 | 'data' => $this->example, 688 | ], 689 | ]); 690 | } 691 | 692 | if (! empty($this->uses_context)) { 693 | $settings = $settings->put('uses_context', $this->uses_context); 694 | } 695 | 696 | if (! empty($this->provides_context)) { 697 | $settings = $settings->put('provides_context', $this->provides_context); 698 | } 699 | 700 | return $this->settings = $settings; 701 | } 702 | 703 | /** 704 | * Retrieve the Block settings as JSON. 705 | */ 706 | public function toJson(): string 707 | { 708 | $settings = $this->settings() 709 | ->put('name', $this->namespace) 710 | ->put('apiVersion', $this->getApiVersion()) 711 | ->put('acf', [ 712 | 'blockVersion' => $this->blockVersion, 713 | 'mode' => $this->mode, 714 | 'postTypes' => $this->post_types, 715 | 'renderTemplate' => $this::class, 716 | 'usePostMeta' => $this->usePostMeta, 717 | 'validate' => $this->validate, 718 | ]) 719 | ->forget([ 720 | 'api_version', 721 | 'acf_block_version', 722 | 'align', 723 | 'alignContent', 724 | 'alignText', 725 | 'mode', 726 | 'post_types', 727 | 'render_callback', 728 | 'use_post_meta', 729 | 'validate', 730 | ]); 731 | 732 | return $settings->filter()->toJson(JSON_PRETTY_PRINT); 733 | } 734 | 735 | /** 736 | * Retrieve the Block JSON path. 737 | */ 738 | public function jsonPath(): string 739 | { 740 | return $this->composer->manifest()->path("blocks/{$this->slug}/block.json"); 741 | } 742 | 743 | /** 744 | * Determine if the Block has a JSON file. 745 | */ 746 | public function hasJson(): bool 747 | { 748 | return file_exists($this->jsonPath()); 749 | } 750 | 751 | /** 752 | * Render the ACF block. 753 | * 754 | * @param array $block 755 | * @param string $content 756 | * @param bool $preview 757 | * @param int $post_id 758 | * @param \WP_Block $wp_block 759 | * @param array $context 760 | * @return string 761 | */ 762 | public function render($block, $content = '', $preview = false, $post_id = 0, $wp_block = false, $context = false) 763 | { 764 | $this->block = (object) $block; 765 | $this->content = $content; 766 | $this->preview = $preview; 767 | $this->post_id = $post_id; 768 | $this->instance = $wp_block; 769 | $this->context = $context; 770 | 771 | $this->post = get_post($post_id); 772 | 773 | $this->template = is_array($this->template) 774 | ? $this->handleTemplate($this->template)->toJson() 775 | : $this->template; 776 | 777 | $this->classes = $this->getClasses(); 778 | $this->style = $this->getStyle(); 779 | $this->inlineStyle = $this->getInlineStyle(); 780 | 781 | $attributes = (new ComponentAttributeBag) 782 | ->class($this->classes) 783 | ->style($this->inlineStyle) 784 | ->filter(fn ($value) => filled($value) && $value !== ';'); 785 | 786 | if (! is_admin() && method_exists($this, 'assets')) { 787 | $instance = (array) ($this->block ?? []); 788 | 789 | add_action('enqueue_block_assets', function () use ($instance): void { 790 | $this->assets($instance); 791 | }); 792 | } 793 | 794 | return $this->view($this->view, [ 795 | 'block' => $this, 796 | 'attributes' => $attributes, 797 | ]); 798 | } 799 | 800 | /** 801 | * Assets enqueued when rendering the block. 802 | * 803 | * @return void 804 | * 805 | * @deprecated Use `assets($block)` instead. 806 | */ 807 | public function enqueue() 808 | { 809 | // 810 | } 811 | } 812 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | isAbstract() 100 | ) { 101 | $partial = $partial::make($this->composer())->compose($args); 102 | } 103 | 104 | if (! is_a($partial, FieldsBuilder::class)) { 105 | throw new InvalidArgumentException('The partial must return an instance of Builder.'); 106 | } 107 | 108 | return $this->addFields($partial); 109 | } 110 | 111 | /** 112 | * Add multiple partials to the field group. 113 | */ 114 | public function addPartials(array $partials): self 115 | { 116 | foreach ($partials as $key => $value) { 117 | $partial = is_string($value) ? $value : $key; 118 | $args = is_array($value) ? $value : []; 119 | 120 | $this->addPartial($partial, $args); 121 | } 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Retrieve the custom field types. 128 | */ 129 | protected function getFieldTypes(): array 130 | { 131 | if (! is_null($this->types)) { 132 | return $this->types; 133 | } 134 | 135 | $types = config('acf.types', []); 136 | 137 | return $this->types = $this->collect($types)->mapWithKeys(fn ($type, $key) => [ 138 | Str::of($key)->studly()->start('add')->toString() => $type, 139 | ])->all(); 140 | } 141 | 142 | /** 143 | * Retrieve the specified field type. 144 | */ 145 | protected function getFieldType(string $type): ?string 146 | { 147 | return $this->getFieldTypes()[$type] ?? null; 148 | } 149 | 150 | /** 151 | * Retrieve an instance of ACF Composer. 152 | */ 153 | protected function composer(): AcfComposer 154 | { 155 | if ($this->composer) { 156 | return $this->composer; 157 | } 158 | 159 | return $this->composer = app('AcfComposer'); 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function addField($name, $type, array $args = []) 166 | { 167 | return $this->initializeField(new FieldBuilder($name, $type, $args)); 168 | } 169 | 170 | /** 171 | * Initialize the field and push it to the field manager. 172 | * 173 | * @param FieldBuilder $field 174 | * @return FieldBuilder 175 | */ 176 | protected function initializeField($field) 177 | { 178 | $field->setParentContext($this); 179 | 180 | $this->getFieldManager()->pushField($field); 181 | 182 | return $field; 183 | } 184 | 185 | /** 186 | * {@inheritdoc} 187 | */ 188 | public function setLocation($param, $operator, $value) 189 | { 190 | $location = parent::setLocation($param, $operator, $value); 191 | 192 | $location->setParentContext($this); 193 | 194 | return $location; 195 | } 196 | 197 | /** 198 | * Add a group field. 199 | * 200 | * @param string $name 201 | * @return \Log1x\AcfComposer\Builder\GroupBuilder 202 | */ 203 | public function addGroup($name, array $args = []) 204 | { 205 | return $this->initializeField(new GroupBuilder($name, 'group', $args)); 206 | } 207 | 208 | /** 209 | * Add a repeater field. 210 | * 211 | * @param string $name 212 | * @return \Log1x\AcfComposer\Builder\RepeaterBuilder 213 | */ 214 | public function addRepeater($name, array $args = []) 215 | { 216 | return $this->initializeField(new RepeaterBuilder($name, 'repeater', $args)); 217 | } 218 | 219 | /** 220 | * Add a flexible content field. 221 | * 222 | * @param string $name 223 | * @return \Log1x\AcfComposer\Builder\FlexibleContentBuilder 224 | */ 225 | public function addFlexibleContent($name, array $args = []) 226 | { 227 | return $this->initializeField(new FlexibleContentBuilder($name, 'flexible_content', $args)); 228 | } 229 | 230 | /** 231 | * Add a tab field. 232 | * 233 | * @param string $label 234 | * @return \Log1x\AcfComposer\Builder\TabBuilder 235 | */ 236 | public function addTab($label, array $args = []) 237 | { 238 | return $this->initializeField(new TabBuilder($label, 'tab', $args)); 239 | } 240 | 241 | /** 242 | * Add an accordion field. 243 | * 244 | * @param string $label 245 | * @return \Log1x\AcfComposer\Builder\AccordionBuilder 246 | */ 247 | public function addAccordion($label, array $args = []) 248 | { 249 | return $this->initializeField(new AccordionBuilder($label, 'accordion', $args)); 250 | } 251 | 252 | /** 253 | * Add a choice field. 254 | * 255 | * @param string $name 256 | * @param string $type 257 | * @return \Log1x\AcfComposer\Builder\ChoiceFieldBuilder 258 | */ 259 | public function addChoiceField($name, $type, array $args = []) 260 | { 261 | return $this->initializeField(new ChoiceFieldBuilder($name, $type, $args)); 262 | } 263 | 264 | /** 265 | * Check for custom field types before calling the requested method. 266 | * 267 | * @param string $method 268 | * @param array $arguments 269 | * @return mixed 270 | */ 271 | public function __call($method, $arguments) 272 | { 273 | if ($context = $this->getParentContext()) { 274 | if ($type = $context->getFieldType($method)) { 275 | $name = array_shift($arguments); 276 | 277 | return $this->addField($name, $type, ...$arguments); 278 | } 279 | } 280 | 281 | if ($type = $this->getFieldType($method)) { 282 | $name = array_shift($arguments); 283 | 284 | return $this->addField($name, $type, ...$arguments); 285 | } 286 | 287 | return parent::__call($method, $arguments); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/Builder/AccordionBuilder.php: -------------------------------------------------------------------------------- 1 | fieldsBuilder, $method) || $this->fieldsBuilder->getFieldType($method)) 19 | ) { 20 | $field = $this->handleCall($method, $args); 21 | $field->setParentContext($this); 22 | 23 | return $field; 24 | } 25 | 26 | return parent::__call($method, $args); 27 | } 28 | 29 | /** 30 | * Hanlde the Fields Builder method call. 31 | * 32 | * @param string $method 33 | * @param array $args 34 | * @return mixed 35 | */ 36 | protected function handleCall($method, $args) 37 | { 38 | return call_user_func_array([$this->fieldsBuilder, $method], $args); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Builder/FieldBuilder.php: -------------------------------------------------------------------------------- 1 | isAbstract() 74 | ) { 75 | $layout = $layout::make($this->composer())->compose(); 76 | } 77 | 78 | $layout = is_a($layout, FieldsBuilder::class) 79 | ? clone $layout 80 | : Builder::make($layout, $args); 81 | 82 | $layout = $this->initializeLayout($layout, $args); 83 | 84 | $this->pushLayout($layout); 85 | 86 | return $layout; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Builder/GroupBuilder.php: -------------------------------------------------------------------------------- 1 | fieldsBuilder = Builder::make($name); 76 | 77 | $this->fieldsBuilder->setParentContext($this); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Builder/RepeaterBuilder.php: -------------------------------------------------------------------------------- 1 | fieldsBuilder = Builder::make($name); 76 | 77 | $this->fieldsBuilder->setParentContext($this); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Builder/TabBuilder.php: -------------------------------------------------------------------------------- 1 | app = $this->composer->app; 44 | 45 | $this->defaults = $this->collect($this->app->config->get('acf.defaults')) 46 | ->merge($this->defaults) 47 | ->mapWithKeys(fn ($value, $key) => [Str::snake($key) => $value]); 48 | 49 | $this->fields = $this->getFields(); 50 | } 51 | 52 | /** 53 | * Make a new instance of the Composer. 54 | */ 55 | public static function make(AcfComposer $composer): self 56 | { 57 | return new static($composer); 58 | } 59 | 60 | /** 61 | * Handle the Composer instance. 62 | */ 63 | public function handle(): self 64 | { 65 | $this->call('beforeRegister'); 66 | 67 | $this->compose(); 68 | 69 | $this->call('afterRegister'); 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Call a method using the application container. 76 | */ 77 | protected function call(string $hook, array $args = []): mixed 78 | { 79 | if (! method_exists($this, $hook)) { 80 | return null; 81 | } 82 | 83 | return $this->app->call([$this, $hook], [ 84 | 'args' => $args, 85 | ]); 86 | } 87 | 88 | /** 89 | * Register the field group with Advanced Custom Fields. 90 | */ 91 | protected function register(?callable $callback = null): void 92 | { 93 | if (blank($this->fields)) { 94 | return; 95 | } 96 | 97 | if ($callback) { 98 | $callback(); 99 | } 100 | 101 | acf_add_local_field_group( 102 | $this->build($this->fields) 103 | ); 104 | } 105 | 106 | /** 107 | * Retrieve the field group fields. 108 | */ 109 | public function getFields(bool $cache = true): array 110 | { 111 | if ($this->fields && $cache) { 112 | return $this->fields; 113 | } 114 | 115 | if ($cache && $this->composer->manifest()->has($this)) { 116 | return $this->composer->manifest()->get($this); 117 | } 118 | 119 | $fields = $this->resolveFields(); 120 | 121 | $fields = is_a($fields, FieldsBuilder::class) 122 | ? $fields->build() 123 | : $fields; 124 | 125 | if (! is_array($fields)) { 126 | throw new InvalidFieldsException; 127 | } 128 | 129 | if ($this->defaults->has('field_group')) { 130 | $fields = array_merge($this->defaults->get('field_group'), $fields); 131 | } 132 | 133 | return $fields; 134 | } 135 | 136 | /** 137 | * Resolve the fields from the Composer with the container. 138 | */ 139 | public function resolveFields(array $args = []): mixed 140 | { 141 | return $this->call('fields', $args) ?? []; 142 | } 143 | 144 | /** 145 | * Build the field group with the default field type settings. 146 | */ 147 | public function build(array $fields = []): array 148 | { 149 | return $this->collect($fields)->map(function ($value, $key) { 150 | if ( 151 | ! in_array($key, $this->keys) || 152 | ($key === 'type' && ! $this->defaults->has($value)) 153 | ) { 154 | return $value; 155 | } 156 | 157 | return array_map(function ($field) { 158 | foreach ($field as $key => $value) { 159 | if (in_array($key, $this->keys)) { 160 | return $this->build($field); 161 | } 162 | 163 | if ($key === 'type' && $this->defaults->has($value)) { 164 | $field = array_merge($this->defaults->get($field['type'], []), $field); 165 | } 166 | } 167 | 168 | return $field; 169 | }, $value); 170 | })->all(); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Concerns/HasCollection.php: -------------------------------------------------------------------------------- 1 | block) && 22 | ! empty($this->preview) 23 | ) { 24 | $preview = Str::replaceLast( 25 | $name = Str::afterLast($view, '.'), 26 | Str::start($name, 'preview-'), 27 | $view 28 | ); 29 | 30 | $view = view()->exists($preview) ? $preview : $view; 31 | } 32 | 33 | return view($view, $with, $this->with())->render(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithPartial.php: -------------------------------------------------------------------------------- 1 | isAbstract() 25 | ) { 26 | return $partial::make($this->composer)->compose(); 27 | } 28 | 29 | if (is_a($partial, FieldsBuilder::class) || is_array($partial)) { 30 | return $partial; 31 | } 32 | 33 | if (file_exists($partial)) { 34 | return include $partial; 35 | } 36 | 37 | return file_exists( 38 | $partial = $this->app->path( 39 | Str::finish( 40 | strtr($partial, ['.php' => '', '.' => '/']), 41 | '.php' 42 | ) 43 | ) 44 | ) ? include $partial : []; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Console/BlockMakeCommand.php: -------------------------------------------------------------------------------- 1 | ['background', 'text', 'gradients'], 56 | 'spacing' => ['padding', 'margin'], 57 | ]; 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function buildClass($name) 63 | { 64 | $stub = parent::buildClass($name); 65 | 66 | $name = Str::of($name) 67 | ->afterLast('\\') 68 | ->kebab() 69 | ->headline() 70 | ->replace('-', ' '); 71 | 72 | $description = "A beautiful {$name} block."; 73 | 74 | $description = text( 75 | label: 'Enter the block description', 76 | placeholder: $description, 77 | ) ?: $description; 78 | 79 | $categories = get_default_block_categories(); 80 | 81 | $category = select( 82 | label: 'Select the block category', 83 | options: $this->collect($categories)->mapWithKeys(fn ($category) => [$category['slug'] => $category['title']]), 84 | default: 'common', 85 | ); 86 | 87 | $postTypes = multiselect( 88 | label: 'Select the supported post types', 89 | options: $this->collect( 90 | get_post_types(['public' => true]) 91 | )->mapWithKeys(fn ($postType) => [$postType => Str::headline($postType)])->all(), 92 | hint: 'Leave empty to support all post types.', 93 | ); 94 | 95 | $postTypes = $this->collect($postTypes) 96 | ->map(fn ($postType) => sprintf("'%s'", $postType)) 97 | ->join(', '); 98 | 99 | $supports = multiselect( 100 | label: 'Select the supported block features', 101 | options: $this->getSupports(), 102 | default: config('acf.generators.supports', []), 103 | scroll: 8, 104 | ); 105 | 106 | $stub = str_replace( 107 | ['DummySupports', 'DummyDescription', 'DummyCategory', 'DummyPostTypes'], 108 | [$this->buildSupports($supports), $description, $category, $postTypes], 109 | $stub 110 | ); 111 | 112 | return $stub; 113 | } 114 | 115 | /** 116 | * Build the block supports array. 117 | */ 118 | protected function buildSupports(array $selected): string 119 | { 120 | return $this->collect($this->supports)->map(function ($value, $key) use ($selected) { 121 | if (is_int($key)) { 122 | return sprintf("'%s' => %s,", $value, in_array($value, $selected) ? 'true' : 'false'); 123 | } 124 | 125 | $options = $this->collect($value) 126 | ->map(fn ($option) => sprintf( 127 | "%s'%s' => %s,", 128 | Str::repeat(' ', 12), 129 | $option, 130 | in_array($option, $selected) ? 'true' : 'false' 131 | )) 132 | ->join("\n"); 133 | 134 | return sprintf("'%s' => [\n%s\n ],", $key, $options); 135 | })->join("\n "); 136 | } 137 | 138 | /** 139 | * Retrieve the support options. 140 | */ 141 | protected function getSupports(): array 142 | { 143 | return $this->collect($this->supports) 144 | ->mapWithKeys(fn ($value, $key) => is_array($value) 145 | ? $this->collect($value)->mapWithKeys(fn ($option) => [$option => Str::of($option)->finish(" {$key}")->headline()->toString()])->all() 146 | : [$value => Str::headline($value)] 147 | )->all(); 148 | } 149 | 150 | /** 151 | * Get the stub file for the generator. 152 | * 153 | * @return string 154 | */ 155 | protected function getStub() 156 | { 157 | if ($this->option('localize')) { 158 | return $this->resolveStub('block.localized'); 159 | } 160 | 161 | return $this->resolveStub('block'); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Console/CacheCommand.php: -------------------------------------------------------------------------------- 1 | composer = $this->laravel['AcfComposer']; 47 | 48 | if ($this->option('clear')) { 49 | return $this->call('acf:clear'); 50 | } 51 | 52 | if ($this->option('status')) { 53 | $status = $this->composer->manifest()->exists() 54 | ? 'cached' 55 | : 'not cached'; 56 | 57 | return $this->components->info("ACF Composer is currently {$status}."); 58 | } 59 | 60 | $composers = $this->collect( 61 | $this->composer->composers() 62 | )->flatten(); 63 | 64 | $composers->each(function ($composer) { 65 | if (! $this->composer->manifest()->add($composer)) { 66 | $name = $composer::class; 67 | 68 | return $this->components->error("Failed to add {$name} to the manifest."); 69 | } 70 | 71 | $this->count++; 72 | }); 73 | 74 | if ($this->count !== $composers->count() && ! $this->option('force')) { 75 | return $this->components->error('Failed to cache the ACF Composer field groups.'); 76 | } 77 | 78 | if (! $manifest = $this->composer->manifest()->write()) { 79 | return $this->components->error('Failed to write the ACF Composer manifest.'); 80 | } 81 | 82 | $blocks = ! $this->option('manifest') 83 | ? $this->composer->manifest()->writeBlocks() 84 | : 0; 85 | 86 | $this->components->info("Successfully cached {$manifest} field group(s) and {$blocks} block(s)."); 87 | } 88 | 89 | /** 90 | * Configure the command. 91 | * 92 | * @return void 93 | */ 94 | public function configure() 95 | { 96 | $this->setAliases([ 97 | 'acf:optimize', 98 | ]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Console/ClearCommand.php: -------------------------------------------------------------------------------- 1 | composer = $this->laravel['AcfComposer']; 35 | 36 | return $this->composer->manifest()->delete() 37 | ? $this->components->info('Successfully cleared the ACF Composer cache.') 38 | : $this->components->info('The ACF Composer cache is already cleared.'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/FieldMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveStub('field'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/IdeHelpersCommand.php: -------------------------------------------------------------------------------- 1 | components->info('You do not have any configured custom field types.'); 35 | } 36 | 37 | $path = $this->option('path') ? 38 | base_path($this->option('path')) : 39 | __DIR__.'/../../_ide-helpers.php'; 40 | 41 | $methods = []; 42 | 43 | foreach ($types as $key => $value) { 44 | $key = Str::studly($key); 45 | 46 | $methods[] = "* @method \Log1x\AcfComposer\Builder\FieldBuilder add{$key}(string \$name, array \$args = [])"; 47 | } 48 | 49 | $methods = implode("\n\t ", $methods); 50 | 51 | $builder = <<components->info('The IDE helpers have been successfully generated.'); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Console/MakeCommand.php: -------------------------------------------------------------------------------- 1 | name = $this->qualifyClass($this->getNameInput()); 36 | $this->path = $this->getPath($this->name); 37 | $this->type = str_replace('/', ' ', $this->type); 38 | 39 | $this->createClass(); 40 | $this->createView(); 41 | $this->summary(); 42 | } 43 | 44 | /** 45 | * Replace the namespace for the given stub. 46 | * 47 | * @param string $stub 48 | * @param string $name 49 | * @return $this 50 | */ 51 | protected function replaceDummyStrings(&$stub, $name) 52 | { 53 | $searches = [ 54 | ['DummyTitle', 'DummyCamel', 'DummySlug', 'DummySnake'], 55 | ['{{ DummyTitle }}', '{{ DummyCamel }}', '{{ DummySlug }}', '{{ DummySnake }}'], 56 | ['{{DummyTitle}}', '{{DummyCamel}}', '{{DummySlug}}', '{{DummySnake}}'], 57 | ]; 58 | 59 | $name = Str::title( 60 | str_replace('-', ' ', Str::kebab(Str::afterLast($name, '\\'))) 61 | ); 62 | 63 | foreach ($searches as $search) { 64 | $stub = str_replace( 65 | $search, 66 | [$name, Str::camel($name), Str::kebab($name), Str::snake($name)], 67 | $stub 68 | ); 69 | } 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Build the class with the given name. 76 | * 77 | * @param string $name 78 | * @return string 79 | * 80 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 81 | */ 82 | protected function buildClass($name) 83 | { 84 | $stub = $this->files->get($this->getStub()); 85 | 86 | return $this 87 | ->replaceDummyStrings($stub, $name) 88 | ->replaceNamespace($stub, $name) 89 | ->replaceClass($stub, $name); 90 | } 91 | 92 | /** 93 | * Return the full view destination. 94 | * 95 | * @return string 96 | */ 97 | public function getView() 98 | { 99 | return Str::finish($this->getViewPath(), $this->getViewName()); 100 | } 101 | 102 | /** 103 | * Return the view destination filename. 104 | * 105 | * @return string 106 | */ 107 | public function getViewName() 108 | { 109 | return Str::finish( 110 | str_replace('.', '/', Str::slug(Str::snake($this->getNameInput()))), 111 | '.blade.php' 112 | ); 113 | } 114 | 115 | /** 116 | * Return the view destination path. 117 | * 118 | * @return string 119 | */ 120 | public function getViewPath() 121 | { 122 | return $this->getPaths().'/'.Str::slug(Str::plural($this->type)).'/'; 123 | } 124 | 125 | /** 126 | * Get the view stub file for the generator. 127 | * 128 | * @return string 129 | */ 130 | protected function getViewStub() 131 | { 132 | if (empty($this->view)) { 133 | return; 134 | } 135 | 136 | return $this->resolveStub("views/{$this->view}"); 137 | } 138 | 139 | /** 140 | * Get the default namespace for the class. 141 | * 142 | * @param string $rootNamespace 143 | * @return string 144 | */ 145 | protected function getDefaultNamespace($rootNamespace) 146 | { 147 | return $rootNamespace.'\\'.Str::plural($this->type); 148 | } 149 | 150 | /** 151 | * Return the applications view path. 152 | * 153 | * @param string $name 154 | * @return void 155 | */ 156 | protected function getPaths() 157 | { 158 | $paths = $this->app['view.finder']->getPaths(); 159 | 160 | if (count($paths) === 1) { 161 | return head($paths); 162 | } 163 | 164 | return $this->choice('Where do you want to create the view(s)?', $paths, head($paths)); 165 | } 166 | 167 | /** 168 | * Return the Composer type. 169 | * 170 | * @return void 171 | */ 172 | protected function getType() 173 | { 174 | return $this->collect(explode('\\', $this->type))->map(function ($value) { 175 | return Str::singular(Str::slug($value)); 176 | })->implode(' '); 177 | } 178 | 179 | /** 180 | * Create a Composer class. 181 | * 182 | * @return void 183 | */ 184 | protected function createClass() 185 | { 186 | if ($this->isReservedName($this->getNameInput())) { 187 | throw new Exception('The name "'.$this->getNameInput().'" is reserved by PHP.'); 188 | } 189 | 190 | if ( 191 | (! $this->hasOption('force') || 192 | ! $this->option('force')) && 193 | $this->alreadyExists($this->getNameInput()) 194 | ) { 195 | throw new Exception('File `'.$this->shortenPath($this->path).'` already exists.'); 196 | } 197 | 198 | $this->makeDirectory($this->path); 199 | 200 | $this->files->put($this->path, $this->sortImports($this->buildClass($this->name))); 201 | } 202 | 203 | /** 204 | * Create the Composer view. 205 | * 206 | * @return void 207 | */ 208 | protected function createView() 209 | { 210 | if ( 211 | empty($this->getViewStub()) || 212 | (! $this->hasOption('force') || 213 | ! $this->option('force')) && 214 | $this->files->exists($this->getView()) 215 | ) { 216 | return; 217 | } 218 | 219 | if (! $this->files->exists($this->getViewPath())) { 220 | $this->files->makeDirectory($this->getViewPath()); 221 | } 222 | 223 | $this->files->put($this->getView(), $this->files->get($this->getViewStub())); 224 | } 225 | 226 | /** 227 | * Return the block creation summary. 228 | * 229 | * @return void 230 | */ 231 | protected function summary() 232 | { 233 | if (! Str::contains($this->type, ['Block', 'Widget'])) { 234 | $this->newLine(); 235 | } 236 | 237 | $this->line("🎉 {$this->getNameInput()} {$this->getType()} successfully composed."); 238 | $this->line(" ⮑ {$this->shortenPath($this->path)}"); 239 | 240 | if ($this->view) { 241 | $this->line(" ⮑ {$this->shortenPath($this->getView(), 4)}"); 242 | } 243 | 244 | if (! function_exists('acf')) { 245 | $this->newLine(); 246 | 247 | $this->components->warn('Advanced Custom Fields does not appear to be activated.'); 248 | } 249 | } 250 | 251 | /** 252 | * Returns a shortened path. 253 | * 254 | * @param string $path 255 | * @param int $index 256 | * @return string 257 | */ 258 | protected function shortenPath($path, $index = 3) 259 | { 260 | return $this->collect( 261 | explode('/', $path) 262 | )->slice(-$index, $index)->implode('/'); 263 | } 264 | 265 | /** 266 | * Get the stub file for the generator. 267 | * 268 | * @return string 269 | */ 270 | protected function getStub() 271 | { 272 | // 273 | } 274 | 275 | /** 276 | * Get the resolved stub file path. 277 | * 278 | * @param string $name 279 | * @return string 280 | */ 281 | protected function resolveStub($name) 282 | { 283 | $path = '/'.$name.'.stub'; 284 | 285 | return $this->files->exists($stubsPath = $this->app->basePath('stubs/acf-composer').$path) 286 | ? $stubsPath 287 | : __DIR__.'/stubs'.$path; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/Console/OptionsMakeCommand.php: -------------------------------------------------------------------------------- 1 | option('full')) { 45 | return $this->resolveStub('options.full'); 46 | } 47 | 48 | return $this->resolveStub('options'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Console/PartialMakeCommand.php: -------------------------------------------------------------------------------- 1 | resolveStub('partial'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/StubPublishCommand.php: -------------------------------------------------------------------------------- 1 | app->basePath('stubs/acf-composer'))) { 52 | (new Filesystem)->makeDirectory($stubsPath, 0755, true); 53 | } 54 | 55 | if (! is_dir($stubsPath.'/views')) { 56 | (new Filesystem)->makeDirectory($stubsPath.'/views', 0755, true); 57 | } 58 | 59 | $files = $this->collect($this->stubs) 60 | ->mapWithKeys(function ($stub) use ($stubsPath) { 61 | return [__DIR__.'/stubs/'.$stub => $stubsPath.'/'.$stub]; 62 | })->toArray(); 63 | 64 | foreach ($files as $from => $to) { 65 | if (! file_exists($to) || $this->option('force')) { 66 | file_put_contents($to, file_get_contents($from)); 67 | } 68 | } 69 | 70 | $this->info('🎉 ACF Composer stubs successfully published.'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Console/UpgradeCommand.php: -------------------------------------------------------------------------------- 1 | composer = $this->laravel['AcfComposer']; 44 | 45 | $this->replacements = [ 46 | 'use StoutLogic\\AcfBuilder\\FieldsBuilder;' => 'use Log1x\\AcfComposer\\Builder;', 47 | 'new FieldsBuilder(' => 'Builder::make(', 48 | 'public function assets($block)' => 'public function assets(array $block): void', 49 | 'public function enqueue($block)' => 'public function assets(array $block): void', 50 | 'public function enqueue($block = [])' => 'public function assets(array $block): void', 51 | 'public function enqueue()' => 'public function assets(array $block): void', 52 | '/->addFields\(\$this->get\((.*?)\)\)/' => fn ($match) => "->addPartial({$match[1]})", 53 | '/->addLayout\(\$this->get\((.*?)\)\)/' => fn ($match) => "->addLayout({$match[1]})", 54 | ]; 55 | 56 | $this->components->info('Checking for outdated ACF Composer classes...'); 57 | 58 | $classes = $this->collect($this->composer->paths())->flatMap(fn ($classes, $path) => $this->collect($classes) 59 | ->map(fn ($class) => Str::of($class)->replace('\\', '/')->after('/')->start($path.'/')->finish('.php')->toString()) 60 | ->all() 61 | ) 62 | ->filter(fn ($class) => file_exists($class)) 63 | ->mapWithKeys(fn ($class) => [$class => file_get_contents($class)]) 64 | ->filter(fn ($class) => Str::contains($class, array_keys($this->replacements))); 65 | 66 | if ($classes->isEmpty()) { 67 | return $this->components->info('No outdated ACF Composer classes found.'); 68 | } 69 | 70 | $files = $classes 71 | ->keys() 72 | ->map(fn ($class) => basename($class, '.php')) 73 | ->map(fn ($class) => "{$class}") 74 | ->all(); 75 | 76 | $this->components->bulletList($files); 77 | 78 | if (! $this->components->confirm("Found {$classes->count()} ACF Composer classes to upgrade. Do you wish to continue?", true)) { 79 | return $this->components->error('The ACF Composer upgrade has been cancelled.'); 80 | } 81 | 82 | $classes->each(function ($class, $path) { 83 | $name = basename($path, '.php'); 84 | 85 | $this->components->task( 86 | "Upgrading the {$name} class", 87 | fn () => $this->handleUpgrade($path, $class) 88 | ); 89 | }); 90 | 91 | $this->newLine(); 92 | 93 | $this->components->info("Successfully upgraded {$classes->count()} ACF Composer classes."); 94 | } 95 | 96 | /** 97 | * Upgrade the ACF Composer class file. 98 | */ 99 | protected function handleUpgrade(string $path, string $class): bool 100 | { 101 | foreach ($this->replacements as $pattern => $replacement) { 102 | $class = is_callable($replacement) ? 103 | preg_replace_callback($pattern, $replacement, $class) : 104 | str_replace($pattern, $replacement, $class); 105 | } 106 | 107 | return file_put_contents($path, $class) !== false; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Console/UsageCommand.php: -------------------------------------------------------------------------------- 1 | components->error('Advanced Custom Fields must be installed and activated to use this command.'); 43 | } 44 | 45 | $this->types = $this->types(); 46 | 47 | $field = $this->argument('field') ?: search( 48 | label: "Search {$this->types->count()} registered field types", 49 | options: fn (string $value) => $this->search($value)->all(), 50 | hint: '* indicates a custom field type', 51 | scroll: 8, 52 | ); 53 | 54 | $type = $this->type($field); 55 | 56 | if (! $type) { 57 | return $this->components->error("The field type [{$field}] could not be found."); 58 | } 59 | 60 | $options = $this->collect([ 61 | ...$type->defaults ?? [], 62 | ...$type->supports ?? [], 63 | ])->except('escaping_html'); 64 | 65 | table(['Label', 'Name', 'Category', 'Options #', 'Source'], [[ 66 | $type->label, 67 | $type->name, 68 | Str::title($type->category), 69 | $options->count(), 70 | $type->source, 71 | ]]); 72 | 73 | $method = Str::of($type->name) 74 | ->studly() 75 | ->start('add') 76 | ->toString(); 77 | 78 | $native = method_exists('Log1x\AcfComposer\Builder', $method) ? $method : null; 79 | 80 | $options = $this->collect($options)->map(fn ($value) => match (true) { 81 | is_string($value) => "'{$value}'", 82 | is_array($value) => '[]', 83 | is_bool($value) => $value ? 'true' : 'false', 84 | default => $value, 85 | })->map(fn ($value, $key) => "\t '{$key}' => {$value},"); 86 | 87 | $usage = view('acf-composer::field-usage', [ 88 | 'name' => $type->name, 89 | 'label' => "{$type->label} Example", 90 | 'options' => $options->implode("\n"), 91 | 'native' => $native, 92 | ]); 93 | 94 | $usage = $this->collect(explode("\n", $usage)) 95 | ->map(fn ($line) => rtrim(" {$line}")) 96 | ->filter() 97 | ->implode("\n"); 98 | 99 | $this->line($usage); 100 | } 101 | 102 | /** 103 | * Search for a field type. 104 | */ 105 | protected function search(?string $search = null): Collection 106 | { 107 | if (filled($search)) { 108 | return $this->types 109 | ->filter(fn ($type) => Str::contains($type->label, $search, ignoreCase: true) || Str::contains($type->name, $search, ignoreCase: true)) 110 | ->map(fn ($type) => $type->label) 111 | ->values(); 112 | } 113 | 114 | return $this->types->map(fn ($type) => $type->label); 115 | } 116 | 117 | /** 118 | * Retrieve a field type by label or name. 119 | */ 120 | protected function type(string $field): ?object 121 | { 122 | $exactMatch = $this->types->first(fn ($type) => strcasecmp($type->label, $field) === 0 || strcasecmp($type->name, $field) === 0); 123 | 124 | if ($exactMatch) { 125 | return $exactMatch; 126 | } 127 | 128 | $matches = $this->types->filter(fn ($type) => strcasecmp($type->label, $field) === 0 || strcasecmp($type->name, $field) === 0 129 | || Str::contains($type->label, $field, ignoreCase: true) 130 | || Str::contains($type->name, $field, ignoreCase: true)); 131 | 132 | if ($matches->isEmpty()) { 133 | return null; 134 | } 135 | 136 | if ($matches->count() === 1) { 137 | return $matches->first(); 138 | } 139 | 140 | $selected = search( 141 | label: "Found {$matches->count()} registered field types Please choose one:", 142 | options: fn () => $matches->pluck('label', 'name')->all(), 143 | hint: '* indicates a custom field type', 144 | scroll: 8, 145 | ); 146 | 147 | return $matches->firstWhere('name', $selected); 148 | } 149 | 150 | /** 151 | * Retrieve all registered field types. 152 | */ 153 | protected function types(): Collection 154 | { 155 | return $this->collect( 156 | acf_get_field_types() 157 | )->map(function ($type) { 158 | if (Str::startsWith($type->doc_url, 'https://www.advancedcustomfields.com')) { 159 | $type->source = 'Official'; 160 | 161 | return $type; 162 | } 163 | 164 | $type->label = "{$type->label} *"; 165 | $type->source = $this->source($type); 166 | 167 | return $type; 168 | })->sortBy('label'); 169 | } 170 | 171 | /** 172 | * Attempt to retrieve the field type source. 173 | */ 174 | protected function source(object $type): string 175 | { 176 | $paths = [WPMU_PLUGIN_DIR, WP_PLUGIN_DIR]; 177 | 178 | $plugin = (new \ReflectionClass($type))->getFileName(); 179 | 180 | if (! Str::contains($plugin, $paths)) { 181 | return 'Unknown'; 182 | } 183 | 184 | $source = Str::of($plugin)->replace($paths, '') 185 | ->trim('/') 186 | ->explode('/') 187 | ->first(); 188 | 189 | return Str::of($source) 190 | ->headline() 191 | ->replace('Acf', 'ACF'); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Console/WidgetMakeCommand.php: -------------------------------------------------------------------------------- 1 | afterLast('\\') 49 | ->kebab() 50 | ->headline() 51 | ->replace('-', ' '); 52 | 53 | $description = "A beautiful {$name} widget."; 54 | 55 | $description = text( 56 | label: 'Enter the widget description', 57 | placeholder: $description, 58 | ) ?: $description; 59 | 60 | $stub = str_replace('DummyDescription', $description, $stub); 61 | 62 | return $stub; 63 | } 64 | 65 | /** 66 | * Get the stub file for the generator. 67 | * 68 | * @return string 69 | */ 70 | protected function getStub() 71 | { 72 | return $this->resolveStub('widget'); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Console/stubs/block.localized.stub: -------------------------------------------------------------------------------- 1 | [ 110 | ['item' => 'Item one'], 111 | ['item' => 'Item two'], 112 | ['item' => 'Item three'], 113 | ], 114 | ]; 115 | 116 | /** 117 | * The block template. 118 | * 119 | * @var array 120 | */ 121 | public $template = [ 122 | 'core/heading' => ['placeholder' => 'Hello World'], 123 | 'core/paragraph' => ['placeholder' => 'Welcome to the DummyTitle block.'], 124 | ]; 125 | 126 | /** 127 | * The block name. 128 | */ 129 | public function getName(): string 130 | { 131 | return __('DummyTitle', 'sage'); 132 | } 133 | 134 | /** 135 | * The block description. 136 | */ 137 | public function getDescription(): string 138 | { 139 | return __('DummyDescription', 'sage'); 140 | } 141 | 142 | /** 143 | * Data to be passed to the block before rendering. 144 | */ 145 | public function with(): array 146 | { 147 | return [ 148 | 'items' => $this->items(), 149 | ]; 150 | } 151 | 152 | /** 153 | * The block field group. 154 | */ 155 | public function fields(): array 156 | { 157 | $fields = Builder::make('DummySnake'); 158 | 159 | $fields 160 | ->addRepeater('items') 161 | ->addText('item') 162 | ->endRepeater(); 163 | 164 | return $fields->build(); 165 | } 166 | 167 | /** 168 | * Retrieve the items. 169 | * 170 | * @return array 171 | */ 172 | public function items() 173 | { 174 | return get_field('items') ?: $this->example['items']; 175 | } 176 | 177 | /** 178 | * Assets enqueued with 'enqueue_block_assets' when rendering the block. 179 | * 180 | * @link https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/#editor-content-scripts-and-styles 181 | */ 182 | public function assets(array $block): void 183 | { 184 | // 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Console/stubs/block.stub: -------------------------------------------------------------------------------- 1 | null, 101 | 'margin' => null, 102 | ]; 103 | 104 | /** 105 | * The supported block features. 106 | * 107 | * @var array 108 | */ 109 | public $supports = [ 110 | DummySupports 111 | ]; 112 | 113 | /** 114 | * The block styles. 115 | * 116 | * @var array 117 | */ 118 | public $styles = ['light', 'dark']; 119 | 120 | /** 121 | * The block preview example data. 122 | * 123 | * @var array 124 | */ 125 | public $example = [ 126 | 'items' => [ 127 | ['item' => 'Item one'], 128 | ['item' => 'Item two'], 129 | ['item' => 'Item three'], 130 | ], 131 | ]; 132 | 133 | /** 134 | * The block template. 135 | * 136 | * @var array 137 | */ 138 | public $template = [ 139 | 'core/heading' => ['placeholder' => 'Hello World'], 140 | 'core/paragraph' => ['placeholder' => 'Welcome to the DummyTitle block.'], 141 | ]; 142 | 143 | /** 144 | * Data to be passed to the block before rendering. 145 | */ 146 | public function with(): array 147 | { 148 | return [ 149 | 'items' => $this->items(), 150 | ]; 151 | } 152 | 153 | /** 154 | * The block field group. 155 | */ 156 | public function fields(): array 157 | { 158 | $fields = Builder::make('DummySnake'); 159 | 160 | $fields 161 | ->addRepeater('items') 162 | ->addText('item') 163 | ->endRepeater(); 164 | 165 | return $fields->build(); 166 | } 167 | 168 | /** 169 | * Retrieve the items. 170 | * 171 | * @return array 172 | */ 173 | public function items() 174 | { 175 | return get_field('items') ?: $this->example['items']; 176 | } 177 | 178 | /** 179 | * Assets enqueued with 'enqueue_block_assets' when rendering the block. 180 | * 181 | * @link https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/#editor-content-scripts-and-styles 182 | */ 183 | public function assets(array $block): void 184 | { 185 | // 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Console/stubs/field.stub: -------------------------------------------------------------------------------- 1 | setLocation('post_type', '==', 'post'); 19 | 20 | $fields 21 | ->addRepeater('items') 22 | ->addText('item') 23 | ->endRepeater(); 24 | 25 | return $fields->build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Console/stubs/options.full.stub: -------------------------------------------------------------------------------- 1 | addRepeater('items') 119 | ->addText('item') 120 | ->endRepeater(); 121 | 122 | return $fields->build(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Console/stubs/options.stub: -------------------------------------------------------------------------------- 1 | addRepeater('items') 33 | ->addText('item') 34 | ->endRepeater(); 35 | 36 | return $fields->build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/stubs/partial.stub: -------------------------------------------------------------------------------- 1 | addRepeater('items') 19 | ->addText('item') 20 | ->endRepeater(); 21 | 22 | return $fields; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Console/stubs/views/block.stub: -------------------------------------------------------------------------------- 1 | @unless ($block->preview) 2 |
3 | @endunless 4 | 5 | @if ($items) 6 |
    7 | @foreach ($items as $item) 8 |
  • {{ $item['item'] }}
  • 9 | @endforeach 10 |
11 | @else 12 |

{{ $block->preview ? 'Add an item...' : 'No items found!' }}

13 | @endif 14 | 15 |
16 | 17 |
18 | 19 | @unless ($block->preview) 20 |
21 | @endunless 22 | -------------------------------------------------------------------------------- /src/Console/stubs/views/widget.stub: -------------------------------------------------------------------------------- 1 | @if ($items) 2 |
    3 | @foreach ($items as $item) 4 |
  • {{ $item['item'] }}
  • 5 | @endforeach 6 |
7 | @else 8 |

No items found!

9 | @endif -------------------------------------------------------------------------------- /src/Console/stubs/widget.stub: -------------------------------------------------------------------------------- 1 | $this->items(), 31 | ]; 32 | } 33 | 34 | /** 35 | * The widget title. 36 | */ 37 | public function title(): string 38 | { 39 | return get_field('title', $this->widget->id); 40 | } 41 | 42 | /** 43 | * The widget field group. 44 | */ 45 | public function fields(): array 46 | { 47 | $fields = Builder::make('DummySnake'); 48 | 49 | $fields 50 | ->addText('title'); 51 | 52 | $fields 53 | ->addRepeater('items') 54 | ->addText('item') 55 | ->endRepeater(); 56 | 57 | return $fields->build(); 58 | } 59 | 60 | /** 61 | * Return the items field. 62 | * 63 | * @return array 64 | */ 65 | public function items() 66 | { 67 | return get_field('items', $this->widget->id) ?: []; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Contracts/Block.php: -------------------------------------------------------------------------------- 1 | fields)) { 16 | return; 17 | } 18 | 19 | if ($callback) { 20 | $callback(); 21 | } 22 | 23 | foreach ($this->fields as $fields) { 24 | acf_add_local_field_group( 25 | $this->build($fields) 26 | ); 27 | } 28 | } 29 | 30 | /** 31 | * Retrieve the field group fields. 32 | */ 33 | public function getFields(bool $cache = true): array 34 | { 35 | if ($this->fields && $cache) { 36 | return $this->fields; 37 | } 38 | 39 | if ($cache && $this->composer->manifest()->has($this)) { 40 | return $this->composer->manifest()->get($this); 41 | } 42 | 43 | $fields = $this->resolveFields(); 44 | 45 | if (blank($fields)) { 46 | return []; 47 | } 48 | 49 | $fields = is_a($fields, FieldsBuilder::class) 50 | ? [$fields] 51 | : $fields; 52 | 53 | if (! is_array($fields)) { 54 | throw new InvalidFieldsException; 55 | } 56 | 57 | $fields = ! empty($fields['key']) ? [$fields] : $fields; 58 | 59 | foreach ($fields as $key => $field) { 60 | $fields[$key] = is_a($field, FieldsBuilder::class) 61 | ? $field->build() 62 | : $field; 63 | } 64 | 65 | if ($this->defaults->has('field_group')) { 66 | foreach ($fields as $key => $field) { 67 | $fields[$key] = array_merge($this->defaults->get('field_group'), $field); 68 | } 69 | } 70 | 71 | return $fields; 72 | } 73 | 74 | /** 75 | * Compose and register the defined field groups with ACF. 76 | * 77 | * @return mixed 78 | */ 79 | public function compose() 80 | { 81 | if (empty($this->fields)) { 82 | return; 83 | } 84 | 85 | $this->register(); 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Manifest.php: -------------------------------------------------------------------------------- 1 | composer = $composer; 38 | 39 | $this->path = Str::finish( 40 | config('acf-composer.manifest', storage_path('framework/cache')), 41 | '/acf-composer' 42 | ); 43 | 44 | $this->manifest = $this->exists() 45 | ? Collection::make(require $this->path()) 46 | : Collection::make(); 47 | } 48 | 49 | /** 50 | * Make a new instance of Manifest. 51 | */ 52 | public static function make(AcfComposer $composer): self 53 | { 54 | return new static($composer); 55 | } 56 | 57 | /** 58 | * Retrieve the manifest path. 59 | */ 60 | public function path(?string $filename = 'manifest.php'): string 61 | { 62 | return "{$this->path}/{$filename}"; 63 | } 64 | 65 | /** 66 | * Determine if the manifest exists. 67 | */ 68 | public function exists(?string $filename = null): bool 69 | { 70 | return file_exists($this->path($filename)); 71 | } 72 | 73 | /** 74 | * Add the Composer to the manifest. 75 | */ 76 | public function add(Composer $composer): bool 77 | { 78 | try { 79 | $this->manifest->put($composer::class, $composer->getFields(cache: false)); 80 | 81 | if (is_a($composer, Block::class)) { 82 | $this->blocks[$composer->jsonPath()] = $composer->toJson(); 83 | } 84 | 85 | return true; 86 | } catch (Exception) { 87 | return false; 88 | } 89 | } 90 | 91 | /** 92 | * Retrieve the cached Composer from the manifest. 93 | */ 94 | public function get(Composer $composer): array 95 | { 96 | return $this->manifest->get($composer::class); 97 | } 98 | 99 | /** 100 | * Determine if the Composer is cached. 101 | */ 102 | public function has(Composer $class): bool 103 | { 104 | return $this->manifest->has($class::class); 105 | } 106 | 107 | /** 108 | * Write the the manifest to disk. 109 | */ 110 | public function write(): ?int 111 | { 112 | File::ensureDirectoryExists($this->path); 113 | 114 | $manifest = $this->toArray(); 115 | 116 | return file_put_contents( 117 | $this->path(), 118 | 'blocks) { 128 | return 0; 129 | } 130 | 131 | $path = $this->path('blocks'); 132 | 133 | foreach ($this->blocks as $path => $block) { 134 | File::ensureDirectoryExists(dirname($path)); 135 | File::put($path, $block); 136 | } 137 | 138 | return count($this->blocks); 139 | } 140 | 141 | /** 142 | * Delete the manifest from disk. 143 | */ 144 | public function delete(): bool 145 | { 146 | return File::isDirectory($this->path) 147 | ? File::deleteDirectory($this->path) 148 | : false; 149 | } 150 | 151 | /** 152 | * Retrieve the manifest as an array. 153 | */ 154 | public function toArray(): array 155 | { 156 | return $this->manifest->all(); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Options.php: -------------------------------------------------------------------------------- 1 | name)) { 122 | return; 123 | } 124 | 125 | if (empty($this->slug)) { 126 | $this->slug = Str::slug($this->name); 127 | } 128 | 129 | if (empty($this->title)) { 130 | $this->title = $this->name; 131 | } 132 | 133 | if (! Arr::has($this->fields, 'location.0.0')) { 134 | Arr::set($this->fields, 'location.0.0', [ 135 | 'param' => 'options_page', 136 | 'operator' => '==', 137 | 'value' => $this->slug, 138 | ]); 139 | } 140 | 141 | $this->register(function () { 142 | if (! $this->menu) { 143 | return; 144 | } 145 | 146 | acf_add_options_page( 147 | array_merge([ 148 | 'menu_title' => $this->name, 149 | 'menu_slug' => $this->slug, 150 | 'page_title' => $this->title, 151 | 'capability' => $this->capability, 152 | 'position' => $this->position, 153 | 'parent_slug' => $this->parent, 154 | 'icon_url' => $this->icon, 155 | 'redirect' => $this->redirect, 156 | 'post_id' => $this->post, 157 | 'autoload' => $this->autoload, 158 | 'update_button' => $this->updateButton(), 159 | 'updated_message' => $this->updatedMessage(), 160 | ], $this->settings) 161 | ); 162 | }); 163 | 164 | return $this; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Partial.php: -------------------------------------------------------------------------------- 1 | resolveFields($args); 16 | 17 | if (blank($fields)) { 18 | return; 19 | } 20 | 21 | return $fields; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Providers/AcfComposerServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('AcfComposer', fn () => AcfComposer::make($this->app)); 21 | 22 | if (! defined('PWP_NAME')) { 23 | define('PWP_NAME', 'ACF Composer'); 24 | } 25 | } 26 | 27 | /** 28 | * Bootstrap any application services. 29 | * 30 | * @return void 31 | */ 32 | public function boot() 33 | { 34 | $this->publishes([ 35 | __DIR__.'/../../config/acf.php' => $this->app->configPath('acf.php'), 36 | ], 'acf-composer'); 37 | 38 | $this->loadViewsFrom(__DIR__.'/../../resources/views', 'acf-composer'); 39 | $this->mergeConfigFrom(__DIR__.'/../../config/acf.php', 'acf'); 40 | 41 | $composer = $this->app->make('AcfComposer'); 42 | 43 | $composer->boot(); 44 | 45 | if ($this->app->runningInConsole()) { 46 | $this->commands([ 47 | Console\BlockMakeCommand::class, 48 | Console\CacheCommand::class, 49 | Console\ClearCommand::class, 50 | Console\FieldMakeCommand::class, 51 | Console\IdeHelpersCommand::class, 52 | Console\OptionsMakeCommand::class, 53 | Console\PartialMakeCommand::class, 54 | Console\StubPublishCommand::class, 55 | Console\UpgradeCommand::class, 56 | Console\UsageCommand::class, 57 | Console\WidgetMakeCommand::class, 58 | ]); 59 | 60 | if (class_exists(AboutCommand::class) && class_exists(InstalledVersions::class)) { 61 | AboutCommand::add('ACF Composer', [ 62 | 'Status' => $composer->manifest()->exists() ? 'CACHED' : 'NOT CACHED', 63 | 'Version' => InstalledVersions::getPrettyVersion('log1x/acf-composer'), 64 | ]); 65 | } 66 | 67 | if (method_exists($this, 'optimizes')) { 68 | $this->optimizes( 69 | optimize: 'acf:cache', 70 | clear: 'acf:clear', 71 | key: 'acf-composer', 72 | ); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Widget.php: -------------------------------------------------------------------------------- 1 | fields) || empty($this->name)) { 58 | return; 59 | } 60 | 61 | if (empty($this->slug)) { 62 | $this->slug = Str::slug($this->name); 63 | } 64 | 65 | if (! Arr::has($this->fields, 'location.0.0')) { 66 | Arr::set($this->fields, 'location.0.0', [ 67 | 'param' => 'widget', 68 | 'operator' => '==', 69 | 'value' => $this->slug, 70 | ]); 71 | } 72 | 73 | $this->register(function () { 74 | $this->widget = (object) $this->collect( 75 | Arr::get($GLOBALS, 'wp_registered_widgets') 76 | )->filter(fn ($value) => $value['name'] === $this->name)->pop(); 77 | }); 78 | 79 | add_filter('widgets_init', function () { 80 | register_widget($this->widget()); 81 | }, 20); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Determine if the widget should be displayed. 88 | */ 89 | public function show(): bool 90 | { 91 | return true; 92 | } 93 | 94 | /** 95 | * Create a new WP_Widget instance. 96 | */ 97 | protected function widget(): WP_Widget 98 | { 99 | return new class($this) extends WP_Widget 100 | { 101 | /** 102 | * Create a new WP_Widget instance. 103 | */ 104 | public function __construct(public Widget $composer) 105 | { 106 | parent::__construct( 107 | $this->composer->slug, 108 | $this->composer->name, 109 | ['description' => $this->composer->description] 110 | ); 111 | } 112 | 113 | /** 114 | * Render the widget. 115 | * 116 | * @param array $args 117 | * @param array $instance 118 | * @return void 119 | */ 120 | public function widget($args, $instance) 121 | { 122 | $this->composer->id = $this->composer->widget->id = Str::start($args['widget_id'], 'widget_'); 123 | 124 | if (! $this->composer->show()) { 125 | return; 126 | } 127 | 128 | echo Arr::get($args, 'before_widget'); 129 | 130 | if (! empty($this->composer->title())) { 131 | echo $this->composer->collect([ 132 | Arr::get($args, 'before_title'), 133 | $this->composer->title(), 134 | Arr::get($args, 'after_title'), 135 | ])->implode(PHP_EOL); 136 | } 137 | 138 | echo $this->composer->view( 139 | Str::finish('widgets.', $this->composer->slug), 140 | ['widget' => $this->composer] 141 | ); 142 | 143 | echo Arr::get($args, 'after_widget'); 144 | } 145 | 146 | /** 147 | * Output the widget settings update form. 148 | * This is intentionally blank due to it being set by ACF. 149 | * 150 | * @param array $instance 151 | * @return void 152 | */ 153 | public function form($instance) 154 | { 155 | // 156 | } 157 | }; 158 | } 159 | } 160 | --------------------------------------------------------------------------------