├── README.md ├── composer.json ├── helpers.php └── src ├── ACF.php ├── Block.php ├── BlockInterface.php ├── BlockType.php ├── BlockTypeInterface.php ├── BlockTypeRegistry.php ├── BlockTypeRegistryTrait.php ├── Blocks.php ├── ContentBuilder.php ├── FlexibleContentBlockType.php ├── Setting.php ├── SettingInterface.php └── Settings ├── BlockAdminLabel.php └── Id.php /README.md: -------------------------------------------------------------------------------- 1 | # ACF Blocks 2 | ACF Blocks is a lightweight library that provides a clean, object-oriented API to create and render ACF field groups. 3 | 4 | Your WordPress code base doesn't have to be a mess. 5 | 6 | **This library is in beta. Use at your own risk. Contributions are welcome.** 7 | 8 | ## Overview 9 | ACF Blocks introduces the concept of *blocks*, which are essentially Controllers (or actually ViewModels) for field groups and flexible layouts. Fields are created using the excellent [ACF Builder](https://github.com/StoutLogic/acf-builder) library. 10 | 11 | The three main benefits of using ACF blocks are: 12 | - Speed up development of simple sites, 13 | - Provide a super simple, but clean architecture for developing more complex sites, 14 | - Allow re-using blocks between projects. 15 | 16 | ## Installation 17 | ``` 18 | composer require codelight/acf-blocks dev-master 19 | ``` 20 | If you're still not using Composer in 2018, then do yourself a **huge** favor and get started now. [todo: article] 21 | 22 | ## Example 1: Quick procedural blocks 23 | As an example, let's go through creating and rendering a simple field group. 24 | ```php 25 | getFieldsBuilder() 42 | ->addImage('awesome_image', ['return_format' => 'id']) 43 | ->addWysiwyg('boring_text') 44 | ->setLocation('page_template', '==', 'template-image.php'); 45 | 46 | /** 47 | * Add a function for processing raw ACF data before it's sent to the template. 48 | * This allows you to do additional processing depending on the data and keep your templates clean. 49 | */ 50 | $imageBlock->addCallback(function($data) { 51 | 52 | // Return the full image html with srcset attribute generated by wordpress 53 | $data['image'] = wp_get_attachment_image($data['awesome_image'], 'large'); 54 | 55 | // Split the wysiwyg contents into an excerpt and the full text 56 | $data['excerpt'] = wp_trim_words($data['boring_text'], 25); 57 | $data['text'] = $data['boring_text']; 58 | 59 | return $data; 60 | }); 61 | 62 | // Set the template for this block 63 | $imageBlock->setTemplate('templates/blocks/image.php'); 64 | 65 | // Register the block with the main block manager class 66 | $blocks = Blocks::getInstance(); 67 | $blocks->registerBlock($imageBlock); 68 | } 69 | ``` 70 | ```php 71 | get()` inside the Loop will return all pre-rendered blocks 78 | * assigned to that specific page or post. 79 | */ 80 | ?> 81 | 82 | get() as $block): ?> 83 | 84 | 85 | 86 | ``` 87 | ```php 88 | 95 |
96 | 97 |
98 |
99 | 100 |
101 |
102 | 103 |
104 |
105 |
106 | ``` 107 | 108 | ## Example 2: Encapsulate the block in a class 109 | Let's create the same block in a much cleaner way - as a class. This class should be in a separate file called ImageBlock.php. You'll probably want to keep it in a separate folder, which you might want to call 'blocks'. 110 | ```php 111 | 'example_image'; 119 | // The location of the template 120 | 'template' => 'templates/blocks/image.php', 121 | ]; 122 | 123 | // This function is called when the block type is initialized for the first time. 124 | // You'll use it mostly to register the fields 125 | public function init() 126 | { 127 | $this->getFieldsBuilder() 128 | ->addImage('awesome_image', ['return_format' => 'id']) 129 | ->addWysiwyg('boring_text') 130 | ->setLocation('page_template', '==', 'template-image.php'); 131 | } 132 | 133 | // This function works in a similar way to addCallback() - it allows you to 134 | // modify the data that's passed into the template 135 | public function filterData($data) 136 | { 137 | // Return the full image html with srcset attribute generated by wordpress 138 | $data['image'] = wp_get_attachment_image($data['awesome_image'], 'large'); 139 | 140 | // Split the wysiwyg contents into an excerpt and the full text 141 | $data['excerpt'] = wp_trim_words($data['boring_text'], 25); 142 | $data['text'] = $data['boring_text']; 143 | 144 | return $data; 145 | } 146 | } 147 | ``` 148 | 149 | We'll also need to register the block we just created. This goes into your functions.php (or equivalent): 150 | ```php 151 | init([ 157 | 'blocktypes' => [ 158 | // array of block class names as strings 159 | 'ImageBlock', 160 | ] 161 | ]); 162 | }); 163 | ``` 164 | 165 | And that's it. You'll also need to add the templates as in the previous example. 166 | 167 | ## Example 3: Setting up flexible layouts 168 | Let's continue the previous example, but register the ImageBlock as a Flexible Content layout. 169 | First, we'll need to create the Flexible Content block which will contain our ImageBlock. 170 | ```php 171 | 'flexible_block', 179 | // The location of the template 180 | 'template' => 'blocks.content-builder', 181 | ]; 182 | 183 | public function init() 184 | { 185 | $this->getFieldsBuilder() 186 | ->setGroupConfig('title', 'Content Blocks') 187 | ->setLocation('post_type', '==', 'page'); 188 | 189 | // This registers our ImageBlock as a child block of this flexible content layout 190 | $this->registerBlockType('ImageBlock'); 191 | } 192 | } 193 | ``` 194 | 195 | This flexible content block works exactly as any other block. To register it, modify the code you previously added to your functions.php (or equivalent) as follows: 196 | ```php 197 | init([ 204 | 'blocktypes' => [ 205 | // array of block class names as strings 206 | 'ImageBlock', 207 | 'FlexibleBlock', 208 | ] 209 | ]); 210 | }); 211 | ``` 212 | 213 | Now, you will have a flexible content area on every Page where you can add the ImageBlock. Note that the ImageBlock will still be added to template-image.php as a regular (non-flexible-layout) block as well. The ImageBlock will use the same template in both cases. This provides an easy way to re-use blocks between templates, flexible content areas and even projects. It's also possible to use a different template in different situations (e.g. flexible layout vs regular page context), whilst keeping the backend code of the block the same. 214 | 215 | ## Example 4: So why is this useful? 216 | One obvious answer is that once you get the general idea, it's about 10x faster compared to writing all the annoying template code by hand. However, the actual main advantage of using ACF Blocks is that it makes you architect things in one specific, clean, flexible and modular way. You now have a really good way to separate your templates and functionality. You can re-use and extend ACF field groups and blocks. You always know where to find your code. This approach speeds up the development of smaller projects but it really shines in the context of massive sites where you have lots of different fields and field groups. 217 | 218 | todo: add more complex examples. 219 | 220 | ## FAQ 221 | **Will using this library have an impact on performance?** 222 | No, it's just a really thin layer of abstraction. It doesn't do much, it just allows you to write better code. 223 | 224 | **I'm using [soberwp/controller](https://github.com/soberwp/controller) which already provides me with a Controller. Why should I use this library?** 225 | The concept behind soberwp/controller is great, but it doesn't have much use in the context of flexible layouts. 226 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codelight/acf-blocks", 3 | "type": "package", 4 | "require": { 5 | "php": ">=7.0.0", 6 | "stoutlogic/acf-builder": "*" 7 | }, 8 | "autoload": { 9 | "psr-4": { 10 | "Codelight\\ACFBlocks\\": "src/" 11 | }, 12 | "files": [ 13 | "helpers.php" 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /helpers.php: -------------------------------------------------------------------------------- 1 | getFieldsBuilder(); 63 | 64 | if ($fieldsBuilder) { 65 | $fields = $fieldsBuilder->build(); 66 | if ($fields) { 67 | acf_add_local_field_group($fields); 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Get the block type names which are registered to an object with the given ID 74 | * 75 | * @param $postId 76 | * @return array 77 | */ 78 | public function getPostFieldGroupNames($postId) 79 | { 80 | if ('option' === $postId) { 81 | return $this->getGlobalFieldGroupKeys(); 82 | } 83 | 84 | $fieldGroupKeys = []; 85 | $fieldGroups = acf_get_field_groups(['post_id' => $postId]); 86 | 87 | foreach ($fieldGroups as $fieldGroup) { 88 | // Remove ACF-s internal prefix 89 | $fieldGroupKeys[] = substr($fieldGroup['key'], 6); 90 | } 91 | 92 | return $fieldGroupKeys; 93 | } 94 | 95 | /** 96 | * Get names of all global field groups 97 | * 98 | * @return array 99 | */ 100 | public function getGlobalFieldGroupKeys() 101 | { 102 | $blockTypeNames = []; 103 | $fieldGroups = acf_get_field_groups(); 104 | 105 | // Parse all Options Page field groups 106 | foreach ($fieldGroups as $fieldGroup) { 107 | if (isset($fieldGroup['location']) && count($fieldGroup['location'])) { 108 | foreach ($fieldGroup['location'] as $location) { 109 | foreach ($location as $rule) { 110 | if ('options_page' === $rule['param'] && in_array($rule['operator'], ['=', '=='])) { 111 | // Remove ACF-s internal prefix 112 | $blockTypeNames[] = substr($fieldGroup['key'], 6); 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | return $blockTypeNames; 120 | } 121 | 122 | /** 123 | * Get all the ACF data of an object with the given ID 124 | * 125 | * @param $postId 126 | * @param FieldsBuilder $fieldsBuilder 127 | * @return array 128 | */ 129 | public function getPostBlockData($postId, FieldsBuilder $fieldsBuilder) 130 | { 131 | // acf_get_field_groups() doesn't return the field names, so we'll need to build the 132 | // config again to actually get them 133 | $fieldGroup = $fieldsBuilder->build(); 134 | 135 | $data = []; 136 | 137 | foreach ($fieldGroup['fields'] as $field) { 138 | $data[$field['name']] = get_field($field['key'], $postId); 139 | } 140 | 141 | return $data; 142 | } 143 | 144 | public function getPostBlockSettings($postId, FieldsBuilder $fieldsBuilder, $groupName) 145 | { 146 | // acf_get_field_groups() doesn't return the field names, so we'll need to build the 147 | // config again to actually get them 148 | $fieldGroup = $fieldsBuilder->build(); 149 | 150 | $settings = []; 151 | 152 | foreach ($fieldGroup['fields'] as $field) { 153 | if ($field['name'] === $groupName) { 154 | $settings = get_field($field['key'], $postId); 155 | } 156 | } 157 | 158 | return $settings; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Block.php: -------------------------------------------------------------------------------- 1 | blockType = $blockType; 45 | } 46 | 47 | public function getId() 48 | { 49 | return $this->id; 50 | } 51 | 52 | public function setId($id) 53 | { 54 | $this->id = $id; 55 | } 56 | 57 | public function getObjectId() 58 | { 59 | return $this->objectId; 60 | } 61 | 62 | public function setObjectId($id) 63 | { 64 | $this->objectId = $id; 65 | } 66 | 67 | /** 68 | * Return the block data 69 | * 70 | * @return array 71 | */ 72 | public function getData() 73 | { 74 | return $this->data; 75 | } 76 | 77 | public function getSettings() 78 | { 79 | return $this->settings; 80 | } 81 | 82 | public function setSettings(array $data, array $settings) 83 | { 84 | // If any Settings have been registered, run the data through them 85 | if (count($this->blockType->getSettings())) { 86 | foreach ($this->blockType->getSettings() as $setting) { 87 | /* @var SettingInterface $setting */ 88 | if (method_exists($setting, 'filterData')) { 89 | $settings = $setting->filterData($data, $settings, $this->id, $this->objectId, $this->blocks); 90 | } 91 | } 92 | } 93 | 94 | $this->settings = $settings; 95 | } 96 | 97 | /** 98 | * Set the block data, passing it through any callbacks 99 | * 100 | * @param $data 101 | */ 102 | public function setData(array $data, array $settings) 103 | { 104 | // Add ID to the block's data 105 | // DEPRECATED: remove in v2 106 | if (!isset($data['block_id'])) { 107 | $data['block_id'] = $this->id; 108 | } 109 | 110 | // Pass data through registered callbacks 111 | // This allows comfortably overriding data if the block type is defined procedurally 112 | if (is_array($this->blockType->getCallbacks()) && count($this->blockType->getCallbacks())) { 113 | foreach ($this->blockType->getCallbacks() as $callback) { 114 | if (is_callable($callback)) { 115 | $data = call_user_func($callback, $data, $settings, $this->id, $this->objectId, $this->blocks); 116 | } else { 117 | trigger_error("A callback registered to {$this->getBlockTypeName()} is not callable.", E_USER_WARNING); 118 | } 119 | } 120 | } 121 | 122 | // If a data filtering function is defined, pass data through it 123 | // This allows comfortably overriding data if the block type is defined as a child class 124 | if (method_exists($this->blockType, 'filterData')) { 125 | $data = $this->blockType->filterData($data, $settings, $this->id, $this->objectId, $this->blocks); 126 | } 127 | 128 | $this->data = $data; 129 | } 130 | 131 | /** 132 | * @return string 133 | */ 134 | public function getTemplate() 135 | { 136 | return $this->blockType->getTemplate(); 137 | } 138 | 139 | /** 140 | * @return string 141 | */ 142 | public function getBlockTypeName() 143 | { 144 | return $this->blockType->getName(); 145 | } 146 | 147 | /** 148 | * @return BlockTypeInterface 149 | */ 150 | public function getBlockType() 151 | { 152 | return $this->blockType; 153 | } 154 | 155 | /** 156 | * @param $blocks 157 | */ 158 | public function setBlocks(array $blocks) 159 | { 160 | $this->blocks = $blocks; 161 | } 162 | 163 | /** 164 | * @return array 165 | */ 166 | public function getRawData() 167 | { 168 | return $this->rawData; 169 | } 170 | 171 | /** 172 | * @param array $data 173 | */ 174 | public function setRawData(array $data) 175 | { 176 | $this->rawData = $data; 177 | } 178 | 179 | /** 180 | * @return array 181 | */ 182 | public function getRawSettings() 183 | { 184 | return $this->rawSettings; 185 | } 186 | 187 | /** 188 | * @param array $settings 189 | */ 190 | public function setRawSettings(array $settings) 191 | { 192 | $this->rawSettings = $settings; 193 | } 194 | 195 | /** 196 | * @param $key 197 | * @return mixed|null 198 | */ 199 | public function getMeta($key) 200 | { 201 | return isset($this->meta[$key]) ? $this->meta[$key] : null; 202 | } 203 | 204 | /** 205 | * @param $key 206 | * @param $value 207 | */ 208 | public function setMeta($key, $value) 209 | { 210 | $this->meta[$key] = $value; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/BlockInterface.php: -------------------------------------------------------------------------------- 1 | config = $this->config + $config; 48 | 49 | if ( ! $this->name) { 50 | $this->name = $this->config['name']; 51 | } 52 | 53 | $this->fieldsBuilder = new FieldsBuilder(isset($this->config['field_group_name'])?$this->config['field_group_name']:$this->name); 54 | 55 | $this->setup(); 56 | } 57 | 58 | /** 59 | * Run setup functions 60 | */ 61 | protected function setup() 62 | { 63 | // Pseudo-constructor for child classes 64 | if (method_exists($this, 'init')) { 65 | $this->init(); 66 | } 67 | 68 | // Configure ACF fields 69 | if (method_exists($this, 'configureFields')) { 70 | $this->configureFields(); 71 | } 72 | 73 | // If any Settings have been defined, run them as well 74 | if (!count($this->settings)) { 75 | return; 76 | } 77 | 78 | $builder = $this->getFieldsBuilder(); 79 | $builder = $builder->addGroup('settings', ['label' => __('Block Settings', 'acf-blocks'), 'wrapper' => ['width' => 100]]); 80 | foreach ($this->settings as $setting) { 81 | /* @var SettingInterface $setting */ 82 | $builder = $builder->addFields($setting->getFieldsBuilder()); 83 | } 84 | $builder = $builder->endGroup(); 85 | } 86 | 87 | /** 88 | * Initialize a new Block object 89 | * 90 | * @return BlockInterface 91 | */ 92 | public function createBlock() 93 | { 94 | if (method_exists($this, 'build')) { 95 | $this->build(); 96 | } 97 | 98 | return new $this->blockClass($this); 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | public function getName() 105 | { 106 | return $this->name; 107 | } 108 | 109 | /** 110 | * @return mixed 111 | */ 112 | public function getTitle() 113 | { 114 | return $this->getFieldsBuilder()->getGroupConfig('title') ? $this->getFieldsBuilder()->getGroupConfig('title') : $this->getName(); 115 | } 116 | 117 | /** 118 | * @return FieldsBuilder 119 | */ 120 | public function getFieldsBuilder() 121 | { 122 | return $this->fieldsBuilder; 123 | } 124 | 125 | /** 126 | * @return string 127 | */ 128 | public function getTemplate() 129 | { 130 | if ( ! isset($this->config['template']) || empty($this->config['template'])) { 131 | trigger_error("Template not configured for block type {$this->getName()}", E_USER_ERROR); 132 | } 133 | 134 | return $this->config['template']; 135 | } 136 | 137 | /** 138 | * @param $template 139 | */ 140 | public function setTemplate($template) 141 | { 142 | $this->config['template'] = $template; 143 | } 144 | 145 | /** 146 | * @return array 147 | */ 148 | public function getCallbacks() 149 | { 150 | return $this->callbacks; 151 | } 152 | 153 | /** 154 | * @param callable $callback 155 | */ 156 | public function addCallback(callable $callback) 157 | { 158 | $this->callbacks[] = $callback; 159 | } 160 | 161 | /** 162 | * @param callable $callback 163 | */ 164 | public function removeCallback(callable $callback) 165 | { 166 | $index = array_search($callback, $this->callbacks); 167 | 168 | if ($index !== false) { 169 | unset($this->callbacks[$index]); 170 | } else { 171 | trigger_error("Attempting to remove a callback that doesn't exist.", E_USER_WARNING); 172 | } 173 | } 174 | 175 | /** 176 | * @param SettingsInterface $setting 177 | */ 178 | public function registerSetting(SettingInterface $setting) 179 | { 180 | $this->settings[] = $setting; 181 | } 182 | 183 | /** 184 | * @return array 185 | */ 186 | public function getSettings() 187 | { 188 | return $this->settings; 189 | } 190 | 191 | /** 192 | * @param $id 193 | * @param $blocks 194 | * @return Block|null 195 | */ 196 | public function getPreviousBlock($id, $blocks) 197 | { 198 | return $this->getBlockByRelativePosition(-1, $id, $blocks); 199 | } 200 | 201 | /** 202 | * @param $id 203 | * @param $blocks 204 | * @return Block|null 205 | */ 206 | public function getCurrentBlock($id, $blocks) 207 | { 208 | return $this->getBlockByRelativePosition(0, $id, $blocks); 209 | } 210 | 211 | /** 212 | * @param $id 213 | * @param $blocks 214 | * @return Block|null 215 | */ 216 | public function getNextBlock($id, $blocks) 217 | { 218 | return $this->getBlockByRelativePosition(1, $id, $blocks); 219 | } 220 | 221 | /** 222 | * @param $id 223 | * @param $blocks 224 | * @return Block|null 225 | */ 226 | public function getBlockByRelativePosition($position, $id, $blocks) 227 | { 228 | $blockOrder = array_keys($blocks); 229 | $blockNames = array_flip($blockOrder); 230 | 231 | if (!isset($blockNames[$id])) { 232 | return false; 233 | } 234 | 235 | $currentBlockIndex = $blockNames[$id]; 236 | $relativeIndex = $currentBlockIndex + $position; 237 | 238 | if (!isset($blockOrder[$relativeIndex])) { 239 | return false; 240 | } 241 | 242 | $relativeBlockName = $blockOrder[$relativeIndex]; 243 | 244 | return isset($blocks[$relativeBlockName]) ? $blocks[$relativeBlockName] : null; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/BlockTypeInterface.php: -------------------------------------------------------------------------------- 1 | blockTypes; 26 | } 27 | 28 | /** 29 | * Get a block by its unique name 30 | * 31 | * @param $fieldGroupName 32 | * 33 | * @return BlockTypeInterface[] 34 | */ 35 | public function getBlockTypesByFieldGroupName($fieldGroupName) 36 | { 37 | $blockTypes = []; 38 | /* @var BlockTypeInterface $blockType */ 39 | foreach ($this->blockTypes as $blockType){ 40 | if ($blockType->getFieldsBuilder()->getName() == $fieldGroupName){ 41 | $blockTypes[$blockType->getName()]=$blockType; 42 | } 43 | } 44 | 45 | return $blockTypes; 46 | } 47 | 48 | /** 49 | * Register an array of blocks 50 | * 51 | * @param array $blocks 52 | */ 53 | public function registerBlockTypes(array $blockTypes) 54 | { 55 | if (count($blockTypes)) { 56 | foreach ($blockTypes as $blockType) { 57 | $this->registerBlockType($blockType); 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Register a single block 64 | * 65 | * @param $block 66 | * @param string $name 67 | */ 68 | public function registerBlockType($blockType) 69 | { 70 | if ($blockType instanceof BlockTypeInterface) { 71 | if (isset($this->blockTypes[$blockType->getName()])) { 72 | trigger_error("A block with the name {$blockType->getName()} already exists!", E_USER_ERROR); 73 | } 74 | $this->blockTypes[$blockType->getName()] = $blockType; 75 | } else { 76 | trigger_error("Block does not implement BlockTypeInterface and will be ignored.", E_USER_WARNING); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/BlockTypeRegistryTrait.php: -------------------------------------------------------------------------------- 1 | blockTypeRegistry->registerBlockTypes($blocks); 21 | } 22 | 23 | /** 24 | * @param $block 25 | */ 26 | public function registerBlockType($block) 27 | { 28 | $this->blockTypeRegistry->registerBlockType($block); 29 | } 30 | 31 | /** 32 | * @return array 33 | */ 34 | public function getBlockTypes() 35 | { 36 | return $this->blockTypeRegistry->getBlockTypes(); 37 | } 38 | 39 | /** 40 | * @param $name 41 | * @return BlockTypeInterface 42 | */ 43 | public function getBlockType($name) 44 | { 45 | return $this->blockTypeRegistry->getBlockTypes()[$name]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Blocks.php: -------------------------------------------------------------------------------- 1 | acf = new ACF(); 45 | $this->blockTypeRegistry = new BlockTypeRegistry(); 46 | } 47 | 48 | /** 49 | * Initialize the class by creating the block type objects and registering them 50 | * 51 | * @param array $config 52 | */ 53 | public function init($config = []) 54 | { 55 | // Parse block types from given config 56 | $blockTypes = $this->parseConfig($config); 57 | 58 | // Register blocks from config, if applicable 59 | $this->registerBlockTypes($blockTypes); 60 | } 61 | 62 | /** 63 | * Parse the config array, automatically add namespaces if applicable 64 | * 65 | * @param $config 66 | * @return mixed 67 | */ 68 | protected function parseConfig($config) 69 | { 70 | // If the key 'blocktypes' is not set, we assume that $config is just a simple array of block classes or objects 71 | if (!array_key_exists('blocktypes', $config)) { 72 | return $config; 73 | } 74 | 75 | // If both 'blocktypes' and 'namespace' keys are set, assume we have an array of non-namespaced classes 76 | // so let's add the namespace automatically 77 | if (array_key_exists('namespace', $config) && array_key_exists('blocktypes', $config)) { 78 | 79 | // Add trailing slash if it's missing 80 | $namespace = rtrim($config['namespace'], '\\') . '\\'; 81 | 82 | foreach ($config['blocktypes'] as &$blockType) { 83 | if (is_string($blockType)) { 84 | $blockType = $namespace . $blockType; 85 | } 86 | } 87 | } 88 | 89 | return $config['blocktypes']; 90 | } 91 | 92 | /** 93 | * Register an array of block types 94 | * 95 | * @param array $blockTypes 96 | */ 97 | public function registerBlockTypes(array $blockTypes) 98 | { 99 | if (count($blockTypes)) { 100 | foreach ($blockTypes as $blockType) { 101 | $this->registerBlockType($blockType); 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Register a single block type 108 | * 109 | * @param BlockTypeInterface $blockType 110 | */ 111 | public function registerBlockType($blockType) 112 | { 113 | if (is_string($blockType)) { 114 | // Todo: add support for dependency injection 115 | $blockType = new $blockType(); 116 | } 117 | 118 | // Register the block type in the main registry 119 | $this->blockTypeRegistry->registerBlockType($blockType); 120 | 121 | // Register the block type's ACF fields 122 | $this->acf->registerBlockTypeFields($blockType); 123 | } 124 | 125 | /** 126 | * Get and populate blocks by post id 127 | * 128 | * @param $postId 129 | * @return array 130 | */ 131 | public function getBlocksByPostId($postId) 132 | { 133 | $blocks = []; 134 | $FieldGroupNames = $this->acf->getPostFieldGroupNames($postId); 135 | 136 | // Set up blocks 137 | foreach ($FieldGroupNames as $FieldGroupName) { 138 | 139 | $blockTypes = $this->blockTypeRegistry->getBlockTypesByFieldGroupName($FieldGroupName); 140 | 141 | // Disregard field groups that are not created using ACF Blocks 142 | if (!$blockTypes) { 143 | continue; 144 | } 145 | 146 | foreach ($blockTypes as $blockTypeName => $blockType){ 147 | $block = $blockType->createBlock(); 148 | 149 | $block->setId($blockTypeName); 150 | $block->setObjectId($postId); 151 | 152 | $data = $this->acf->getPostBlockData($postId, $blockType->getFieldsBuilder()); 153 | $settings = $this->acf->getPostBlockSettings($postId, $blockType->getFieldsBuilder(), 'settings'); 154 | 155 | $block->setRawData($data); 156 | $block->setRawSettings($settings); 157 | 158 | $blocks[$blockTypeName] = $block; 159 | } 160 | } 161 | 162 | // Set blocks in init config order by default 163 | $sortedBlocks = []; 164 | foreach ($this->blockTypeRegistry->getBlockTypes() as $registeredBlockType){ 165 | if (isset($blocks[$registeredBlockType->getName()])){ 166 | $sortedBlocks[$registeredBlockType->getName()] = $blocks[$registeredBlockType->getName()]; 167 | } 168 | } 169 | 170 | // On second loop, once each blocks has access to all the blocks' data in the current context. 171 | // This allows making decisions based on which specific block comes before or after the current. 172 | foreach ($sortedBlocks as $blockTypeName => $block) { 173 | /* @var Block $block */ 174 | $block->setBlocks($sortedBlocks); 175 | $block->setSettings($block->getRawData(), $block->getRawSettings()); 176 | $block->setData($block->getRawData(), $block->getRawSettings()); 177 | } 178 | 179 | return $sortedBlocks; 180 | } 181 | 182 | /** 183 | * Get an instantiated block type object 184 | * 185 | * @param $blockTypeName 186 | * @return BlockTypeInterface 187 | */ 188 | public function getBlockType($blockTypeName) 189 | { 190 | return $this->blockTypeRegistry->getBlockTypes()[$blockTypeName]; 191 | } 192 | 193 | /** 194 | * Fetch all rendered blocks associated with a given Post 195 | * 196 | * @param $postId 197 | */ 198 | public function get($postId = null) 199 | { 200 | $builder = $this->getBuilder($postId); 201 | $blocks = $builder->getRenderedBlocks(); 202 | 203 | // Set blocks in init config order by default 204 | $sortedBlocks = []; 205 | foreach ($this->blockTypeRegistry->getBlockTypes() as $registeredBlockType){ 206 | if (isset($blocks[$registeredBlockType->getName()])){ 207 | $sortedBlocks[$registeredBlockType->getName()] = $blocks[$registeredBlockType->getName()]; 208 | } 209 | } 210 | 211 | return $sortedBlocks; 212 | } 213 | 214 | /** 215 | * Fetch all block objects associated with a given Post without rendering them 216 | * 217 | * @param null $postId 218 | * @return array 219 | */ 220 | public function getBlockObjects($postId = null) 221 | { 222 | $postId = $this->resolvePostId($postId); 223 | $builder = $this->getBuilder($postId); 224 | return $builder->getBlocks(); 225 | } 226 | 227 | /** 228 | * Fetch all rendered global blocks 229 | * 230 | * @return array 231 | */ 232 | public function getGlobal() 233 | { 234 | $builder = $this->getBuilder('option'); 235 | return $builder->getRenderedBlocks(); 236 | } 237 | 238 | /** 239 | * Fetch all global block objects without rendering them 240 | * 241 | * @param null $postId 242 | * @return array 243 | */ 244 | public function getGlobalBlockObjects() 245 | { 246 | $builder = $this->getBuilder('option'); 247 | return $builder->getBlocks(); 248 | } 249 | 250 | /** 251 | * Get a block by name 252 | * 253 | * @param $name 254 | * @param null $postId 255 | * 256 | * @return null|BlockInterface 257 | */ 258 | public function getByName($name, $postId = null) 259 | { 260 | $postId = $this->resolvePostId($postId); 261 | $builder = $this->getBuilder($postId); 262 | return $builder->getBlock($name); 263 | } 264 | 265 | /** 266 | * Get a rendered block by name 267 | * 268 | * @param $name 269 | * @param null $postId 270 | * 271 | * @return array 272 | */ 273 | public function getRenderedBlockByName($name, $postId = null) 274 | { 275 | $postId = $this->resolvePostId($postId); 276 | $builder = $this->getBuilder($postId); 277 | return $builder->getRenderedBlock($name); 278 | } 279 | 280 | /** 281 | * Get populated content builder 282 | * 283 | * @param $postId 284 | * @return ContentBuilder 285 | */ 286 | public function getBuilder($postId = null) 287 | { 288 | $postId = $this->resolvePostId($postId); 289 | 290 | if (!isset($this->builders[$postId])) { 291 | $this->builders[$postId] = $this->createContentBuilder($postId); 292 | } 293 | 294 | return $this->builders[$postId]; 295 | } 296 | 297 | /** 298 | * @param $postId 299 | * 300 | * @return int 301 | */ 302 | protected function resolvePostId($postId) 303 | { 304 | $postId = apply_filters('codelight/acf_blocks/post_id', $postId); 305 | 306 | if ($postId) { 307 | return $postId; 308 | } 309 | 310 | if (is_home()) { 311 | return get_option('page_for_posts'); 312 | } 313 | 314 | global $post; 315 | 316 | if ($post) { 317 | return $post->ID; 318 | } 319 | 320 | return null; 321 | } 322 | 323 | /** 324 | * Factory method to create a new ContentBuilder 325 | * 326 | * @param $postId 327 | * @param $blocks 328 | * 329 | * @return ContentBuilder 330 | */ 331 | protected function createContentBuilder($postId) 332 | { 333 | $blocks = $this->getBlocksByPostId($postId); 334 | return new ContentBuilder($postId, $blocks); 335 | } 336 | 337 | /** 338 | * @return Blocks 339 | */ 340 | public static function getInstance() 341 | { 342 | if (!isset(static::$instance)) { 343 | static::$instance = new static(); 344 | } 345 | 346 | return static::$instance; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/ContentBuilder.php: -------------------------------------------------------------------------------- 1 | postId = $postId; 34 | $this->blocks = $blocks; 35 | } 36 | 37 | /** 38 | * Get prepared block by name from the current builder 39 | * 40 | * @param $name 41 | * @return BlockInterface 42 | */ 43 | public function getBlock($name) 44 | { 45 | if (isset($this->blocks[$name])) { 46 | return $this->blocks[$name]; 47 | } 48 | 49 | return null; 50 | } 51 | 52 | /** 53 | * Get prepared blocks 54 | * 55 | * @return array 56 | */ 57 | public function getBlocks() 58 | { 59 | return $this->blocks; 60 | } 61 | 62 | /** 63 | * Get a prepared and rendered block block by name 64 | * 65 | * @param $name 66 | * 67 | * @return mixed|null 68 | */ 69 | public function getRenderedBlock($name) 70 | { 71 | if (empty($this->renderedBlocks)) { 72 | $this->renderBlocks(); 73 | } 74 | 75 | if (isset($this->renderedBlocks[$name])) { 76 | return $this->renderedBlocks[$name]; 77 | } 78 | 79 | return null; 80 | } 81 | 82 | /** 83 | * Get all prepared and rendered blocks 84 | * 85 | * @return array 86 | */ 87 | public function getRenderedBlocks() 88 | { 89 | if (empty($this->renderedBlocks)) { 90 | $this->renderBlocks(); 91 | } 92 | 93 | return $this->renderedBlocks; 94 | } 95 | 96 | /** 97 | * Get array of rendered blocks 98 | * 99 | * @return array 100 | */ 101 | public function renderBlocks() 102 | { 103 | if (is_array($this->blocks) && count($this->blocks)) { 104 | foreach ($this->blocks as $name => $block) { 105 | $this->renderedBlocks[$name] = $this->renderBlock($name); 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * Render one of the current builder's blocks by name 112 | * 113 | * @param $name 114 | * @param null $template 115 | * @return string 116 | */ 117 | public function renderBlock($name, $template = null) 118 | { 119 | $block = $this->getBlock($name); 120 | 121 | if (!$block) { 122 | return false; 123 | } 124 | 125 | if (!$template) { 126 | $template = $block->getTemplate(); 127 | } 128 | 129 | // Allow overriding the render method in BlockType class 130 | if (method_exists($block->getBlockType(), 'render')) { 131 | return $block->getBlockType()->render($block); 132 | } 133 | 134 | return \App\template( 135 | $template, 136 | [ 137 | 'id' => $name, 138 | 'data' => $block->getData(), 139 | 'settings' => $block->getSettings(), 140 | 'block' => $block, 141 | ] 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/FlexibleContentBlockType.php: -------------------------------------------------------------------------------- 1 | blockTypeRegistry = new BlockTypeRegistry(); 30 | $this->setupFlexibleContent(); 31 | $this->addCallback([$this, 'renderRegisteredBlocks']); 32 | 33 | parent::setup(); 34 | } 35 | 36 | /** 37 | * Set up the main ACF flexible content field 38 | */ 39 | protected function setupFlexibleContent() 40 | { 41 | $this->getFieldsBuilder() 42 | ->addFlexibleContent($this->getName(), ['button_label' => 'Add Block',]) 43 | ->endFlexibleContent(); 44 | } 45 | 46 | /** 47 | * Register a block type as ACF Flexible Content layout 48 | * 49 | * @param mixed $blockType 50 | */ 51 | public function registerBlockType($blockType) 52 | { 53 | if (is_string($blockType)) { 54 | $blockType = new $blockType(); 55 | } 56 | 57 | $this->getFieldsBuilder() 58 | ->getField($this->getName()) 59 | ->addLayout($blockType->getName(), ['title' => $blockType->getTitle()]) 60 | ->addFields($blockType->getFieldsBuilder()); 61 | 62 | $this->blockTypeRegistry->registerBlockType($blockType); 63 | } 64 | 65 | /** 66 | * Get the contained Block objects from this Flexible Content block 67 | * 68 | * @param $data 69 | * @return array|string 70 | */ 71 | protected function getBlocks($data, $objectId = null) 72 | { 73 | /** 74 | * Fetch the data of the main flexible content field. 75 | * It should be an array of flexible layouts structured like this: 76 | * 77 | * [ 78 | * 'acf_fc_layout' => 'layout_name', 79 | * 'content_field_name_1' => 'some value', 80 | * 'content_field_name_2' => 'some other value', 81 | * // etc 82 | * ] 83 | */ 84 | $flexibleContentData = $data[$this->getName()]; 85 | 86 | if (empty($flexibleContentData)) { 87 | return ''; 88 | } 89 | 90 | $blocks = []; 91 | 92 | // For every item in the layout.. 93 | foreach ($flexibleContentData as $layout) { 94 | // Get the block type object 95 | $blockType = $this->getBlockType($layout['acf_fc_layout']); 96 | 97 | // Check if the block type exists, i.e. that this is a valid FC Layout 98 | // (Old, not cleaned up layouts might still exist in the database) 99 | if (!$blockType) { 100 | // todo: debug mode 101 | if (false) { 102 | trigger_error( 103 | "Skipping flexible content layout {$layout['acf_fc_layout']} which does not have a valid BlockType.", 104 | E_USER_NOTICE 105 | ); 106 | } 107 | continue; 108 | } 109 | 110 | // Create the block 111 | $block = $blockType->createBlock(); 112 | 113 | // Generate an ID 114 | $id = $this->findUniqueIndex($blockType->getName(), $blocks); 115 | 116 | $settings = isset($layout['settings']) && is_array($layout['settings']) ? $layout['settings'] : []; 117 | 118 | // Set the ID 119 | $block->setId($id); 120 | 121 | // Also set the current object (page?) ID 122 | $block->setObjectId($objectId); 123 | 124 | // Set unprocessed data 125 | $block->setRawData($layout); 126 | 127 | // Set unprocessed settings 128 | $block->setRawSettings($settings); 129 | 130 | // Add it to the list of blocks 131 | $blocks[$id] = $block; 132 | 133 | // Store the layout for later use 134 | $layouts[$id] = $layout; 135 | } 136 | 137 | // On second loop, once each blocks has access to all the blocks' data in the current context. 138 | // This allows making decisions based on which specific block comes before or after the current. 139 | foreach ($blocks as $id => $block) { 140 | $block->setBlocks($blocks); 141 | $block->setSettings($block->getRawData(), $block->getRawSettings()); 142 | $block->setData($block->getRawData(), $block->getRawSettings()); 143 | } 144 | 145 | return $blocks; 146 | } 147 | 148 | /** 149 | * Get the registered blocks 150 | * 151 | * @param $data 152 | * @return array|string 153 | */ 154 | public function getRegisteredBlockObjects($data) 155 | { 156 | $data['blocks'] = $this->getBlocks($data); 157 | 158 | return $data; 159 | } 160 | 161 | /** 162 | * Render the registered blocks 163 | * 164 | * @param $data 165 | * @return array|string 166 | */ 167 | public function renderRegisteredBlocks($data, $settings, $id, $objectId) 168 | { 169 | $blocks = $this->getBlocks($data, $objectId); 170 | // Create a new Builder, inject the blocks 171 | $builder = new ContentBuilder($this->getName(), $blocks); 172 | // And let it render the blocks 173 | $data['blocks'] = $builder->getRenderedBlocks(); 174 | 175 | return $data; 176 | } 177 | 178 | /** 179 | * Find a unique name for block in the flexible layout 180 | * 181 | * @param $name 182 | * @param $blocks 183 | * @param int $i 184 | * @return string 185 | */ 186 | protected function findUniqueIndex($name, $blocks, $i = 2) 187 | { 188 | // No suffix for the first item 189 | if (!array_key_exists($name, $blocks)) { 190 | return $name; 191 | } 192 | 193 | // For the rest of the items, start count from 2 194 | // e.g. 'itemName-2' 195 | if (array_key_exists($name . '-' . $i, $blocks)) { 196 | $i++; 197 | return $this->findUniqueIndex($name, $blocks, $i); 198 | } 199 | 200 | return $name . '-' . $i; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Setting.php: -------------------------------------------------------------------------------- 1 | config = $this->config + $config; 26 | 27 | if ( ! $this->name) { 28 | $this->name = $this->config['name']; 29 | } 30 | 31 | $this->fieldsBuilder = new FieldsBuilder(isset($this->config['field_group_name'])?$this->config['field_group_name']:$this->name); 32 | 33 | $this->setup(); 34 | } 35 | 36 | /** 37 | * Run setup functions 38 | */ 39 | protected function setup() 40 | { 41 | // Pseudo-constructor for child classes 42 | if (method_exists($this, 'init')) { 43 | $this->init(); 44 | } 45 | 46 | // Configure ACF fields 47 | if (method_exists($this, 'configureFields')) { 48 | $this->configureFields(); 49 | } 50 | } 51 | 52 | /** 53 | * @return FieldsBuilder 54 | */ 55 | public function getFieldsBuilder() 56 | { 57 | return $this->fieldsBuilder; 58 | } 59 | 60 | abstract function init(); 61 | } 62 | -------------------------------------------------------------------------------- /src/SettingInterface.php: -------------------------------------------------------------------------------- 1 | getFieldsBuilder() 14 | ->addText('block_admin_label', ['label' => 'Admin Label', 'instructions' => 'Give this block a name. This is only displayed for administrators.']); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Settings/Id.php: -------------------------------------------------------------------------------- 1 | getFieldsBuilder() 15 | ->addText('id', ['label' => 'ID', 'instructions' => 'Can be used to reference this block via links or buttons']); 16 | } 17 | } 18 | --------------------------------------------------------------------------------