From 4218fdbd05ed2d981992cc40e0dfac823dab2f3b Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 11 Mar 2020 22:25:14 -0500 Subject: [PATCH] v1.2.0 BREAKING(blocks): `generate()` has been deprecated. Block options are now set using class variables. refactor(composer): Most functionality has been moved to a separate parent class. feat(widgets): Added ACF sidebar widget functionality. Batteries included. feat(options): Added the ability to easily create option pages. feat(console): Added commands / stubs for `acf:widget` and `acf:options` chore(console): Clean up existing stubs. chore(docs): Write actual documentation. chore(composer): Add suggestion for `log1x/modern-acf-options` --- .editorconfig | 6 + LICENSE.md | 2 +- README.md | 107 +++++- composer.json | 5 +- config/acf.php | 14 + resources/views/view-404.blade.php | 9 - src/Block.php | 347 ++++-------------- src/Composer.php | 172 +++++++++ src/Console/BlockMakeCommand.php | 9 +- src/Console/OptionsMakeCommand.php | 50 +++ src/Console/WidgetMakeCommand.php | 103 ++++++ src/Console/stubs/block.full.stub | 105 ++++-- src/Console/stubs/block.stub | 54 +-- src/Console/stubs/field.stub | 18 +- src/Console/stubs/options.stub | 33 ++ .../stubs/views/{block.stub => repeater.stub} | 0 src/Console/stubs/widget.stub | 74 ++++ src/Field.php | 147 +++----- src/Providers/AcfComposerServiceProvider.php | 26 +- src/Widget.php | 157 ++++++++ 20 files changed, 971 insertions(+), 467 deletions(-) delete mode 100644 resources/views/view-404.blade.php create mode 100644 src/Composer.php create mode 100644 src/Console/OptionsMakeCommand.php create mode 100644 src/Console/WidgetMakeCommand.php create mode 100644 src/Console/stubs/options.stub rename src/Console/stubs/views/{block.stub => repeater.stub} (100%) create mode 100644 src/Console/stubs/widget.stub create mode 100644 src/Widget.php diff --git a/.editorconfig b/.editorconfig index ecbdec66..11241731 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,5 +14,11 @@ trim_trailing_whitespace = false [*.php] indent_size = 4 +[src/Console/stubs/*.stub] +indent_size = 4 + +[src/Console/stubs/views/*.stub] +indent_size = 2 + [*.blade.php] indent_size = 2 diff --git a/LICENSE.md b/LICENSE.md index ea7da0e7..9d66fdb7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Brandon Nifong +Copyright (c) 2020 Brandon Nifong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4faa80a0..c090ad87 100644 --- a/README.md +++ b/README.md @@ -4,34 +4,129 @@ ![CircleCI](https://img.shields.io/circleci/build/gh/Log1x/acf-composer.svg?style=flat-square) ![Packagist](https://img.shields.io/packagist/dt/log1x/acf-composer.svg?style=flat-square) -ACF Composer assists you with ~~creating~~ **composing** Fields and Blocks using [ACF Builder](https://github.com/stoutlogic/acf-builder) alongside [Sage 10](https://github.com/roots/sage). +ACF Composer assists you with ~~creating~~ **composing** Fields, Blocks, Widgets, and Options pages using [ACF Builder](https://github.com/stoutlogic/acf-builder) alongside [Sage 10](https://github.com/roots/sage). ## Requirements - [Sage](https://github.com/roots/sage) >= 10.0 - [ACF](https://www.advancedcustomfields.com/) >= 5.8.0 +- [PHP](https://secure.php.net/manual/en/install.php) >= 7.2 +- [Composer](https://getcomposer.org/download/) ## Installation +Install via Composer: + ```bash $ composer require log1x/acf-composer ``` ## Usage -### Publish Config +### Basic Usage + +Start by publishing the `config/acf.php` configuration file using Acorn: ```bash $ wp acorn vendor:publish --provider="Log1x\AcfComposer\Providers\AcfComposerServiceProvider" ``` -Initialize fields, blocks, and default field type values in `config/acf.php`. +Looking at the config file, you will see documented keys for configuration. When creating a field group, simply add each class to their respective type. + +### Generating a Field + +Generating fields with ACF Composer is done using Acorn. + +To create your first field, start by running the following command from your theme directory: + +```bash +$ wp acorn acf:field Example +``` + +This will create `src/Fields/Example.php` which is where you will create and manage your field group. + +Once finished, follow up by uncommenting `App\Fields\Example::class` in `acf.php`. + +Taking a glance at the generated `Example.php` stub, you will notice that it has a simple list configured. + +Proceed by checking the `Add Post` for the field to ensure things are working as intended– and then [get to work](https://github.com/Log1x/acf-builder-cheatsheet). + +### Generating a Block + +Generating a Block is generally the same as generating a field as seen above. + +Start by creating the Block field using Acorn: + +```bash +$ wp acorn acf:block Example +``` + +Optionally, you may pass `--full` to the command above to generate a stub that contains additional configuration examples. + +```bash +$ wp acorn acf:block Example --full +``` + +Once finished, similarily to Fields, simply add the new block, `App\Blocks\Example::class` to `config/acf.php`. -### Create a Block or Field +When running the ACF Block generator, one difference to a generic field is an accompanied View is generated in the `resources/views/blocks` directory. + +Like the Field generator, the example block contains a simple list repeater and is working out of the box. + +### Generating a Widget + +Creating a sidebar widget using ACF Composer is extremely easy. Widgets are automatically loaded and rendered with Blade. Batteries included. + +Start by creating a Widget using Acorn: ```bash -$ wp acorn acf:block MyBlock -$ wp acorn acf:field MyField +$ wp acorn acf:widget Example +``` + +Once finished, simply add `App\Widgets\Example::class` to the `widgets` key in `config/acf.php` + +Similar to Blocks, Widgets are also accompanied by a view generated in `resources/views/widgets`. + +Out of the box, the Example widget is ready to go and should appear in the backend. + +### Generating an Options Page + +When creating a field, you have the option of populating the `$options` variable automatically generating an Options page as well as setting the field group location. + +Start by creating an option page using Acorn: + +```bash +$ wp acorn acf:options Options +``` + +Outside of the `$options` variable being set in the options stub, it is effectively a Field. That being said, `App\Fields\Options::class` should be registered in the `fields` array in `config/acf.php` + +### Field Defaults + +One of my personal favorite features of ACF Composer is thet ability to set field defaults. + +Taking a look at `config/acf.php`, you will see a few pre-configured defaults: + +```php +'defaults' => [ + 'trueFalse' => ['ui' => 1], + 'select' => ['ui' => 1], +], +``` + +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. + +Here are a few others that I personally use: + +```php +'defaults' => [ + 'fieldGroup' => ['instruction_placement' => 'acfe_instructions_tooltip'], + 'repeater' => ['layout' => 'block', 'acfe_repeater_stylised_button' => 1], + 'postObject' => ['ui' => 1, 'return_format' => 'object'], + 'accordion' => ['multi_expand' => 1], + 'group' => ['layout' => 'table', 'acfe_group_modal' => 1], + 'tab' => ['placement' => 'left'], +], ``` ## Bug Reports diff --git a/composer.json b/composer.json index 6de1f9e1..2459c790 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "log1x/acf-composer", "type": "package", - "description": "ACF Composer assists you with creating Fields and Blocks with ACF Builder and Roots Sage", + "description": "Create fields, blocks, and more using ACF Builder and Roots Sage", "license": "MIT", "authors": [ { @@ -22,7 +22,8 @@ }, "suggest": { "stoutlogic/acf-builder": "Provides an essential fluid interface for creating ACF fields.", - "log1x/sage-directives": "Provides Sage-specific Blade directives for use with WordPress and ACF within your views." + "log1x/sage-directives": "Provides Sage-specific Blade directives for use with WordPress and ACF within your views.", + "log1x/modern-acf-options": "Gives ACF option pages a much needed design overhaul." }, "extra": { "acorn": { diff --git a/config/acf.php b/config/acf.php index a97a7170..d5e262f0 100644 --- a/config/acf.php +++ b/config/acf.php @@ -30,6 +30,20 @@ // App\Blocks\Example::class, ], + /* + |-------------------------------------------------------------------------- + | Sidebar Widgets + |-------------------------------------------------------------------------- + | + | The widgets listed here will be automatically loaded on the + | request to your application. + | + */ + + 'widgets' => [ + // App\Widgets\Example::class, + ], + /* |-------------------------------------------------------------------------- | Default Field Type Settings diff --git a/resources/views/view-404.blade.php b/resources/views/view-404.blade.php deleted file mode 100644 index f27112a2..00000000 --- a/resources/views/view-404.blade.php +++ /dev/null @@ -1,9 +0,0 @@ -
- - View not found. - - - - {{ $view }} - -
diff --git a/src/Block.php b/src/Block.php index c22a4382..2e21ba1a 100644 --- a/src/Block.php +++ b/src/Block.php @@ -2,28 +2,52 @@ namespace Log1x\AcfComposer; -use Roots\Acorn\Application; use Illuminate\Support\Str; use Illuminate\Support\Arr; -use function Roots\asset; -use function Roots\view; - -abstract class Block +abstract class Block extends Composer { /** - * The application instance. + * The block properties. + * + * @var array + */ + protected $block; + + /** + * The block content. + * + * @var string + */ + protected $content; + + /** + * The block preview status. + * + * @var bool + */ + protected $preview; + + /** + * The current post ID. + * + * @param int + */ + protected $post; + + /** + * The block prefix. * - * @var \Roots\Acorn\Application + * @var string */ - protected $app; + protected $prefix = 'acf/'; /** - * Default field type settings. + * The block namespace. * - * @return array + * @var string */ - protected $defaults = []; + protected $namespace; /** * The display name of the block. @@ -96,285 +120,76 @@ abstract class Block protected $supports = []; /** - * Assets enqueued when the block is shown. - * - * @var array - */ - protected $assets = []; - - /** - * The blocks status. - * - * @var boolean - */ - protected $enabled = true; - - /** - * The block field groups. - * - * @var array - */ - protected $fields; - - /** - * The block namespace. - * - * @var string - */ - protected $namespace; - - /** - * The block prefix. - * - * @var string - */ - protected $prefix = 'acf/'; - - /** - * The block properties. - * - * @var array - */ - protected $block; - - /** - * The block content. - * - * @var string - */ - protected $content; - - /** - * The block preview status. - * - * @var bool - */ - protected $preview; - - /** - * The current post ID. - * - * @param int - */ - protected $post; - - /** - * Create a new Block instance. - * - * @param \Roots\Acorn\Application $app - * @return void - */ - public function __construct(Application $app) - { - $this->app = $app; - } - - /** - * Compose the block. + * Compose and register the defined field groups with ACF. * + * @param callback $callback * @return void */ - public function compose() + public function compose($callback = null) { - if (! $this->register() || ! function_exists('acf')) { + if (empty($this->name)) { return; } - collect($this->register())->each(function ($value, $name) { - $this->{$name} = $value; - }); - - $this->defaults = collect( - $this->app->config->get('acf.defaults') - )->merge($this->defaults)->mapWithKeys(function ($value, $key) { - return [Str::snake($key) => $value]; - }); - - $this->slug = Str::slug($this->name); - $this->namespace = $this->prefix . $this->slug; - $this->fields = $this->fields(); - - if (! $this->enabled) { - return; + if (empty($this->namespace)) { + $this->namespace = Str::start($this->slug, $this->prefix); } - add_action('init', function () { - acf_register_block([ - 'name' => $this->slug, - 'title' => $this->name, - 'description' => $this->description, - 'category' => $this->category, - 'icon' => $this->icon, - 'keywords' => $this->keywords, - 'post_types' => $this->post_types, - 'mode' => $this->mode, - 'align' => $this->align, - 'supports' => $this->supports, - 'enqueue_assets' => [$this, 'assets'], - 'render_callback' => function ($block, $content = '', $preview = false, $post = 0) { - $this->block = (object) $block; - $this->content = $content; - $this->preview = $preview; - $this->post = $post; - - echo $this->view(); - } - ]); - - if (! empty($this->fields)) { - if ($this->defaults->has('field_group')) { - $this->fields = array_merge($this->fields, $this->defaults->get('field_group')); - } - - if (! Arr::has($this->fields, 'location.0.0')) { - Arr::set($this->fields, 'location.0.0', [ - 'param' => 'block', - 'operator' => '==', - 'value' => $this->namespace, - ]); - } - - acf_add_local_field_group($this->build()); + parent::compose(function () { + if (! Arr::has($this->fields, 'location.0.0')) { + Arr::set($this->fields, 'location.0.0', [ + 'param' => 'block', + 'operator' => '==', + 'value' => $this->namespace, + ]); } - }, 20); - } - - /** - * Build the field group with our default field type settings. - * - * @param array $fields - * @return array - */ - protected function build($fields = []) - { - return collect($fields ?: $this->fields)->map(function ($value, $key) use ($fields) { - if ( - ! Str::contains($key, ['fields', 'sub_fields', 'layouts']) || - (Str::is($key, 'type') && ! $this->defaults->has($value)) - ) { - return $value; - } - - return array_map(function ($field) { - if (collect($field)->keys()->intersect(['fields', 'sub_fields', 'layouts'])->isNotEmpty()) { - return $this->build($field); - } + }); - return array_merge($this->defaults->get($field['type'], []), $field); - }, $value); - })->all(); + acf_register_block([ + 'name' => $this->slug, + 'title' => $this->name, + 'description' => $this->description, + 'category' => $this->category, + 'icon' => $this->icon, + 'keywords' => $this->keywords, + 'post_types' => $this->post_types, + 'mode' => $this->mode, + 'align' => $this->align, + 'supports' => $this->supports, + 'enqueue_assets' => [$this,'assets'], + 'render_callback' => [$this, 'render'] + ]); } /** - * URI for the block. + * Render the ACF block. * - * @return string + * @param array $block + * @param string $content + * @param bool $preview + * @param int $post + * @return void */ - protected function uri($path = '') + public function render($block, $content = '', $preview = false, $post = 0) { - return str_replace( - get_theme_file_path(), - get_theme_file_uri(), - home_url($path) + $this->block = (object) $block; + $this->content = $content; + $this->preview = $preview; + $this->post = $post; + + echo $this->view( + Str::finish('views.blocks.', $this->slug), + ['block' => $this] ); } /** - * View used for rendering the block. - * - * @return string - */ - public function view() - { - if (view($view = $this->app->resourcePath("views/blocks/{$this->slug}.blade.php"))) { - return view( - $view, - array_merge($this->with(), ['block' => $this->block]) - )->render(); - } - - if (view()->exists($this->app->resourcePath('views/blocks/view-404.blade.php'))) { - return view( - $this->app->resourcePath('views/blocks/view-404.blade.php'), - ['view' => $view] - )->render(); - } - - return view( - __DIR__ . '/../resources/views/view-404.blade.php', - ['view' => $view] - )->render(); - } - - /** - * Assets used when rendering the block. + * Assets enqueued when rendering the block. * * @return void */ - public function assets() - { - $styles = [ - 'css/blocks/' . $this->slug . '.css', - 'styles/blocks/' . $this->slug . '.css', - ]; - - $scripts = [ - 'js/blocks/' . $this->slug . '.js', - 'scripts/blocks/' . $this->slug . '.js', - ]; - - if (! empty($this->assets)) { - foreach ($this->assets as $asset) { - if (Str::endsWith($asset, '.css')) { - $styles = Arr::prepend($styles, $asset); - } - - if (Str::endsWith($asset, '.js')) { - $scripts = Arr::prepend($scripts, $asset); - } - } - } - - foreach ($styles as $style) { - if (asset($style)->exists()) { - wp_enqueue_style($this->namespace, asset($style)->uri(), false, null); - } - } - - foreach ($scripts as $script) { - if (asset($script)->exists()) { - wp_enqueue_script($this->namespace, asset($script)->uri(), null, null, true); - } - } - } - - /** - * Data to be passed to the block before registering. - * - * @return array - */ - public function register() - { - return []; - } - - /** - * Fields to be attached to the block. - * - * @return array - */ - public function fields() - { - return []; - } - - /** - * Data to be passed to the rendered block. - * - * @return array - */ - public function with() + public function enqueue() { - return []; + // } } diff --git a/src/Composer.php b/src/Composer.php new file mode 100644 index 00000000..f240dc5e --- /dev/null +++ b/src/Composer.php @@ -0,0 +1,172 @@ +app = $app; + + $this->defaults = collect( + $this->app->config->get('acf.defaults') + )->merge($this->defaults)->mapWithKeys(function ($value, $key) { + return [Str::snake($key) => $value]; + }); + + if (! empty($this->name) && empty($this->slug)) { + $this->slug = Str::slug($this->name); + } + + $this->fields = $this->fields(); + } + + /** + * Compose and register the defined field groups with ACF. + * + * @param callback $callback + * @return void + */ + public function compose($callback = null) + { + if (! $this->fields) { + return; + } + + add_action('init', function () use ($callback) { + if ($this->defaults->has('field_group')) { + $this->fields = array_merge($this->fields, $this->defaults->get('field_group')); + } + + if ($callback) { + $callback(); + } + + acf_add_local_field_group($this->build()); + }, 20); + } + + /** + * Build the field group with the default field type settings. + * + * @param array $fields + * @return array + */ + protected function build($fields = []) + { + return collect($fields ?: $this->fields)->map(function ($value, $key) { + if ( + ! Str::contains($key, ['fields', 'sub_fields', 'layouts']) || + (Str::is($key, 'type') && ! $this->defaults->has($value)) + ) { + return $value; + } + + return array_map(function ($field) { + if (collect($field)->keys()->intersect(['fields', 'sub_fields', 'layouts'])->isNotEmpty()) { + return $this->build($field); + } + + return array_merge($this->defaults->get($field['type'], []), $field); + }, $value); + })->all(); + } + + /** + * Render the view using Blade. + * + * @param string $view + * @param array $with + * @return string + */ + public function view($view, $with = []) + { + $view = $this->app->resourcePath( + Str::finish( + str_replace('.', '/', basename($view, '.blade.php')), + '.blade.php' + ) + ); + + if (! file_exists($view)) { + return; + } + + return $this->app->make('view')->file( + $view, + array_merge($this->with(), $with) + )->render(); + } + + /** + * Get field partial if it exists. + * + * @param string $name + * @param string $path + * @return mixed + */ + public function get($name = '', $path = 'Fields') + { + $name = strtr($name, [ + '.php' => '', + '.' => '/' + ]); + + include $this->app->path( + Str::finish( + Str::finish($path, '/'), + Str::finish($name, '.php') + ), + ); + } + + /** + * The field group. + * + * @return array + */ + public function fields() + { + return []; + } + + /** + * Data to be passed to the rendered view. + * + * @return array + */ + public function with() + { + return []; + } +} diff --git a/src/Console/BlockMakeCommand.php b/src/Console/BlockMakeCommand.php index a43f8a64..d595271d 100644 --- a/src/Console/BlockMakeCommand.php +++ b/src/Console/BlockMakeCommand.php @@ -13,7 +13,6 @@ class BlockMakeCommand extends GeneratorCommand * @var string */ protected $signature = 'acf:block {name* : The name of the block} - {--with-view : Create a corresponding view for the block} {--full : Scaffold a block that contains the complete configuration.}'; /** @@ -21,7 +20,7 @@ class BlockMakeCommand extends GeneratorCommand * * @var string */ - protected $description = 'Create a new ACF block.'; + protected $description = 'Create a new block using ACF.'; /** * The type of class being generated. @@ -39,10 +38,6 @@ public function handle() { parent::handle(); - if (! $this->option('with-view')) { - return; - } - $view = Str::finish(str_replace('.', '/', Str::slug(head($this->argument('name')))), '.blade.php'); $path = $this->getPaths() . '/blocks/'; @@ -97,7 +92,7 @@ protected function getStub() */ protected function getViewStub() { - return __DIR__ . '/stubs/views/block.stub'; + return __DIR__ . '/stubs/views/repeater.stub'; } /** diff --git a/src/Console/OptionsMakeCommand.php b/src/Console/OptionsMakeCommand.php new file mode 100644 index 00000000..6913e538 --- /dev/null +++ b/src/Console/OptionsMakeCommand.php @@ -0,0 +1,50 @@ +argument('name')))), '.blade.php'); + $path = $this->getPaths() . '/widgets/'; + + if (! $this->files->exists($path)) { + $this->files->makeDirectory($path); + } + + if ($this->files->exists($path . $view)) { + return $this->error("File {$view} already exists!"); + } + + $this->files->put($path . $view, $this->files->get($this->getViewStub())); + + return $this->info("File {$view} created."); + } + + /** + * Return the applications view path. + * + * @param string $name + * @return void + */ + protected function getPaths() + { + $paths = $this->app['view.finder']->getPaths(); + + if (count($paths) === 1) { + return head($paths); + } + + return $this->choice('Where do you want to create the view(s)?', $paths, head($paths)); + } + + /** + * Get the stub file for the generator. + * + * @return string + */ + protected function getStub() + { + return __DIR__ . '/stubs/widget.stub'; + } + + /** + * Get the view stub file for the generator. + * + * @return string + */ + protected function getViewStub() + { + return __DIR__ . '/stubs/views/repeater.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace . '\Widgets'; + } +} diff --git a/src/Console/stubs/block.full.stub b/src/Console/stubs/block.full.stub index 0c3d68e1..2575fe15 100644 --- a/src/Console/stubs/block.full.stub +++ b/src/Console/stubs/block.full.stub @@ -5,69 +5,102 @@ namespace DummyNamespace; use Log1x\AcfComposer\Block; use StoutLogic\AcfBuilder\FieldsBuilder; -class DummyClass extends Block { +class DummyClass extends Block +{ /** - * Default field type configuration. + * The display name of the block. * - * @return array + * @var string + */ + protected $name = 'DummyClass'; + + /** + * The description of the block. + * + * @var string + */ + protected $description = 'Lorem ipsum...'; + + /** + * The category this block belongs to. + * + * @var string + */ + protected $category = 'common'; + + /** + * The icon of this block. + * + * @var string|array + */ + protected $icon = 'star-half'; + + /** + * An array of block keywords. + * + * @var array + */ + protected $keywords = []; + + /** + * An array of post types the block will be available to. + * + * @var array + */ + protected $post_types = ['post', 'page']; + + /** + * The default display mode of the block that is shown to the user. + * + * @var string + */ + protected $mode = 'preview'; + + /** + * The block alignment class. + * + * @var string */ - protected $defaults = []; + protected $align = ''; /** - * Data to be passed to the block before registering. + * Features supported by the block. + * + * @var array + */ + protected $supports = []; + + /** + * Data to be passed to the block before rendering. * * @return array */ - public function register() + public function with() { return [ - 'name' => 'DummyClass', // Block name - 'description' => 'Lorem ipsum', // Block description - 'category' => 'formatting', // Block category - 'icon' => '', // Block icon (Dashicons or SVG) - 'keywords' => [], // Block keywords - 'post_types' => ['post', 'page'], // Block post types - 'mode' => 'preview', // Default block mode - 'align' => '', // Default block alignment - 'assets' => [ - 'scripts/blocks/DummySlug.js', // Assets enqueued when the block is shown. - 'styles/blocks/DummySlug.css', // Defaults to the blocks/DummySlug.{js,css} if they exist. - ], - 'enabled' => true, // Block status + 'items' => $this->items(), ]; } /** - * Fields to be attached to the block. + * The block field group. * * @return array */ public function fields() { - $example = new FieldsBuilder('example'); + $DummySlug = new FieldsBuilder('DummySlug'); - $example + $DummySlug ->addRepeater('items') ->addText('item') ->endRepeater(); - return $example->build(); - } - - /** - * Data to be passed to the rendered block. - * - * @return array - */ - public function with() - { - return [ - 'items' => $this->items(), - ]; + return $DummySlug->build(); } /** - * Returns the items field. + * Return the items field. * * @return array */ diff --git a/src/Console/stubs/block.stub b/src/Console/stubs/block.stub index 8b29a788..a238b624 100644 --- a/src/Console/stubs/block.stub +++ b/src/Console/stubs/block.stub @@ -5,30 +5,50 @@ namespace DummyNamespace; use Log1x\AcfComposer\Block; use StoutLogic\AcfBuilder\FieldsBuilder; -class DummyClass extends Block { +class DummyClass extends Block +{ /** - * Default field type configuration. + * The name of the block. * - * @return array + * @var string + */ + protected $name = 'DummyClass'; + + /** + * The block description. + * + * @var string + */ + protected $description = 'Lorem ipsum...'; + + /** + * The block category. + * + * @var string + */ + protected $category = 'common'; + + /** + * The icon of this block. + * + * @var string|array */ - protected $defaults = []; + protected $icon = 'star-half'; /** - * Data to be passed to the block before registering. + * Data to be passed to the block before rendering. * * @return array */ - public function register() + public function with() { return [ - 'name' => 'DummyClass', - 'description' => 'Lorem ipsum', - 'category' => 'formatting', + 'items' => $this->items(), ]; } /** - * Fields to be attached to the block. + * The block field group. * * @return array */ @@ -45,19 +65,7 @@ class DummyClass extends Block { } /** - * Data to be passed to the rendered block. - * - * @return array - */ - public function with() - { - return [ - 'items' => $this->items(), - ]; - } - - /** - * Returns the items field. + * Return the items field. * * @return array */ diff --git a/src/Console/stubs/field.stub b/src/Console/stubs/field.stub index 317db6d9..13e4ec58 100644 --- a/src/Console/stubs/field.stub +++ b/src/Console/stubs/field.stub @@ -5,16 +5,10 @@ namespace DummyNamespace; use Log1x\AcfComposer\Field; use StoutLogic\AcfBuilder\FieldsBuilder; -class DummyClass extends Field { +class DummyClass extends Field +{ /** - * Default field type configuration. - * - * @return array - */ - protected $defaults = []; - - /** - * Fields to be registered with the application. + * The field group. * * @return array */ @@ -23,9 +17,9 @@ class DummyClass extends Field { $DummySlug = new FieldsBuilder('DummySlug'); $DummySlug - ->addTrueFalse('enabled') - ->addText('label') - ->addTextarea('description') + ->setLocation('post_type', '==', 'post'); + + $DummySlug ->addRepeater('items') ->addText('item') ->endRepeater(); diff --git a/src/Console/stubs/options.stub b/src/Console/stubs/options.stub new file mode 100644 index 00000000..efa11e7b --- /dev/null +++ b/src/Console/stubs/options.stub @@ -0,0 +1,33 @@ +addRepeater('items') + ->addText('item') + ->endRepeater(); + + return $DummySlug->build(); + } +} diff --git a/src/Console/stubs/views/block.stub b/src/Console/stubs/views/repeater.stub similarity index 100% rename from src/Console/stubs/views/block.stub rename to src/Console/stubs/views/repeater.stub diff --git a/src/Console/stubs/widget.stub b/src/Console/stubs/widget.stub new file mode 100644 index 00000000..6857b3c0 --- /dev/null +++ b/src/Console/stubs/widget.stub @@ -0,0 +1,74 @@ + $this->items(), + ]; + } + + /** + * The title of the widget. + * + * @return string + */ + public function title() { + return get_field('title', $this->widget->id); + } + + /** + * The widget field group. + * + * @return array + */ + public function fields() + { + $DummySlug = new FieldsBuilder('DummySlug'); + + $DummySlug + ->addText('title'); + + $DummySlug + ->addRepeater('items') + ->addText('item') + ->endRepeater(); + + return $DummySlug->build(); + } + + /** + * Return the items field. + * + * @return array + */ + public function items() + { + return get_field('items') ?: []; + } +} diff --git a/src/Field.php b/src/Field.php index 11cd9d0a..7f90d6c5 100644 --- a/src/Field.php +++ b/src/Field.php @@ -2,125 +2,88 @@ namespace Log1x\AcfComposer; -use Roots\Acorn\Application; use Illuminate\Support\Arr; use Illuminate\Support\Str; -use function Roots\app_path; - -abstract class Field +abstract class Field extends Composer { /** - * The application instance. - * - * @var \Roots\Acorn\Application - */ - protected $app; - - /** - * The field group. - * - * @var array - */ - protected $fields; - - /** - * Default field type settings. + * Create an options page for this field group. * - * @return array + * @param string|array|bool */ - protected $defaults = []; + protected $options = false; /** - * Create a new Field instance. + * Compose and register the defined field groups with ACF. * - * @param \Roots\Acorn\Application $app + * @param callback $callback * @return void */ - public function __construct(Application $app) + public function compose($callback = null) { - $this->app = $app; - } - - /** - * Compose the field. - * - * @return void - */ - public function compose() - { - if (! $this->fields() || ! function_exists('acf')) { - return; + if (empty($this->options)) { + return parent::compose(); } - $this->fields = $this->fields(); + if (is_array($this->options)) { + $this->options = collect($this->options); - $this->defaults = collect( - $this->app->config->get('acf.defaults') - )->merge($this->defaults)->mapWithKeys(function ($value, $key) { - return [Str::snake($key) => $value]; - }); + if (! $this->options->get('menu_title')) { + return parent::compose(); + } + } - if (! empty($this->fields)) { - add_action('init', function () { - if ($this->defaults->has('field_group')) { - $this->fields = array_merge($this->fields, $this->defaults->get('field_group')); - } + if (is_string($this->options)) { + $this->options = collect([ + 'menu_title' => Str::title(Str::slug($this->options, ' ')), + 'menu_slug' => Str::slug($this->options), + ]); + } - acf_add_local_field_group($this->build()); - }, 20); + if (! $this->options->get('menu_slug')) { + $this->options->put('menu_slug', Str::slug($this->options->get('menu_title'))); } - } - /** - * Build the field group with our default field type settings. - * - * @param array $fields - * @return array - */ - protected function build($fields = []) - { - return collect($fields ?: $this->fields)->map(function ($value, $key) use ($fields) { - if ( - ! Str::contains($key, ['fields', 'sub_fields', 'layouts']) || - (Str::is($key, 'type') && ! $this->defaults->has($value)) - ) { - return $value; + $this->options = array_merge([ + 'page_title' => get_bloginfo('name', 'display'), + 'capability' => 'edit_theme_options', + 'position' => PHP_INT_MAX, + 'autoload' => true + ], $this->options->all()); + + parent::compose(function () { + acf_add_options_page($this->options); + + if (! Arr::has($this->fields, 'location.0.0')) { + Arr::set($this->fields, 'location.0.0', [ + 'param' => 'options_page', + 'operator' => '==', + 'value' => $this->options['menu_slug'], + ]); } - - return array_map(function ($field) { - if (collect($field)->keys()->intersect(['fields', 'sub_fields', 'layouts'])->isNotEmpty()) { - return $this->build($field); - } - - return array_merge($this->defaults->get($field['type'], []), $field); - }, $value); - })->all(); + }); } /** - * Get field partial if it exists. + * A simple helper method for creating an options page. * * @param string $name - * @return mixed - */ - protected function get($name = '') - { - $name = strtr($name, [ - '.php' => '', - '.' => '/' - ]); - - return include app_path("Fields/{$name}.php"); - } - - /** - * Fields to be attached to the field. - * - * @return array + * @param array $options + * @return void */ - public function fields() + public function options($name, $options = []) { - return []; + acf_add_options_page( + collect([ + 'page_title' => get_bloginfo('name'), + 'menu_title' => Str::title($name), + 'menu_slug' => Str::slug($name), + 'update_button' => 'Update Options', + 'capability' => 'edit_theme_options', + 'position' => '999', + 'autoload' => true + ])->merge($options)->all() + ); } } diff --git a/src/Providers/AcfComposerServiceProvider.php b/src/Providers/AcfComposerServiceProvider.php index b8517f2a..aa804c31 100644 --- a/src/Providers/AcfComposerServiceProvider.php +++ b/src/Providers/AcfComposerServiceProvider.php @@ -7,24 +7,25 @@ class AcfComposerServiceProvider extends ServiceProvider { /** - * Register and compose fields. + * Register any application services. * * @return void */ public function register() { - collect($this->app->config->get('acf.blocks')) - ->each(function ($block) { - if (is_string($block)) { - $block = new $block($this->app); - } - - $block->compose(); - }); + if (! function_exists('acf')) { + return; + } collect($this->app->config->get('acf.fields')) + ->merge($this->app->config->get('acf.blocks')) + ->merge($this->app->config->get('acf.widgets')) ->each(function ($field) { if (is_string($field)) { + if (! class_exists($field)) { + return; + } + $field = new $field($this->app); } @@ -41,14 +42,13 @@ public function boot() { $this->publishes([ __DIR__ . '/../../config/acf.php' => $this->app->configPath('acf.php'), - __DIR__ . '/../../resources/views/view-404.blade.php' => $this->app->resourcePath( - 'views/blocks/view-404.blade.php' - ), ], 'acf-composer'); $this->commands([ - \Log1x\AcfComposer\Console\BlockMakeCommand::class, \Log1x\AcfComposer\Console\FieldMakeCommand::class, + \Log1x\AcfComposer\Console\BlockMakeCommand::class, + \Log1x\AcfComposer\Console\WidgetMakeCommand::class, + \Log1x\AcfComposer\Console\OptionsMakeCommand::class, ]); } } diff --git a/src/Widget.php b/src/Widget.php new file mode 100644 index 00000000..b490a4f7 --- /dev/null +++ b/src/Widget.php @@ -0,0 +1,157 @@ +name)) { + return; + } + + parent::compose(function () { + $this->widget = (object) collect( + Arr::get($GLOBALS, 'wp_registered_widgets') + )->filter(function ($value) { + return $value['name'] == $this->name; + })->pop(); + + $this->widget->id = Str::start($this->widget->id, 'widget_'); + $this->id = $this->widget->id; + + if (! Arr::has($this->fields, 'location.0.0')) { + Arr::set($this->fields, 'location.0.0', [ + 'param' => 'widget', + 'operator' => '==', + 'value' => $this->slug, + ]); + } + }); + + add_filter('widgets_init', function () { + register_widget($this->widget()); + }, 20); + } + + /** + * Returns an instance of WP_Widget used to register the widget. + * + * @return WP_Widget + */ + public function widget() + { + return (new class ($this) extends WP_Widget { + /** + * Create a new WP_Widget instance. + * + * @param \Log1x\AcfComposer\Widget $widget + * @return void + */ + public function __construct($widget) + { + $this->widget = $widget; + + parent::__construct( + $this->widget->slug, + $this->widget->name, + ['description' => $this->widget->description] + ); + } + + /** + * Render the widget for WordPress. + * + * @param array $args + * @param array $instance + * @return void + */ + public function widget($args, $instance) + { + echo Arr::get($args, 'before_widget'); + + if (! empty($this->widget->title())) { + echo collect([ + Arr::get($args, 'before_title'), + $this->widget->title(), + Arr::get($args, 'after_title') + ])->implode(''); + } + + echo $this->widget->view( + Str::finish('views.widgets.', $this->widget->slug), + ['widget' => $this->widget] + ); + + echo Arr::get($args, 'after_widget'); + } + + /** + * Output the widget settings update form. + * This is intentionally blank due to it being set by ACF. + * + * @param array $instance + * @return void + */ + public function form($instance) + { + // + } + }); + } +}