├── .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 | 
4 | 
5 | 
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 | 
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 |
277 | @foreach ($items as $item)
278 | - {{ $item['item'] }}
279 | @endforeach
280 |
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 |
395 | @foreach ($items as $item)
396 | - {{ $item['item'] }}
397 | @endforeach
398 |
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 |
--------------------------------------------------------------------------------