├── .github └── workflows │ └── main.yml ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── autoload.php ├── composer.json ├── composer.lock ├── gulpfile.js ├── package.json ├── phpunit.xml ├── src ├── AccordionBuilder.php ├── Builder.php ├── ChoiceFieldBuilder.php ├── ConditionalBuilder.php ├── FieldBuilder.php ├── FieldManager.php ├── FieldNameCollisionException.php ├── FieldNotFoundException.php ├── FieldsBuilder.php ├── FlexibleContentBuilder.php ├── GroupBuilder.php ├── LayoutNotFoundException.php ├── LocationBuilder.php ├── ModifyFieldReturnTypeException.php ├── NamedBuilder.php ├── ParentDelegationBuilder.php ├── RepeaterBuilder.php ├── TabBuilder.php ├── Traits │ └── CanSingularize.php └── Transform │ ├── ConditionalField.php │ ├── ConditionalLogic.php │ ├── FlexibleContentLayout.php │ ├── IterativeTransform.php │ ├── NamespaceFieldKey.php │ ├── RecursiveTransform.php │ └── Transform.php └── tests ├── AccordionBuilderTest.php ├── ChoiceFieldBuilderTest.php ├── ConditionalBuilderTest.php ├── FieldBuilderTest.php ├── FieldManagerTest.php ├── FieldNameCollisionExceptionTest.php ├── FieldsBuilderCustomFieldKeysTest.php ├── FieldsBuilderTest.php ├── FlexibleContentBuilderTest.php ├── GroupBuilderTest.php ├── LocationBuilderTest.php ├── ParentDelegationBuilderTest.php ├── RepeaterBuilderTest.php ├── TabBuilderTest.php ├── TestUtils.php ├── Transform ├── ConditionalFieldTest.php ├── ConditionalLogicTest.php ├── FlexibleContentLayoutTest.php └── NamespaceFieldKeyTest.php └── test_autoload.php /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: TESTS 2 | 3 | on: 4 | pull_request: null 5 | push: 6 | branches: 7 | - master 8 | - develop 9 | 10 | jobs: 11 | tests: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | php: ['7.4', '8.0', '8.1', '8.2'] 16 | 17 | name: PHP ${{ matrix.php }} tests 18 | steps: 19 | # basically git clone 20 | - uses: actions/checkout@v2 21 | 22 | # use PHP of specific version 23 | - uses: shivammathur/setup-php@v2 24 | with: 25 | php-version: ${{ matrix.php }} 26 | coverage: none # disable xdebug, pcov 27 | 28 | # if we 2 steps like this, we can better see if composer failed or tests 29 | - run: composer update --no-progress 30 | 31 | - run: vendor/bin/phpunit 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | node_modules 3 | .idea 4 | .stignore 5 | .stversions 6 | .stfolder 7 | .phpunit.result.cache 8 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - "tests/*" 4 | checks: 5 | php: true 6 | javascript: true 7 | coding_style: 8 | php: {} 9 | build: 10 | environment: 11 | php: 12 | version: 7.4 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | dist: trusty 3 | php: 4 | - 7.4 5 | - 8.0 6 | - 8.1 7 | - 8.2 8 | install: composer update 9 | script: composer run test 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACF Builder 2 | 3 | Create configuration arrays for [Advanced Custom Fields Pro](https://www.advancedcustomfields.com/pro/) using the builder pattern and a fluent API. 4 | 5 | Quickly create, register, and reuse ACF configurations, and keep them in your source code repository. To read more about registering ACF fields via php consult https://www.advancedcustomfields.com/resources/register-fields-via-php/ 6 | 7 | [![Latest Stable Version](https://poser.pugx.org/stoutlogic/acf-builder/v/stable)](https://packagist.org/packages/stoutlogic/acf-builder) 8 | [![Build Status](https://github.com/stoutlogic/acf-builder/workflows/TESTS/badge.svg)](https://github.com/StoutLogic/acf-builder/actions?query=workflow%3ATESTS) 9 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/StoutLogic/acf-builder/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/StoutLogic/acf-builder/?branch=master) 10 | [![Join the chat at https://gitter.im/StoutLogic/acf-builder](https://badges.gitter.im/StoutLogic/acf-builder.svg)](https://gitter.im/StoutLogic/acf-builder?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 11 | 12 | ### Simple Example 13 | ```php 14 | $banner = new StoutLogic\AcfBuilder\FieldsBuilder('banner'); 15 | $banner 16 | ->addText('title') 17 | ->addWysiwyg('content') 18 | ->addImage('background_image') 19 | ->setLocation('post_type', '==', 'page') 20 | ->or('post_type', '==', 'post'); 21 | 22 | add_action('acf/init', function() use ($banner) { 23 | acf_add_local_field_group($banner->build()); 24 | }); 25 | ``` 26 | 27 | `$banner->build();` will return: 28 | ```php 29 | [ 30 | 'key' => 'group_banner', 31 | 'title' => 'Banner', 32 | 'fields' => [ 33 | [ 34 | 'key' => 'field_title', 35 | 'name' => 'title', 36 | 'label' => 'Title', 37 | 'type' => 'text' 38 | ], 39 | [ 40 | 'key' => 'field_content', 41 | 'name' => 'content', 42 | 'label' => 'Content', 43 | 'type' => 'wysiwyg' 44 | ], 45 | [ 46 | 'key' => 'field_background_image', 47 | 'name' => 'background_image', 48 | 'label' => 'Background Image', 49 | 'type' => 'image' 50 | ], 51 | ], 52 | 'location' => [ 53 | [ 54 | [ 55 | 'param' => 'post_type', 56 | 'operator' => '==', 57 | 'value' => 'page' 58 | ] 59 | ], 60 | [ 61 | [ 62 | 'param' => 'post_type', 63 | 'operator' => '==', 64 | 'value' => 'post' 65 | ] 66 | ] 67 | ] 68 | ] 69 | ``` 70 | 71 | As you can see it saves you a lot of typing and is less error-prone. But brevity and correctness isn't the only benefit, you can reuse field configurations in multiple places. For example, a group of fields used for backgrounds: 72 | 73 | ### Reuse Example 74 | 75 | ```php 76 | 77 | use StoutLogic\AcfBuilder\FieldsBuilder; 78 | 79 | $background = new FieldsBuilder('background'); 80 | $background 81 | ->addTab('Background') 82 | ->addImage('background_image') 83 | ->addTrueFalse('fixed') 84 | ->instructions("Check to add a parallax effect where the background image doesn't move when scrolling") 85 | ->addColorPicker('background_color'); 86 | 87 | $banner = new FieldsBuilder('banner'); 88 | $banner 89 | ->addTab('Content') 90 | ->addText('title') 91 | ->addWysiwyg('content') 92 | ->addFields($background) 93 | ->setLocation('post_type', '==', 'page'); 94 | 95 | $section = new FieldsBuilder('section'); 96 | $section 97 | ->addTab('Content') 98 | ->addText('section_title') 99 | ->addRepeater('columns', ['min' => 1, 'layout' => 'block']) 100 | ->addTab('Content') 101 | ->addText('title') 102 | ->addWysiwyg('content') 103 | ->addFields($background) 104 | ->endRepeater() 105 | ->addFields($background) 106 | ->setLocation('post_type', '==', 'page'); 107 | ``` 108 | 109 | Here a `background` field group is created, and then used in two other field groups, including twice in the `section` field group. This can really DRY up your code and keep your admin UI consistent. If you wanted to add a light/dark field for the text color field based on the background used, it would just need to be added in one spot and used everywhere. 110 | 111 | ## Install 112 | Use composer to install: 113 | ``` 114 | composer require stoutlogic/acf-builder 115 | ``` 116 | 117 | If your project isn't using composer, you can require the `autoload.php` file. 118 | 119 | ## Tests 120 | To run the tests you can manually run 121 | ``` 122 | vendor/bin/phpunit 123 | ``` 124 | or you can use the built in gulp task to run it on file change 125 | ``` 126 | npm install 127 | gulp 128 | ``` 129 | 130 | ## Requirements 131 | PHP 5.4 through 7.2 supported but automated tests cannot be run anymore so it might stop working at some point. 132 | >= 7.4, 8 Tested 133 | 134 | ## Documentation 135 | See the [wiki](https://github.com/StoutLogic/acf-builder/wiki) for more thorough documentation. The documentation has its [own repository](https://github.com/StoutLogic/acf-builder-wiki) and accepts pull requests for contributions. Any merges to master will get synced with the wiki here under the main project. 136 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | =5.4.0", 6 | "doctrine/inflector": "^1.1|^2.0" 7 | }, 8 | "require-dev": { 9 | "phpunit/phpunit": "^9.5", 10 | "squizlabs/php_codesniffer": "^2.6", 11 | "phpdocumentor/reflection-docblock": "2.*", 12 | "dms/phpunit-arraysubset-asserts": "^0.4.0", 13 | "phpspec/prophecy-phpunit": "^2.0.1" 14 | }, 15 | "license": "GPL-2.0+", 16 | "authors": [ 17 | { 18 | "name": "Steve Pfisterer", 19 | "email": "steve@stoutlogic.com" 20 | } 21 | ], 22 | "autoload": { 23 | "psr-4": { 24 | "StoutLogic\\AcfBuilder\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "StoutLogic\\AcfBuilder\\Tests\\": "tests/" 30 | } 31 | }, 32 | "scripts": { 33 | "test": "@php vendor/bin/phpunit", 34 | "test:no-deprecation": "@php -d 'error_reporting=E_ALL&~E_DEPRECATED' vendor/bin/phpunit" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | notify = require('gulp-notify'), 3 | phpunit = require('gulp-phpunit'); 4 | 5 | gulp.task('phpunit', function() { 6 | var options = {debug: false, notify: true}; 7 | gulp.src('phpunit.xml') 8 | .pipe(phpunit('', options)) 9 | .on('error', notify.onError({ 10 | title: "Failed Tests!", 11 | message: "Error(s) occurred during testing..." 12 | })); 13 | }); 14 | 15 | gulp.task('default', ['phpunit'], function(){ 16 | gulp.watch('**/*.php', function(){ 17 | gulp.run('phpunit'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "acf-builder", 3 | "version": "1.0.0", 4 | "description": "An Advanced Custom Field Configuration Builder", 5 | "author": "Steve Pfisterer ", 6 | "license": "GPL-2.0+", 7 | "devDependencies": { 8 | "gulp": "^3.9.1", 9 | "gulp-notify": "^2.2.0", 10 | "gulp-phpunit": "^0.14.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/AccordionBuilder.php: -------------------------------------------------------------------------------- 1 | setConfig('open', $value ? 1 : 0); 15 | } 16 | 17 | public function setMultiExpand($value = 1) 18 | { 19 | return $this->setConfig('multi_expand', $value ? 1 : 0); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | setChoices($config['choices']); 24 | unset($config['choices']); 25 | } 26 | parent::__construct($name, $type, $config); 27 | } 28 | 29 | /** 30 | * Add a choice with optional label. If label not supplied, choice value 31 | * will be used. 32 | * @param string $choice choice value 33 | * @param string $label label that appears 34 | * @return $this 35 | */ 36 | public function addChoice($choice, $label = null) 37 | { 38 | $label ?: $label = $choice; 39 | $this->choices[$choice] = $label; 40 | return $this; 41 | } 42 | 43 | /** 44 | * Add multiple choices. Also accepts multiple arguments, one for each choice. 45 | * @param array $choices Can be an array of key values ['choice' => 'label'] 46 | * @return $this 47 | */ 48 | public function addChoices($choices) 49 | { 50 | if (func_num_args() > 1) { 51 | $choices = func_get_args(); 52 | } 53 | 54 | foreach ($choices as $key => $value) { 55 | $label = $choice = $value; 56 | 57 | if (is_array($choice)) { 58 | $label = array_values($choice)[0]; 59 | $choice = array_keys($choice)[0]; 60 | } else if (is_string($key)) { 61 | $choice = $key; 62 | $label = $value; 63 | } 64 | 65 | $this->addChoice($choice, $label); 66 | } 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Discards existing choices and adds multiple choices. 73 | * Also accepts multiple arguments, one for each choice. 74 | * @param array $choices Can be an array of key values ['choice' => 'label'] 75 | * @return $this 76 | */ 77 | public function setChoices($choices) 78 | { 79 | if (func_num_args() > 1) { 80 | $choices = func_get_args(); 81 | } 82 | 83 | $this->choices = []; 84 | return $this->addChoices($choices); 85 | } 86 | 87 | /** 88 | * @return array 89 | */ 90 | private function getChoices() 91 | { 92 | return $this->choices; 93 | } 94 | 95 | /** 96 | * Build the field configuration array 97 | * @return array Field configuration array 98 | */ 99 | public function build() 100 | { 101 | return array_merge([ 102 | 'choices' => $this->getChoices() 103 | ], parent::build()); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ConditionalBuilder.php: -------------------------------------------------------------------------------- 1 | and($name, $operator, $value); 26 | } 27 | 28 | /** 29 | * Build the config 30 | * @return array 31 | */ 32 | public function build() 33 | { 34 | return $this->config; 35 | } 36 | 37 | /** 38 | * Creates an AND condition 39 | * @param string $name 40 | * @param string $operator 41 | * @param string $value 42 | * @return $this 43 | */ 44 | public function andCondition($name, $operator, $value) 45 | { 46 | $orCondition = $this->popOrCondition(); 47 | $orCondition[] = $this->createCondition($name, $operator, $value); 48 | $this->pushOrCondition($orCondition); 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Creates an OR condition 55 | * @param string $name 56 | * @param string $operator 57 | * @param string $value 58 | * @return $this 59 | */ 60 | public function orCondition($name, $operator, $value) 61 | { 62 | $condition = $this->createCondition($name, $operator, $value); 63 | $this->pushOrCondition([$condition]); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Creates a condition 70 | * @param string $name 71 | * @param string $operator 72 | * @param string $value 73 | * @return array 74 | */ 75 | protected function createCondition($name, $operator, $value) 76 | { 77 | return [ 78 | 'field' => $name, 79 | 'operator' => $operator, 80 | 'value' => $value, 81 | ]; 82 | } 83 | 84 | /** 85 | * Removes and returns the last top level OR condition 86 | * @return array 87 | */ 88 | protected function popOrCondition() 89 | { 90 | return array_pop($this->config); 91 | } 92 | 93 | /** 94 | * Adds a top level OR condition 95 | * @param array $condition 96 | * @return void 97 | */ 98 | protected function pushOrCondition($condition) 99 | { 100 | $this->config[] = $condition; 101 | } 102 | 103 | /** 104 | * Allow the use of reserved words and / or for methods. If `and` or `or` 105 | * are not matched, call the method on the parentContext 106 | * @param string $methodName 107 | * @param array $arguments 108 | * @return mixed 109 | */ 110 | public function __call($methodName, $arguments) 111 | { 112 | if ($methodName === 'and') { 113 | list($name, $operator, $value) = $arguments; 114 | return $this->andCondition($name, $operator, $value); 115 | } elseif ($methodName === 'or') { 116 | list($name, $operator, $value) = $arguments; 117 | return $this->orCondition($name, $operator, $value); 118 | } else { 119 | return parent::__call($methodName, $arguments); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/FieldBuilder.php: -------------------------------------------------------------------------------- 1 | config = [ 72 | 'name' => $name, 73 | 'label' => $this->generateLabel($name), 74 | ]; 75 | 76 | $this->type = $type; 77 | $this->setKey($name); 78 | $this->updateConfig($config); 79 | } 80 | 81 | /** 82 | * @return array 83 | */ 84 | public function getConfig() 85 | { 86 | return $this->config; 87 | } 88 | 89 | /** 90 | * Set a config key -> value pair 91 | * @param string $key 92 | * @param mixed $value 93 | * @return $this 94 | */ 95 | public function setConfig($key, $value) 96 | { 97 | return $this->updateConfig([$key => $value]); 98 | } 99 | 100 | /** 101 | * Update multiple config values using and array of key -> value pairs. 102 | * @param array $config 103 | * @return $this 104 | */ 105 | public function updateConfig($config) 106 | { 107 | $this->config = array_merge($this->config, $config); 108 | return $this; 109 | } 110 | 111 | /** 112 | * @return string 113 | */ 114 | public function getName() 115 | { 116 | return $this->config['name']; 117 | } 118 | 119 | /** 120 | * @return string 121 | */ 122 | public function getKey() 123 | { 124 | return $this->config['key']; 125 | } 126 | 127 | /** 128 | * @return string 129 | */ 130 | public function getLabel() 131 | { 132 | return $this->config['label']; 133 | } 134 | 135 | /** 136 | * Will prepend `field_` if missing. 137 | * @param string $key 138 | * @return $this 139 | */ 140 | public function setKey($key) 141 | { 142 | if (!preg_match('/^field_/', $key)) { 143 | $key = 'field_' . $key; 144 | } 145 | 146 | return $this->setConfig('key', $key); 147 | } 148 | 149 | public function setCustomKey($key) 150 | { 151 | return $this 152 | ->setConfig('key', $key) 153 | ->setConfig('_has_custom_key', true); 154 | } 155 | 156 | /** 157 | * @return bool 158 | */ 159 | public function hasCustomKey() 160 | { 161 | return array_key_exists('_has_custom_key', $this->config) && $this->config['_has_custom_key']; 162 | } 163 | 164 | 165 | /** 166 | * Will set field required. 167 | * @return $this 168 | */ 169 | public function setRequired() 170 | { 171 | return $this->setConfig('required', 1); 172 | } 173 | 174 | /** 175 | * Will set field unrequired. 176 | * @return $this 177 | */ 178 | public function setUnrequired() 179 | { 180 | return $this->setConfig('required', 0); 181 | } 182 | 183 | /** 184 | * Will set field's label. 185 | * @param string $label 186 | * @return $this 187 | */ 188 | public function setLabel($label) 189 | { 190 | return $this->setConfig('label', $label); 191 | } 192 | 193 | /** 194 | * Will set field's instructions. 195 | * @param string $instructions 196 | * @return $this 197 | */ 198 | public function setInstructions($instructions) 199 | { 200 | return $this->setConfig('instructions', $instructions); 201 | } 202 | 203 | /** 204 | * Will set field's defaultValue. 205 | * @param string $defaultValue 206 | * @return $this 207 | */ 208 | public function setDefaultValue($defaultValue) 209 | { 210 | return $this->setConfig('default_value', $defaultValue); 211 | } 212 | 213 | /** 214 | * Add a conditional logic statement that will determine if the last added 215 | * field will display or not. You can add `or` or `and` calls after 216 | * to build complex logic. Any other function call will return you to the 217 | * parentContext. 218 | * @param string $name Dependent field name 219 | * (choice type: radio, checkbox, select, trueFalse) 220 | * @param string $operator ==, != 221 | * @param string $value 1 or choice value 222 | * @return ConditionalBuilder 223 | */ 224 | public function conditional($name, $operator, $value) 225 | { 226 | $conditionalBuilder = new ConditionalBuilder($name, $operator, $value); 227 | $conditionalBuilder->setParentContext($this); 228 | 229 | $this->setConfig('conditional_logic', $conditionalBuilder); 230 | 231 | return $conditionalBuilder; 232 | } 233 | 234 | /** 235 | * Set Wrapper container tag attributes 236 | * 237 | * @param array $config 238 | * 239 | * @return FieldBuilder 240 | */ 241 | public function setWrapper($config) 242 | { 243 | return $this->setConfig('wrapper', $config); 244 | } 245 | 246 | /** 247 | * Get Wrapper container tag attributes 248 | * 249 | * @return array|mixed 250 | */ 251 | public function getWrapper() 252 | { 253 | if (isset($this->config['wrapper'])) { 254 | return $this->config['wrapper']; 255 | } 256 | 257 | return []; 258 | } 259 | 260 | /** 261 | * Set width of a Wrapper container 262 | * 263 | * @param string $width Width of a container in % or px. 264 | * 265 | * @return FieldBuilder 266 | */ 267 | public function setWidth($width) 268 | { 269 | $wrapper = $this->getWrapper(); 270 | $wrapper['width'] = $width; 271 | 272 | return $this->setWrapper($wrapper); 273 | } 274 | 275 | /** 276 | * Set specified Attr of a Wrapper container 277 | * 278 | * @param string $name Attribute name, ex. 'class'. 279 | * @param string|null $value Attribute value, ex. 'my-class'. 280 | * 281 | * @return FieldBuilder 282 | */ 283 | public function setAttr($name, $value = null) 284 | { 285 | $wrapper = $this->getWrapper(); 286 | 287 | // set attribute. 288 | $wrapper[$name] = $value; 289 | 290 | return $this->setWrapper($wrapper); 291 | } 292 | 293 | /** 294 | * Set Class and/or ID attribute of a Wrapper container 295 | * use CSS-like selector string to specify css or id 296 | * example: #my-id.foo-class.bar-class 297 | * 298 | * @param string $css_selector 299 | * 300 | * @return FieldBuilder 301 | */ 302 | public function setSelector($css_selector) 303 | { 304 | // if # is the first sign - we start with ID. 305 | if (0 === strpos($css_selector, '#')) { 306 | $css_selector .= '.'; // prevent empty second part. 307 | list($id, $class) = explode('.', $css_selector, 2); 308 | } else { 309 | $css_selector .= '#'; // prevent empty second part. 310 | list($class, $id) = explode('#', $css_selector, 2); 311 | } 312 | 313 | $id = trim($id, '#'); 314 | $class = trim($class, '.'); 315 | 316 | if (!empty($id)) { 317 | $this->setAttr('id', $id); 318 | } 319 | 320 | if (!empty($class)) { 321 | $class = str_replace('.', ' ', $class); 322 | $this->setAttr('class', $class); 323 | } 324 | 325 | return $this; 326 | } 327 | 328 | /** 329 | * Build the field configuration array 330 | * @return array Field configuration array 331 | */ 332 | public function build() 333 | { 334 | $config = array_merge([ 335 | 'type' => $this->type, 336 | ], $this->getConfig()); 337 | 338 | foreach ($config as $key => $value) { 339 | if ($value instanceof Builder) { 340 | $config[$key] = $value->build(); 341 | } 342 | } 343 | 344 | return $config; 345 | } 346 | 347 | /** 348 | * Create a field label based on the field's name. Generates title case. 349 | * @param string $name 350 | * @return string label 351 | */ 352 | protected function generateLabel($name) 353 | { 354 | return ucwords(str_replace("_", " ", $name)); 355 | } 356 | 357 | /** 358 | * Generates a snaked cased name. 359 | * @param string $name 360 | * @return string 361 | */ 362 | protected function generateName($name) 363 | { 364 | return strtolower(str_replace(" ", "_", $name)); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/FieldManager.php: -------------------------------------------------------------------------------- 1 | fields = $fields; 22 | } 23 | 24 | /** 25 | * @return FieldBuilder[] field configs 26 | */ 27 | public function getFields() 28 | { 29 | return $this->fields; 30 | } 31 | 32 | /** 33 | * Return int of fields 34 | * @return int field count 35 | */ 36 | public function getCount() 37 | { 38 | return count($this->getFields()); 39 | } 40 | 41 | /** 42 | * Add field to end of array 43 | * @param FieldBuilder $field Field array config or Builder 44 | * @return void 45 | */ 46 | public function pushField($field) 47 | { 48 | $this->insertFields($field, $this->getCount()); 49 | } 50 | 51 | /** 52 | * Remove last field from end of array 53 | * @throws \OutOfRangeException if array is empty 54 | * @return FieldBuilder Field array config or Builder 55 | */ 56 | public function popField() 57 | { 58 | if ($this->getCount() > 0) { 59 | $fields = $this->removeFieldAtIndex($this->getCount() - 1); 60 | return $fields[0]; 61 | } 62 | 63 | throw new \OutOfRangeException("Can't call popField when the field count is 0"); 64 | } 65 | 66 | /** 67 | * Insert of field at a specific index 68 | * @param FieldBuilder|FieldBuilder[] $fields a single field or an array of fields 69 | * @param int $index insertion point 70 | * @return void 71 | */ 72 | public function insertFields($fields, $index) 73 | { 74 | if (!$fields instanceof FieldBuilder && !is_array($fields)) { 75 | return; 76 | } 77 | 78 | // If a singular field config, put into an array of fields 79 | if ($fields instanceof FieldBuilder) { 80 | $fields = [$fields]; 81 | } 82 | 83 | 84 | foreach ($fields as $i => $field) { 85 | if ($this->validateField($field)) { 86 | array_splice($this->fields, $index + $i, 0, [$field]); 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Remove a field at a specific index 93 | * @param int $index 94 | * @return array removed field 95 | */ 96 | private function removeFieldAtIndex($index) 97 | { 98 | return array_splice($this->fields, $index, 1); 99 | } 100 | 101 | /** 102 | * Remove a speicifc field by name 103 | * @param string $name name of the field 104 | * @return void 105 | */ 106 | public function removeField($name) 107 | { 108 | $index = $this->getFieldIndex($name); 109 | $this->removeFieldAtIndex($index); 110 | } 111 | 112 | /** 113 | * Replace a field with a single field or array of fields 114 | * @param string $name name of field to replace 115 | * @param FieldBuilder|FieldBuilder[] $field single or array of fields 116 | * @throws FieldNotFoundException if the field name doesn't exist 117 | * @return void 118 | */ 119 | public function replaceField($name, $field) 120 | { 121 | $index = $this->getFieldIndex($name); 122 | $this->removeFieldAtIndex($index); 123 | $this->insertFields($field, $index); 124 | } 125 | 126 | /** 127 | * Check to see if a field name already exists 128 | * @param string $name field name 129 | * @return bool 130 | */ 131 | public function fieldNameExists($name) 132 | { 133 | try { 134 | $this->getFieldIndex($name); 135 | } catch (FieldNotFoundException $e) { 136 | return false; 137 | } 138 | 139 | return true; 140 | } 141 | 142 | /** 143 | * Return a field by name 144 | * @param string $name field name 145 | * @return FieldBuilder 146 | */ 147 | public function getField($name) 148 | { 149 | return $this->fields[$this->getFieldIndex($name)]; 150 | } 151 | 152 | /** 153 | * Modify the configuration of a field 154 | * @param string $name field name 155 | * @param array $modifications field configuration 156 | * @return void 157 | */ 158 | public function modifyField($name, $modifications) 159 | { 160 | $field = $this->getField($name)->updateConfig($modifications); 161 | $this->replaceField($name, $field); 162 | } 163 | 164 | /** 165 | * Validate a field 166 | * @param FieldBuilder $field 167 | * @return bool 168 | */ 169 | private function validateField($field) 170 | { 171 | return $this->validateFieldName($field); 172 | } 173 | 174 | /** 175 | * Validates that a field's name doesn't already exist 176 | * @param FieldBuilder $field 177 | * @throws FieldNameCollisionException when the name already exists 178 | * @return bool 179 | */ 180 | private function validateFieldName($field) 181 | { 182 | $fieldName = $field->getName(); 183 | if (!$fieldName || $this->fieldNameExists($fieldName)) { 184 | throw new FieldNameCollisionException("Field Name: `{$fieldName}` already exists."); 185 | } 186 | 187 | return true; 188 | } 189 | 190 | /** 191 | * Return the index in the $this->fields array looked up by the field's name 192 | * @param string $name Field Name 193 | * @throws FieldNotFoundException if the field name doesn't exist 194 | * @return int Field Index 195 | */ 196 | public function getFieldIndex($name) 197 | { 198 | foreach ($this->getFields() as $index => $field) { 199 | if ($field->getName() === $name) { 200 | return $index; 201 | } 202 | } 203 | 204 | throw new FieldNotFoundException("Field `{$name}` not found."); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/FieldNameCollisionException.php: -------------------------------------------------------------------------------- 1 | '; 35 | 36 | /** 37 | * @param string $name Field Group name 38 | * @param array $groupConfig Field Group configuration 39 | */ 40 | public function __construct($name, array $groupConfig = []) 41 | { 42 | $this->fieldManager = new FieldManager(); 43 | $this->name = $name; 44 | $this->setGroupConfig('key', $name); 45 | $this->setGroupConfig('title', $this->generateLabel($name)); 46 | 47 | $this->config = array_merge($this->config, $groupConfig); 48 | } 49 | 50 | /** 51 | * Set a value for a particular key in the group config 52 | * @param string $key 53 | * @param mixed $value 54 | * @return $this 55 | */ 56 | public function setGroupConfig($key, $value) 57 | { 58 | $this->config[$key] = $value; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Get a value for a particular key in the group config. 65 | * Returns null if the key isn't defined in the config. 66 | * @param string $key 67 | * @return mixed|null 68 | */ 69 | public function getGroupConfig($key) 70 | { 71 | if (array_key_exists($key, $this->config)) { 72 | return $this->config[$key]; 73 | } 74 | 75 | return null; 76 | } 77 | 78 | public function updateGroupConfig($config) 79 | { 80 | $this->config = array_merge($this->config, $config); 81 | return $this; 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | public function getName() 88 | { 89 | return $this->name; 90 | } 91 | 92 | /** 93 | * Namespace a group key 94 | * Append the namespace 'group' before the set key. 95 | * 96 | * @param string $key Field Key 97 | * @return string Field Key 98 | */ 99 | private function namespaceGroupKey($key) 100 | { 101 | if (strpos($key, 'group_') !== 0) { 102 | $key = 'group_' . $key; 103 | } 104 | return $key; 105 | } 106 | 107 | /** 108 | * Build the final config array. Build any other builders that may exist 109 | * in the config. 110 | * @return array Final field config 111 | */ 112 | public function build() 113 | { 114 | return array_merge($this->config, [ 115 | 'fields' => $this->buildFields(), 116 | 'location' => $this->buildLocation(), 117 | 'key' => $this->namespaceGroupKey($this->config['key']), 118 | ]); 119 | } 120 | 121 | /** 122 | * Return a fields config array 123 | * @return array 124 | */ 125 | private function buildFields() 126 | { 127 | $fields = array_map(function ($field) { 128 | return ($field instanceof Builder) ? $field->build() : $field; 129 | }, $this->getFields()); 130 | 131 | return $this->transformFields($fields); 132 | } 133 | 134 | /** 135 | * Apply field transforms 136 | * @param array $fields 137 | * @return array Transformed fields config 138 | */ 139 | private function transformFields($fields) 140 | { 141 | $conditionalTransform = new Transform\ConditionalLogic($this); 142 | $namespaceFieldKeyTransform = new Transform\NamespaceFieldKey($this); 143 | 144 | return 145 | $namespaceFieldKeyTransform->transform( 146 | $conditionalTransform->transform($fields) 147 | ); 148 | } 149 | 150 | /** 151 | * Return a locations config array 152 | * @return array|LocationBuilder 153 | */ 154 | private function buildLocation() 155 | { 156 | $location = $this->getLocation(); 157 | return ($location instanceof Builder) ? $location->build() : $location; 158 | } 159 | 160 | /** 161 | * Add multiple fields either via an array or from another builder 162 | * @param FieldsBuilder|array $fields 163 | * @return $this 164 | */ 165 | public function addFields($fields) 166 | { 167 | if ($fields instanceof FieldsBuilder) { 168 | $builder = clone $fields; 169 | $fields = $builder->getFields(); 170 | } 171 | 172 | foreach ($fields as $field) { 173 | $this->getFieldManager()->pushField($field); 174 | } 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * Add a field of a specific type 181 | * @param string $name 182 | * @param string $type 183 | * @param array $args field configuration 184 | * @throws FieldNameCollisionException if name already exists. 185 | * @return FieldBuilder 186 | */ 187 | public function addField($name, $type, array $args = []) 188 | { 189 | return $this->initializeField(new FieldBuilder($name, $type, $args)); 190 | } 191 | 192 | /** 193 | * Add a field of a choice type, allows choices to be added. 194 | * @param string $name 195 | * @param string $type 'select', 'radio', 'checkbox' 196 | * @param array $args field configuration 197 | * @throws FieldNameCollisionException if name already exists. 198 | * @return FieldBuilder 199 | */ 200 | public function addChoiceField($name, $type, array $args = []) 201 | { 202 | return $this->initializeField(new ChoiceFieldBuilder($name, $type, $args)); 203 | } 204 | 205 | /** 206 | * Initialize the FieldBuilder, add to FieldManager 207 | * @param FieldBuilder $field 208 | * @return FieldBuilder 209 | */ 210 | protected function initializeField($field) 211 | { 212 | $field->setParentContext($this); 213 | $this->getFieldManager()->pushField($field); 214 | return $field; 215 | } 216 | 217 | /** 218 | * @param string $name 219 | * @param array $args field configuration 220 | * @throws FieldNameCollisionException if name already exists. 221 | * @return FieldBuilder 222 | */ 223 | public function addText($name, array $args = []) 224 | { 225 | return $this->addField($name, 'text', $args); 226 | } 227 | 228 | /** 229 | * @param string $name 230 | * @param array $args field configuration 231 | * @throws FieldNameCollisionException if name already exists. 232 | * @return FieldBuilder 233 | */ 234 | public function addTextarea($name, array $args = []) 235 | { 236 | return $this->addField($name, 'textarea', $args); 237 | } 238 | 239 | /** 240 | * @param string $name 241 | * @param array $args field configuration 242 | * @throws FieldNameCollisionException if name already exists. 243 | * @return FieldBuilder 244 | */ 245 | public function addNumber($name, array $args = []) 246 | { 247 | return $this->addField($name, 'number', $args); 248 | } 249 | 250 | /** 251 | * @param string $name 252 | * @param array $args field configuration 253 | * @throws FieldNameCollisionException if name already exists. 254 | * @return FieldBuilder 255 | */ 256 | public function addEmail($name, array $args = []) 257 | { 258 | return $this->addField($name, 'email', $args); 259 | } 260 | 261 | /** 262 | * @param string $name 263 | * @param array $args field configuration 264 | * @throws FieldNameCollisionException if name already exists. 265 | * @return FieldBuilder 266 | */ 267 | public function addUrl($name, array $args = []) 268 | { 269 | return $this->addField($name, 'url', $args); 270 | } 271 | 272 | /** 273 | * @param string $name 274 | * @param array $args field configuration 275 | * @throws FieldNameCollisionException if name already exists. 276 | * @return FieldBuilder 277 | */ 278 | public function addPassword($name, array $args = []) 279 | { 280 | return $this->addField($name, 'password', $args); 281 | } 282 | 283 | /** 284 | * @param string $name 285 | * @param array $args field configuration 286 | * @throws FieldNameCollisionException if name already exists. 287 | * @return FieldBuilder 288 | */ 289 | public function addWysiwyg($name, array $args = []) 290 | { 291 | return $this->addField($name, 'wysiwyg', $args); 292 | } 293 | 294 | /** 295 | * @param string $name 296 | * @param array $args field configuration 297 | * @throws FieldNameCollisionException if name already exists. 298 | * @return FieldBuilder 299 | */ 300 | public function addOembed($name, array $args = []) 301 | { 302 | return $this->addField($name, 'oembed', $args); 303 | } 304 | 305 | /** 306 | * @param string $name 307 | * @param array $args field configuration 308 | * @throws FieldNameCollisionException if name already exists. 309 | * @return FieldBuilder 310 | */ 311 | public function addImage($name, array $args = []) 312 | { 313 | return $this->addField($name, 'image', $args); 314 | } 315 | 316 | /** 317 | * @param string $name 318 | * @param array $args field configuration 319 | * @throws FieldNameCollisionException if name already exists. 320 | * @return FieldBuilder 321 | */ 322 | public function addFile($name, array $args = []) 323 | { 324 | return $this->addField($name, 'file', $args); 325 | } 326 | 327 | /** 328 | * @param string $name 329 | * @param array $args field configuration 330 | * @throws FieldNameCollisionException if name already exists. 331 | * @return FieldBuilder 332 | */ 333 | public function addGallery($name, array $args = []) 334 | { 335 | return $this->addField($name, 'gallery', $args); 336 | } 337 | 338 | /** 339 | * @param string $name 340 | * @param array $args field configuration 341 | * @throws FieldNameCollisionException if name already exists. 342 | * @return FieldBuilder 343 | */ 344 | public function addTrueFalse($name, array $args = []) 345 | { 346 | return $this->addField($name, 'true_false', $args); 347 | } 348 | 349 | /** 350 | * @param string $name 351 | * @param array $args field configuration 352 | * @throws FieldNameCollisionException if name already exists. 353 | * @return FieldBuilder 354 | */ 355 | public function addSelect($name, array $args = []) 356 | { 357 | return $this->addChoiceField($name, 'select', $args); 358 | } 359 | 360 | /** 361 | * @param string $name 362 | * @param array $args field configuration 363 | * @throws FieldNameCollisionException if name already exists. 364 | * @return FieldBuilder 365 | */ 366 | public function addRadio($name, array $args = []) 367 | { 368 | return $this->addChoiceField($name, 'radio', $args); 369 | } 370 | 371 | /** 372 | * @param string $name 373 | * @param array $args field configuration 374 | * @throws FieldNameCollisionException if name already exists. 375 | * @return FieldBuilder 376 | */ 377 | public function addCheckbox($name, array $args = []) 378 | { 379 | return $this->addChoiceField($name, 'checkbox', $args); 380 | } 381 | 382 | /** 383 | * @param string $name 384 | * @param array $args field configuration 385 | * @throws FieldNameCollisionException if name already exists. 386 | * @return FieldBuilder 387 | */ 388 | public function addButtonGroup($name, array $args = []) 389 | { 390 | return $this->addChoiceField($name, 'button_group', $args); 391 | } 392 | 393 | /** 394 | * @param string $name 395 | * @param array $args field configuration 396 | * @throws FieldNameCollisionException if name already exists. 397 | * @return FieldBuilder 398 | */ 399 | public function addPostObject($name, array $args = []) 400 | { 401 | return $this->addField($name, 'post_object', $args); 402 | } 403 | 404 | /** 405 | * @param string $name 406 | * @param array $args field configuration 407 | * @throws FieldNameCollisionException if name already exists. 408 | * @return FieldBuilder 409 | */ 410 | public function addPageLink($name, array $args = []) 411 | { 412 | return $this->addField($name, 'page_link', $args); 413 | } 414 | 415 | /** 416 | * @param string $name 417 | * @param array $args field configuration 418 | * @throws FieldNameCollisionException if name already exists. 419 | * @return FieldBuilder 420 | */ 421 | public function addRelationship($name, array $args = []) 422 | { 423 | return $this->addField($name, 'relationship', $args); 424 | } 425 | 426 | /** 427 | * @param string $name 428 | * @param array $args field configuration 429 | * @throws FieldNameCollisionException if name already exists. 430 | * @return FieldBuilder 431 | */ 432 | public function addTaxonomy($name, array $args = []) 433 | { 434 | return $this->addField($name, 'taxonomy', $args); 435 | } 436 | 437 | /** 438 | * @param string $name 439 | * @param array $args field configuration 440 | * @throws FieldNameCollisionException if name already exists. 441 | * @return FieldBuilder 442 | */ 443 | public function addUser($name, array $args = []) 444 | { 445 | return $this->addField($name, 'user', $args); 446 | } 447 | 448 | /** 449 | * @param string $name 450 | * @param array $args field configuration 451 | * @throws FieldNameCollisionException if name already exists. 452 | * @return FieldBuilder 453 | */ 454 | public function addDatePicker($name, array $args = []) 455 | { 456 | return $this->addField($name, 'date_picker', $args); 457 | } 458 | 459 | /** 460 | * @param string $name 461 | * @param array $args field configuration 462 | * @throws FieldNameCollisionException if name already exists. 463 | * @return FieldBuilder 464 | */ 465 | public function addTimePicker($name, array $args = []) 466 | { 467 | return $this->addField($name, 'time_picker', $args); 468 | } 469 | 470 | /** 471 | * @param string $name 472 | * @param array $args field configuration 473 | * @throws FieldNameCollisionException if name already exists. 474 | * @return FieldBuilder 475 | */ 476 | public function addDateTimePicker($name, array $args = []) 477 | { 478 | return $this->addField($name, 'date_time_picker', $args); 479 | } 480 | 481 | /** 482 | * @param string $name 483 | * @param array $args field configuration 484 | * @throws FieldNameCollisionException if name already exists. 485 | * @return FieldBuilder 486 | */ 487 | public function addColorPicker($name, array $args = []) 488 | { 489 | return $this->addField($name, 'color_picker', $args); 490 | } 491 | 492 | /** 493 | * @param string $name 494 | * @param array $args field configuration 495 | * @throws FieldNameCollisionException if name already exists. 496 | * @return FieldBuilder 497 | */ 498 | public function addGoogleMap($name, array $args = []) 499 | { 500 | return $this->addField($name, 'google_map', $args); 501 | } 502 | 503 | /** 504 | * @param string $name 505 | * @param array $args field configuration 506 | * @throws FieldNameCollisionException if name already exists. 507 | * @return FieldBuilder 508 | */ 509 | public function addLink($name, array $args = []) 510 | { 511 | return $this->addField($name, 'link', $args); 512 | } 513 | 514 | /** 515 | * @param string $name 516 | * @param array $args field configuration 517 | * @throws FieldNameCollisionException if name already exists. 518 | * @return FieldBuilder 519 | */ 520 | public function addRange($name, array $args = []) 521 | { 522 | return $this->addField($name, 'range', $args); 523 | } 524 | 525 | /** 526 | * All fields added after will appear under this tab, until another tab 527 | * is added. 528 | * @param string $label Tab label 529 | * @param array $args field configuration 530 | * @throws FieldNameCollisionException if name already exists. 531 | * @return FieldBuilder 532 | */ 533 | public function addTab($label, array $args = []) 534 | { 535 | return $this->initializeField(new TabBuilder($label, 'tab', $args)); 536 | } 537 | 538 | /** 539 | * All fields added after will appear under this accordion, until 540 | * another accordion is added. 541 | * @param string $label Accordion label 542 | * @param array $args field configuration 543 | * @throws FieldNameCollisionException if name already exists. 544 | * @return AccordionBuilder 545 | */ 546 | public function addAccordion($label, array $args = []) 547 | { 548 | return $this->initializeField(new AccordionBuilder($label, 'accordion', $args)); 549 | } 550 | 551 | /** 552 | * Addes a message field 553 | * @param string $label 554 | * @param string $message 555 | * @param array $args field configuration 556 | * @throws FieldNameCollisionException if name already exists. 557 | * @return FieldBuilder 558 | */ 559 | public function addMessage($label, $message, array $args = []) 560 | { 561 | $name = $this->generateName($label) . '_message'; 562 | $args = array_merge([ 563 | 'label' => $label, 564 | 'message' => $message, 565 | ], $args); 566 | 567 | return $this->addField($name, 'message', $args); 568 | } 569 | 570 | /** 571 | * @param string $name 572 | * @param array $args field configuration 573 | * @throws FieldNameCollisionException if name already exists. 574 | * @return GroupBuilder 575 | */ 576 | public function addGroup($name, array $args = []) 577 | { 578 | return $this->initializeField(new GroupBuilder($name, 'group', $args)); 579 | } 580 | 581 | /** 582 | * Add a repeater field. Any fields added after will be added to the repeater 583 | * until `endRepeater` is called. 584 | * @param string $name 585 | * @param array $args field configuration 586 | * @throws FieldNameCollisionException if name already exists. 587 | * @return RepeaterBuilder 588 | */ 589 | public function addRepeater($name, array $args = []) 590 | { 591 | return $this->initializeField(new RepeaterBuilder($name, 'repeater', $args)); 592 | } 593 | 594 | /** 595 | * Add a flexible content field. Once adding a layout with `addLayout`, 596 | * any fields added after will be added to that layout until another 597 | * `addLayout` call is made, or until `endFlexibleContent` is called. 598 | * @param string $name 599 | * @param array $args field configuration 600 | * @throws FieldNameCollisionException if name already exists. 601 | * @return FlexibleContentBuilder 602 | */ 603 | public function addFlexibleContent($name, array $args = []) 604 | { 605 | return $this->initializeField(new FlexibleContentBuilder($name, 'flexible_content', $args)); 606 | } 607 | 608 | /** 609 | * @return FieldManager 610 | */ 611 | protected function getFieldManager() 612 | { 613 | return $this->fieldManager; 614 | } 615 | 616 | /** 617 | * @return FieldBuilder[] 618 | */ 619 | public function getFields() 620 | { 621 | return $this->getFieldManager()->getFields(); 622 | } 623 | 624 | /** 625 | * @param string $name [description] 626 | * @return FieldBuilder 627 | */ 628 | public function getField($name) 629 | { 630 | return $this->getFieldManager()->getField($name); 631 | } 632 | 633 | public function fieldExists($name) 634 | { 635 | return $this->getFieldManager()->fieldNameExists($name); 636 | } 637 | 638 | /** 639 | * Modify an already defined field 640 | * @param string $name Name of the field 641 | * @param array|\Closure $modify Array of field configs or a closure that accepts 642 | * a FieldsBuilder and returns a FieldsBuilder. 643 | * @throws ModifyFieldReturnTypeException if $modify is a closure and doesn't 644 | * return a FieldsBuilder. 645 | * @throws FieldNotFoundException if the field name doesn't exist. 646 | * @return $this 647 | */ 648 | public function modifyField($name, $modify) 649 | { 650 | if ($this->hasDeeplyNestedField($name)) { 651 | $fieldNames = explode(self::DEEP_NESTING_DELIMITER, $name, 2); 652 | $this->getField($fieldNames[0])->modifyField($fieldNames[1], $modify); 653 | 654 | return $this; 655 | } 656 | 657 | if (is_array($modify)) { 658 | $this->getFieldManager()->modifyField($name, $modify); 659 | return $this; 660 | } elseif ($modify instanceof \Closure) { 661 | $field = $this->getField($name); 662 | 663 | // Initialize Modifying FieldsBuilder 664 | $modifyBuilder = new FieldsBuilder(''); 665 | $modifyBuilder->addFields([$field]); 666 | 667 | /** 668 | * @var FieldsBuilder 669 | */ 670 | $modifyBuilder = $modify($modifyBuilder); 671 | 672 | // Check if a FieldsBuilder is returned 673 | if (!$modifyBuilder instanceof FieldsBuilder) { 674 | throw new ModifyFieldReturnTypeException(gettype($modifyBuilder)); 675 | } 676 | 677 | // Insert field(s) 678 | $this->getFieldManager()->replaceField($name, $modifyBuilder->getFields()); 679 | } 680 | 681 | return $this; 682 | } 683 | 684 | /** 685 | * Remove a field by name 686 | * @param string $name Field to remove 687 | * @return $this 688 | */ 689 | public function removeField($name) 690 | { 691 | if ($this->hasDeeplyNestedField($name)) { 692 | $fieldNames = explode(self::DEEP_NESTING_DELIMITER, $name, 2); 693 | $this->getField($fieldNames[0])->removeField($fieldNames[1]); 694 | return $this; 695 | } 696 | 697 | $this->getFieldManager()->removeField($name); 698 | 699 | return $this; 700 | } 701 | 702 | /** 703 | * @param string $name Deeply nested field name 704 | * @return bool 705 | */ 706 | private function hasDeeplyNestedField($name) 707 | { 708 | return strpos($name, static::DEEP_NESTING_DELIMITER) !== false; 709 | } 710 | 711 | /** 712 | * Set the location of the field group. See 713 | * https://github.com/StoutLogic/acf-builder/wiki/location and 714 | * https://www.advancedcustomfields.com/resources/custom-location-rules/ 715 | * for more details. 716 | * @param string $param 717 | * @param string $operator 718 | * @param string $value 719 | * @return LocationBuilder 720 | */ 721 | public function setLocation($param, $operator, $value) 722 | { 723 | if ($this->getParentContext()) { 724 | return $this->getParentContext()->setLocation($param, $operator, $value); 725 | } 726 | 727 | $this->location = new LocationBuilder($param, $operator, $value); 728 | $this->location->setParentContext($this); 729 | 730 | return $this->location; 731 | } 732 | 733 | /** 734 | * @return LocationBuilder 735 | */ 736 | public function getLocation() 737 | { 738 | return $this->location; 739 | } 740 | 741 | /** 742 | * Create a field label based on the field's name. Generates title case. 743 | * @param string $name 744 | * @return string label 745 | */ 746 | protected function generateLabel($name) 747 | { 748 | return ucwords(str_replace('_', ' ', $name)); 749 | } 750 | 751 | /** 752 | * Generates a snaked cased name. 753 | * @param string $name 754 | * @return string 755 | */ 756 | protected function generateName($name) 757 | { 758 | return strtolower(str_replace(' ', '_', $name)); 759 | } 760 | 761 | public function __clone() 762 | { 763 | $this->fieldManager = clone $this->fieldManager; 764 | } 765 | } 766 | -------------------------------------------------------------------------------- /src/FlexibleContentBuilder.php: -------------------------------------------------------------------------------- 1 | setConfig('button_label', $this->getDefaultButtonLabel()); 30 | } 31 | } 32 | 33 | /** 34 | * Return a configuration array 35 | * @return array 36 | */ 37 | public function build() 38 | { 39 | return array_merge(parent::build(), [ 40 | 'layouts' => $this->buildLayouts(), 41 | ]); 42 | } 43 | 44 | /** 45 | * Return a configuration array for each layout 46 | * @return array 47 | */ 48 | private function buildLayouts() 49 | { 50 | return array_map(function ($layout) { 51 | $layout = ($layout instanceof Builder) ? $layout->build() : $layout; 52 | return $this->transformLayout($layout); 53 | }, $this->getLayouts()); 54 | } 55 | 56 | /** 57 | * Apply transformations to a layout 58 | * @param array $layout Layout configuration array 59 | * @return array Transformed layout configuration array 60 | */ 61 | private function transformLayout($layout) 62 | { 63 | $layoutTransform = new Transform\FlexibleContentLayout($this); 64 | $namespaceTransform = new Transform\NamespaceFieldKey($this); 65 | 66 | return 67 | $namespaceTransform->transform( 68 | $layoutTransform->transform($layout) 69 | ); 70 | } 71 | 72 | /** 73 | * Add a layout, which is a FieldsBuilder. `addLayout` can be chained to add 74 | * multiple layouts to the Flexible Content field. 75 | * @param string|FieldsBuilder $layout layout name. 76 | * Alternatively supply a FieldsBuilder to reuse existing fields. The name 77 | * will be inferred from the FieldsBuilder's name. 78 | * @param array $args filed configuration 79 | * @return FieldsBuilder 80 | */ 81 | public function addLayout($layout, $args = []) 82 | { 83 | if ($layout instanceof FieldsBuilder) { 84 | $layout = clone $layout; 85 | } else { 86 | $layout = new FieldsBuilder($layout, $args); 87 | } 88 | 89 | $layout = $this->initializeLayout($layout, $args); 90 | $this->pushLayout($layout); 91 | 92 | return $layout; 93 | } 94 | 95 | /** 96 | * Add multiple layouts either via an array or from another builder 97 | * @param FieldsBuilder|array $layouts 98 | * @return $this 99 | */ 100 | public function addLayouts($layouts) 101 | { 102 | foreach ($layouts as $layout) { 103 | $this->addLayout($layout); 104 | } 105 | return $this; 106 | } 107 | 108 | /** 109 | * Configures the layout FieldsBuilder 110 | * @param FieldsBuilder $layout 111 | * @param array $args FieldGroup Configuration 112 | * @return FieldsBuilder Configured Layout 113 | */ 114 | protected function initializeLayout(FieldsBuilder $layout, $args = []) 115 | { 116 | $layout->setGroupConfig('name', $layout->getName()); 117 | $layout->setGroupConfig('display', 'block'); 118 | 119 | foreach ($args as $key => $value) { 120 | $layout->setGroupConfig($key, $value); 121 | } 122 | 123 | $layout->setParentContext($this); 124 | 125 | return $layout; 126 | } 127 | 128 | /** 129 | * End the current Flexible Content field, return to parent context 130 | * @return Builder 131 | */ 132 | public function endFlexibleContent() 133 | { 134 | return $this->getParentContext(); 135 | } 136 | 137 | /** 138 | * Add layout to internal array 139 | * @param FieldsBuilder $layout 140 | * @return void 141 | */ 142 | protected function pushLayout($layout) 143 | { 144 | $this->layouts[] = $layout; 145 | } 146 | 147 | /** 148 | * @return FieldsBuilder[] 149 | */ 150 | public function getLayouts() 151 | { 152 | return $this->layouts; 153 | } 154 | 155 | /** 156 | * @return FieldsBuilder 157 | * @throws LayoutNotFoundException 158 | */ 159 | public function getLayout($name) 160 | { 161 | $layouts = array_filter($this->getLayouts(), static function(FieldsBuilder $layout) use ($name) { 162 | return $layout->getName() === $name; 163 | }); 164 | 165 | if (count($layouts) === 0) { 166 | throw new LayoutNotFoundException("Layout `{$name}` not found."); 167 | } 168 | 169 | return array_shift($layouts); 170 | } 171 | 172 | /** 173 | * Generates the default button label. 174 | * @return string 175 | */ 176 | private function getDefaultButtonLabel() 177 | { 178 | return 'Add ' . $this->singularize($this->getLabel()); 179 | } 180 | 181 | public function removeLayout($name) 182 | { 183 | if (!$this->layoutExists($name)) { 184 | throw new LayoutNotFoundException("Layout `{$name}` not found."); 185 | } 186 | 187 | $this->layouts = array_values(array_filter($this->getLayouts(), static function(FieldsBuilder $layout) use ($name) { 188 | return $layout->getName() !== $name; 189 | })); 190 | 191 | return $this; 192 | } 193 | 194 | public function layoutExists($name) 195 | { 196 | $layouts = array_filter($this->getLayouts(), static function(FieldsBuilder $layout) use ($name) { 197 | return $layout->getName() === $name; 198 | }); 199 | 200 | return count($layouts) !== 0; 201 | } 202 | 203 | /** 204 | * @param string $name 205 | * @param array $modify 206 | * @return $this 207 | * @throws FieldNotFoundException 208 | * @throws LayoutNotFoundException 209 | * @throws \Exception 210 | */ 211 | public function modifyField($name, $modify) 212 | { 213 | if ($this->hasDeeplyNestedField($name)) { 214 | $fieldNames = explode(FieldsBuilder::DEEP_NESTING_DELIMITER, $name, 2); 215 | $this->getLayout($fieldNames[0])->modifyField($fieldNames[1], $modify); 216 | return $this; 217 | } 218 | 219 | if ($this->layoutExists($name)) { 220 | // Modify layout's FieldsBuilder, update Group Config 221 | if (is_array($modify)) { 222 | $this->getLayout($name)->updateGroupConfig($modify); 223 | } elseif ($modify instanceof \Closure) { 224 | throw new \Exception('FieldsBuilder can\'t be modified with a closure.'); 225 | } 226 | } 227 | else if (is_array($modify)) { 228 | $this->updateConfig($modify); 229 | } elseif ($modify instanceof \Closure) { 230 | throw new \Exception('FlexibleContentBuilder can\'t be modified with a closure.'); 231 | } 232 | 233 | return $this; 234 | } 235 | 236 | public function removeField($name) 237 | { 238 | if ($this->hasDeeplyNestedField($name)) { 239 | $fieldNames = explode(FieldsBuilder::DEEP_NESTING_DELIMITER, $name, 2); 240 | $this->getLayout($fieldNames[0])->removeField($fieldNames[1]); 241 | return $this; 242 | } 243 | 244 | $this->removeLayout($name); 245 | return $this; 246 | } 247 | 248 | /** 249 | * @param string $name Deeply nested field name 250 | * @return bool 251 | */ 252 | private function hasDeeplyNestedField($name) 253 | { 254 | return strpos($name, FieldsBuilder::DEEP_NESTING_DELIMITER) > 0; 255 | } 256 | 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/GroupBuilder.php: -------------------------------------------------------------------------------- 1 | fieldsBuilder = new FieldsBuilder($name); 26 | $this->fieldsBuilder->setParentContext($this); 27 | } 28 | 29 | /** 30 | * Add multiple fields either via an array or from another builder 31 | * @param array|FieldsBuilder $fields 32 | * @return $this 33 | */ 34 | public function addFields($fields) 35 | { 36 | $this->fieldsBuilder->addFields($fields); 37 | return $this; 38 | } 39 | 40 | /** 41 | * Return a group field configuration array 42 | * @return array 43 | */ 44 | public function build() 45 | { 46 | $config = parent::build(); 47 | $fields = $this->fieldsBuilder->build(); 48 | $config['sub_fields'] = $fields['fields']; 49 | return $config; 50 | } 51 | 52 | /** 53 | * Returns call chain to parentContext 54 | * @return FieldBuilder 55 | */ 56 | public function endGroup() 57 | { 58 | return $this->getParentContext(); 59 | } 60 | 61 | /** 62 | * Returns call chain to parentContext 63 | * @return FieldBuilder 64 | */ 65 | public function end() 66 | { 67 | return $this->endGroup(); 68 | } 69 | 70 | /** 71 | * Intercept missing methods, pass any methods that begin with add to the 72 | * internal fieldsBuilder 73 | * @param string $method 74 | * @param array $args 75 | * @return mixed 76 | */ 77 | public function __call($method, $args) 78 | { 79 | if (preg_match('/^add.+/', $method) && method_exists($this->fieldsBuilder, $method)) { 80 | $field = $this->callAddFieldMethod($method, $args); 81 | $field->setParentContext($this); 82 | return $field; 83 | } 84 | 85 | return parent::__call($method, $args); 86 | } 87 | 88 | /** 89 | * Calls an add field method on the FieldsBuilder 90 | * @param string $method [description] 91 | * @param array $args 92 | * @return FieldBuilder 93 | */ 94 | private function callAddFieldMethod($method, $args) 95 | { 96 | return call_user_func_array([$this->fieldsBuilder, $method], $args); 97 | } 98 | 99 | /** 100 | * Remove a field by name 101 | * @param string $name Field to remove 102 | * @return $this 103 | */ 104 | public function removeField($name) 105 | { 106 | $this->fieldsBuilder->removeField($name); 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Modify an already defined field 113 | * @param string $name Name of the field 114 | * @param array|\Closure $modify Array of field configs or a closure that accepts 115 | * a FieldsBuilder and returns a FieldsBuilder. 116 | * @throws ModifyFieldReturnTypeException if $modify is a closure and doesn't 117 | * return a FieldsBuilder. 118 | * @throws FieldNotFoundException if the field name doesn't exist. 119 | * @return $this 120 | */ 121 | public function modifyField($name, $modify) 122 | { 123 | $this->fieldsBuilder->modifyField($name, $modify); 124 | 125 | return $this; 126 | } 127 | 128 | public function getField($name) 129 | { 130 | return $this->fieldsBuilder->getField($name); 131 | } 132 | 133 | public function fieldExists($name) { 134 | return $this->fieldsBuilder->fieldExists($name); 135 | } 136 | } -------------------------------------------------------------------------------- /src/LayoutNotFoundException.php: -------------------------------------------------------------------------------- 1 | $name, 21 | 'operator' => $operator, 22 | 'value' => $value, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ModifyFieldReturnTypeException.php: -------------------------------------------------------------------------------- 1 | parentContext = $builder; 29 | } 30 | 31 | /** 32 | * @return Builder 33 | */ 34 | public function getParentContext() 35 | { 36 | return $this->parentContext; 37 | } 38 | 39 | /** 40 | * Returns the root context 41 | * @return Builder 42 | */ 43 | public function getRootContext() 44 | { 45 | if ($parentContext = $this->getParentContext()) { 46 | if ($parentContext instanceof ParentDelegationBuilder) { 47 | return $parentContext->getRootContext(); 48 | } 49 | return $parentContext; 50 | } 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * If a method is missing, check to see if it exist on the $parentContext 57 | * and delegate the call to it. 58 | * @param string $method 59 | * @param array $args 60 | * @throws \Exception when a method is not found on the $parentContext 61 | * @return mixed 62 | */ 63 | public function __call($method, $args) 64 | { 65 | if ($this->parentContext) { 66 | return call_user_func_array([$this->parentContext, $method], $args); 67 | } 68 | 69 | throw new \Exception('No such function: '.$method); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/RepeaterBuilder.php: -------------------------------------------------------------------------------- 1 | setConfig('button_label', $this->getDefaultButtonLabel()); 30 | } 31 | } 32 | 33 | /** 34 | * Return a repeater field configuration array 35 | * @return array 36 | */ 37 | public function build() 38 | { 39 | $config = parent::build(); 40 | if (array_key_exists('collapsed', $config)) { 41 | $collapseField = $this->fieldsBuilder->getField($config['collapsed']); 42 | $fieldKey = $collapseField->getKey(); 43 | if ($collapseField->hasCustomKey()) { 44 | $config['collapsed'] = $fieldKey; 45 | $config['_has_custom_collapsed_key'] = true; 46 | } else { 47 | $fieldKey = preg_replace('/^field_/', '', $fieldKey); 48 | $config['collapsed'] = $this->getName() . '_' . $fieldKey; 49 | } 50 | } 51 | return $config; 52 | } 53 | 54 | /** 55 | * Returns call chain to parentContext 56 | * @return Builder 57 | */ 58 | public function endRepeater() 59 | { 60 | return $this->getParentContext(); 61 | } 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function end() 67 | { 68 | return $this->endRepeater(); 69 | } 70 | 71 | /** 72 | * Gerenates the default button label. 73 | * @return string 74 | */ 75 | private function getDefaultButtonLabel() 76 | { 77 | return 'Add ' . $this->singularize($this->getLabel()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/TabBuilder.php: -------------------------------------------------------------------------------- 1 | $this->generateLabel($name), 19 | ], $config); 20 | $name = $this->generateName($name).'_'.$type; 21 | 22 | parent::__construct($name, $type, $config); 23 | } 24 | 25 | public function endpoint() 26 | { 27 | return $this->setConfig('endpoint', 1); 28 | } 29 | 30 | public function removeEndpoint() 31 | { 32 | return $this->setConfig('endpoint', 0); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Traits/CanSingularize.php: -------------------------------------------------------------------------------- 1 | build()->singularize($value); 16 | } 17 | 18 | return \Doctrine\Common\Inflector\Inflector::singularize($value); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Transform/ConditionalField.php: -------------------------------------------------------------------------------- 1 | getBuilder()->fieldExists($value)){ 34 | return $this->getBuilder()->getField($value)->getKey(); 35 | } 36 | 37 | return $value; 38 | } 39 | 40 | public function transformConfig($config) 41 | { 42 | if ($this->getBuilder()->fieldExists($config['field']) && $this->getBuilder()->getField($config['field'])->hasCustomKey()) { 43 | $config['_has_custom_key'] = true; 44 | } else if (!$this->getBuilder()->fieldExists($config['field'])) { 45 | $config['_field_does_not_exist'] = $config['field']; 46 | } 47 | 48 | return $config; 49 | } 50 | 51 | public function shouldTransformValue($key, $config) 52 | { 53 | return parent::shouldTransformValue($key, $config) && !(array_key_exists('_has_custom_key', $config) && $config['_has_custom_key'] === true); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Transform/ConditionalLogic.php: -------------------------------------------------------------------------------- 1 | getBuilder()); 46 | return $conditionalFieldTransform->transform($value); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Transform/FlexibleContentLayout.php: -------------------------------------------------------------------------------- 1 | sub_fields 9 | * title => label 10 | */ 11 | class FlexibleContentLayout extends Transform 12 | { 13 | /** 14 | * @param array $config 15 | * @return array 16 | */ 17 | public function transform($config) 18 | { 19 | $config['sub_fields'] = $config['fields']; 20 | unset($config['fields']); 21 | 22 | if (!array_key_exists('label', $config)) { 23 | $config['label'] = $config['title']; 24 | } 25 | unset($config['title']); 26 | 27 | return $config; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Transform/IterativeTransform.php: -------------------------------------------------------------------------------- 1 | dontRecurseKeys, true); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Transform/NamespaceFieldKey.php: -------------------------------------------------------------------------------- 1 | secondTransformPass($config, $config); 36 | 37 | return $config; 38 | } 39 | 40 | /** 41 | * @param string $value Key 42 | * @return string Namedspaced key 43 | */ 44 | public function transformValue($value) 45 | { 46 | $namespace = 'field_'; 47 | $groupName = $this->getBuilder()->getName(); 48 | 49 | if ($groupName) { 50 | // remove field_ or group_ if already at the begining of the key 51 | $value = preg_replace('/^field_|^group_/', '', $value); 52 | $namespace .= str_replace(' ', '_', $groupName) . '_'; 53 | } 54 | return strtolower($namespace . $value); 55 | } 56 | 57 | public function shouldTransformValue($key, $config) 58 | { 59 | if ($key === 'field' && array_key_exists('_field_does_not_exist', $config)) { 60 | return false; 61 | } 62 | 63 | return parent::shouldTransformValue($key, $config) && !$this->hasCustomKey($key, $config) && !$this->hasCustomCollapsedKey($key, $config); 64 | } 65 | 66 | /** 67 | * @param $config 68 | * @return bool 69 | */ 70 | private function hasCustomKey($key, $config) 71 | { 72 | return ($key !== 'collapsed' && array_key_exists('_has_custom_key', $config) && $config['_has_custom_key'] === true); 73 | } 74 | 75 | /** 76 | * @param $config 77 | * @return bool 78 | */ 79 | private function hasCustomCollapsedKey($key, $config) 80 | { 81 | return ($key === 'collapsed' && array_key_exists('_has_custom_collapsed_key', $config) && $config['_has_custom_collapsed_key'] === true); 82 | } 83 | 84 | private function secondTransformPass(array $config, array $parentConfig) 85 | { 86 | foreach ($config as $key => &$value) { 87 | if (array_key_exists('_field_does_not_exist', $config)) { 88 | foreach ($parentConfig as $parentField) { 89 | if ( 90 | is_array($parentField) && 91 | array_key_exists('name', $parentField) && 92 | $parentField['name'] === $config['_field_does_not_exist'] 93 | ) { 94 | $config['field'] = $parentField['key']; 95 | } 96 | } 97 | } 98 | 99 | if ($this->shouldRecurse($value, $key)) { 100 | $value = $this->secondTransformPass($value, $parentConfig); 101 | } 102 | } 103 | 104 | return $config; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Transform/RecursiveTransform.php: -------------------------------------------------------------------------------- 1 | keys; 23 | } 24 | 25 | /** 26 | * Apply the `transformValue` function to all values in multidementional 27 | * associative array where the key matches one of the keys defined 28 | * on the RecursiveTransform. 29 | * @param array $config 30 | * @return array transformed config 31 | */ 32 | public function transform($config) 33 | { 34 | foreach ($config as $key => $value ) { 35 | if ($this->shouldTransformValue($key, $config)) { 36 | $config = $this->transformConfig($config); 37 | $config[$key] = $this->transformValue($value); 38 | } else { 39 | if ($this->shouldRecurse($value, $key)) { 40 | $config[$key] = $this->transform($value); 41 | } 42 | } 43 | } 44 | 45 | return $config; 46 | } 47 | 48 | 49 | /** 50 | * @param string $key 51 | * @param array $config 52 | * @return bool 53 | */ 54 | public function shouldTransformValue($key, $config) 55 | { 56 | return in_array($key, $this->getKeys(), true); 57 | } 58 | 59 | /** 60 | * Based upon the value or key, determine if the transform function 61 | * should recurse. 62 | * @param $value 63 | * @param string $key 64 | * @return bool 65 | */ 66 | protected function shouldRecurse($value, $key) 67 | { 68 | return is_array($value); 69 | } 70 | 71 | /** 72 | * Impelment this in all discrete classes 73 | * @param mixed $value input 74 | * @return mixed output value 75 | */ 76 | abstract public function transformValue($value); 77 | 78 | 79 | /** 80 | * @param array $config 81 | * @return array 82 | */ 83 | public function transformConfig($config) 84 | { 85 | return $config; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Transform/Transform.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 24 | } 25 | 26 | /** 27 | * @return Builder 28 | */ 29 | public function getBuilder() 30 | { 31 | return $this->builder; 32 | } 33 | 34 | /** 35 | * Impelment in all discrete classes 36 | * @param array $config input 37 | * @return array output config 38 | */ 39 | abstract public function transform($config); 40 | } 41 | -------------------------------------------------------------------------------- /tests/AccordionBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(class_exists('StoutLogic\AcfBuilder\AccordionBuilder')); 16 | } 17 | 18 | public function testCreateTabBuilder() 19 | { 20 | $subject = new AccordionBuilder('settings'); 21 | $this->assertArraySubset([ 22 | 'key' => 'field_settings_accordion', 23 | 'name' => 'settings_accordion', 24 | 'label' => 'Settings', 25 | 'type' => 'accordion', 26 | ], $subject->build()); 27 | } 28 | 29 | public function testEndpoint() 30 | { 31 | $subject = new AccordionBuilder('settings'); 32 | $this->assertSame($subject, $subject->endpoint()); 33 | $this->assertArraySubset([ 34 | 'key' => 'field_settings_accordion', 35 | 'name' => 'settings_accordion', 36 | 'label' => 'Settings', 37 | 'type' => 'accordion', 38 | 'endpoint' => 1, 39 | ], $subject->build()); 40 | 41 | $this->assertSame($subject, $subject->removeEndpoint()); 42 | $this->assertArraySubset([ 43 | 'key' => 'field_settings_accordion', 44 | 'name' => 'settings_accordion', 45 | 'label' => 'Settings', 46 | 'type' => 'accordion', 47 | 'endpoint' => 0, 48 | ], $subject->build()); 49 | } 50 | 51 | public function testOpen() 52 | { 53 | $subject = new AccordionBuilder('settings'); 54 | $this->assertArraySubset([ 55 | 'open' => 1, 56 | 'type' => 'accordion', 57 | ], $subject 58 | ->setOpen() 59 | ->build() 60 | ); 61 | 62 | $this->assertArraySubset([ 63 | 'open' => 1, 64 | 'type' => 'accordion', 65 | ], $subject 66 | ->setOpen(true) 67 | ->build() 68 | ); 69 | 70 | $this->assertArraySubset([ 71 | 'open' => 0, 72 | 'type' => 'accordion', 73 | ], $subject 74 | ->setOpen(false) 75 | ->build() 76 | ); 77 | } 78 | 79 | public function testMultiExpand() 80 | { 81 | $subject = new AccordionBuilder('settings'); 82 | $this->assertArraySubset([ 83 | 'multi_expand' => 1, 84 | 'type' => 'accordion', 85 | ], $subject 86 | ->setMultiExpand() 87 | ->build() 88 | ); 89 | 90 | $this->assertArraySubset([ 91 | 'multi_expand' => 1, 92 | 'type' => 'accordion', 93 | ], $subject 94 | ->setMultiExpand(true) 95 | ->build() 96 | ); 97 | 98 | $this->assertArraySubset([ 99 | 'multi_expand' => 0, 100 | 'type' => 'accordion', 101 | ], $subject 102 | ->setMultiExpand(false) 103 | ->build() 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/ChoiceFieldBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(class_exists('StoutLogic\AcfBuilder\ChoiceFieldBuilder')); 16 | } 17 | 18 | public function testAddChoice() 19 | { 20 | $subject = new ChoiceFieldBuilder('choice', 'radio'); 21 | $this->assertSame($subject, $subject->addChoice('red')); 22 | $this->assertArraySubset([ 23 | 'choices' => [ 24 | 'red' => 'red', 25 | ] 26 | ], $subject->build()); 27 | } 28 | 29 | public function testAddChoiceInConstructor() 30 | { 31 | $subject = new ChoiceFieldBuilder('choice', 'radio', [ 32 | 'choices' => [ 33 | 'red', 34 | ['blue' => 'Blue'], 35 | ] 36 | ]); 37 | $this->assertArraySubset([ 38 | 'choices' => [ 39 | 'red' => 'red', 40 | 'blue' => 'Blue', 41 | ] 42 | ], $subject->build()); 43 | } 44 | 45 | public function testAddChoiceInAdditionToConstructor() 46 | { 47 | $subject = new ChoiceFieldBuilder('choice', 'radio', [ 48 | 'choices' => [ 49 | 'red', 50 | ['blue' => 'Blue'], 51 | ] 52 | ]); 53 | $this->assertSame($subject, $subject->addChoice('green', 'Green')); 54 | $this->assertArraySubset([ 55 | 'choices' => [ 56 | 'red' => 'red', 57 | 'blue' => 'Blue', 58 | 'green' => 'Green', 59 | ] 60 | ], $subject->build()); 61 | } 62 | 63 | public function testSetChoices() 64 | { 65 | $subject = new ChoiceFieldBuilder('choice', 'radio', [ 66 | 'choices' => [ 67 | 'red', 68 | ['blue' => 'Blue'], 69 | ] 70 | ]); 71 | $this->assertSame($subject, $subject->setChoices('green', ['yellow' => 'Yellow'])); 72 | $this->assertArraySubset([ 73 | 'choices' => [ 74 | 'green' => 'green', 75 | 'yellow' => 'Yellow', 76 | ] 77 | ], $subject->build()); 78 | } 79 | 80 | public function testConfigInChoicesWithLabels() 81 | { 82 | $subject = new ChoiceFieldBuilder('choice', 'radio', [ 83 | 'label' => 'Are you finished?', 84 | 'allow_null' => true, 85 | 'required' => true, 86 | 'choices' => [ 87 | 'yes' => 'Yes, please send my report for review!', 88 | 'no' => 'No, save my report for later completion.', 89 | ] 90 | ]); 91 | 92 | $config = $subject->build(); 93 | $this->assertArraySubset([ 94 | 'choices' => [ 95 | 'yes' => 'Yes, please send my report for review!', 96 | 'no' => 'No, save my report for later completion.', 97 | ] 98 | ], $config); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/ConditionalBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'color', 21 | 'operator' => '==', 22 | 'value' => 'other', 23 | ], 24 | ] 25 | ]; 26 | 27 | $this->assertArraySubset($expectedConfig, $builder->build()); 28 | } 29 | 30 | public function testAnd() 31 | { 32 | $builder = new ConditionalBuilder('color', '==', 'other'); 33 | $builder->and('number', '!=', ''); 34 | 35 | $expectedConfig = [ 36 | [ 37 | [ 38 | 'field' => 'color', 39 | 'operator' => '==', 40 | 'value' => 'other', 41 | ], 42 | [ 43 | 'field' => 'number', 44 | 'operator' => '!=', 45 | 'value' => '', 46 | ], 47 | ] 48 | ]; 49 | 50 | $this->assertArraySubset($expectedConfig, $builder->build()); 51 | } 52 | 53 | public function testOr() 54 | { 55 | $builder = new ConditionalBuilder('color', '==', 'other'); 56 | $builder->or('number', '>', '5') 57 | ->and('number', '<', '10') 58 | ->and('color', '!=', 'other'); 59 | 60 | $expectedConfig = [ 61 | [ 62 | [ 63 | 'field' => 'color', 64 | 'operator' => '==', 65 | 'value' => 'other', 66 | ], 67 | ], 68 | [ 69 | [ 70 | 'field' => 'number', 71 | 'operator' => '>', 72 | 'value' => '5', 73 | ], 74 | [ 75 | 'field' => 'number', 76 | 'operator' => '<', 77 | 'value' => '10', 78 | ], 79 | [ 80 | 'field' => 'color', 81 | 'operator' => '!=', 82 | 'value' => 'other', 83 | ], 84 | ], 85 | ]; 86 | 87 | $this->assertArraySubset($expectedConfig, $builder->build()); 88 | } 89 | } -------------------------------------------------------------------------------- /tests/FieldBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(class_exists('StoutLogic\AcfBuilder\FieldBuilder')); 16 | } 17 | 18 | public function testGetName() 19 | { 20 | $subject = new FieldBuilder('my_field', 'text'); 21 | $this->assertSame('my_field', $subject->getName()); 22 | } 23 | 24 | public function testBuild() 25 | { 26 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 27 | $this->assertArraySubset([ 28 | 'key' => 'field_my_field', 29 | 'name' => 'my_field', 30 | 'label' => 'My Field', 31 | 'type' => 'text', 32 | 'prepend' => '$', 33 | ], $subject->build()); 34 | } 35 | 36 | public function testSetKey() 37 | { 38 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 39 | $this->assertSame($subject, $subject->setKey('field_new_key')); 40 | $this->assertArraySubset([ 41 | 'key' => 'field_new_key', 42 | 'name' => 'my_field', 43 | 'label' => 'My Field', 44 | 'type' => 'text', 45 | 'prepend' => '$', 46 | ], $subject->build()); 47 | } 48 | 49 | public function testSetKeyWithOutFieldPrepended() 50 | { 51 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 52 | $this->assertSame($subject, $subject->setKey('new_key')); 53 | $this->assertArraySubset([ 54 | 'key' => 'field_new_key', 55 | 'name' => 'my_field', 56 | 'label' => 'My Field', 57 | 'type' => 'text', 58 | 'prepend' => '$', 59 | ], $subject->build()); 60 | } 61 | 62 | public function testSetConfig() 63 | { 64 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 65 | $this->assertSame($subject, $subject->setConfig('prepend', '@')); 66 | $this->assertArraySubset([ 67 | 'key' => 'field_my_field', 68 | 'name' => 'my_field', 69 | 'label' => 'My Field', 70 | 'type' => 'text', 71 | 'prepend' => '@', 72 | ], $subject->build()); 73 | } 74 | 75 | public function testUpdateConfig() 76 | { 77 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 78 | $this->assertSame($subject, $subject->updateConfig([ 79 | 'prepend' => '@', 80 | 'label' => 'My New Label', 81 | ])); 82 | $this->assertArraySubset([ 83 | 'key' => 'field_my_field', 84 | 'name' => 'my_field', 85 | 'label' => 'My New Label', 86 | 'type' => 'text', 87 | 'prepend' => '@', 88 | ], $subject->build()); 89 | } 90 | 91 | public function testSetRequired() 92 | { 93 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 94 | $this->assertSame($subject, $subject->setRequired()); 95 | $this->assertArraySubset([ 96 | 'key' => 'field_my_field', 97 | 'name' => 'my_field', 98 | 'label' => 'My Field', 99 | 'type' => 'text', 100 | 'prepend' => '$', 101 | 'required' => 1, 102 | ], $subject->build()); 103 | 104 | $this->assertSame($subject, $subject->setUnrequired()); 105 | $this->assertArraySubset([ 106 | 'key' => 'field_my_field', 107 | 'name' => 'my_field', 108 | 'label' => 'My Field', 109 | 'type' => 'text', 110 | 'prepend' => '$', 111 | 'required' => 0, 112 | ], $subject->build()); 113 | 114 | $this->assertSame($subject, $subject->setRequired()); 115 | $this->assertArraySubset([ 116 | 'key' => 'field_my_field', 117 | 'name' => 'my_field', 118 | 'label' => 'My Field', 119 | 'type' => 'text', 120 | 'prepend' => '$', 121 | 'required' => 1, 122 | ], $subject->build()); 123 | } 124 | 125 | public function testSetLabel() 126 | { 127 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 128 | $this->assertSame($subject, $subject->setLabel('My Label')); 129 | $this->assertArraySubset([ 130 | 'key' => 'field_my_field', 131 | 'name' => 'my_field', 132 | 'label' => 'My Label', 133 | 'type' => 'text', 134 | 'prepend' => '$', 135 | ], $subject->build()); 136 | } 137 | 138 | public function testSetInstructions() 139 | { 140 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 141 | $this->assertSame($subject, $subject->setInstructions('My Instructions')); 142 | $this->assertArraySubset([ 143 | 'key' => 'field_my_field', 144 | 'name' => 'my_field', 145 | 'label' => 'My Field', 146 | 'type' => 'text', 147 | 'prepend' => '$', 148 | 'instructions' => 'My Instructions', 149 | ], $subject->build()); 150 | } 151 | 152 | public function testSetDefaultValue() 153 | { 154 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 155 | $this->assertSame($subject, $subject->setDefaultValue('My Default')); 156 | $this->assertArraySubset([ 157 | 'key' => 'field_my_field', 158 | 'name' => 'my_field', 159 | 'label' => 'My Field', 160 | 'type' => 'text', 161 | 'prepend' => '$', 162 | 'default_value' => 'My Default', 163 | ], $subject->build()); 164 | } 165 | 166 | public function testGetWrapper() 167 | { 168 | $wrapper = [ 169 | 'class' => 'foo', 170 | 'id' => 'bar', 171 | ]; 172 | $subject = new FieldBuilder('my_field', 'text', ['wrapper' => $wrapper]); 173 | $this->assertSame($subject, $subject->setConfig('prepend', '@')); 174 | $this->assertArraySubset($wrapper, $subject->getWrapper()); 175 | } 176 | 177 | public function testSetWrapper() 178 | { 179 | $subject = new FieldBuilder('my_field', 'text'); 180 | $this->assertSame($subject, $subject->setWrapper(['class' => 'foo', 'id' => 'bar'])); 181 | $this->assertArraySubset([ 182 | 'key' => 'field_my_field', 183 | 'name' => 'my_field', 184 | 'label' => 'My Field', 185 | 'type' => 'text', 186 | 'wrapper' => [ 187 | 'class' => 'foo', 188 | 'id' => 'bar', 189 | ], 190 | ], $subject->build()); 191 | } 192 | 193 | public function testSetWidth() 194 | { 195 | $subject = new FieldBuilder('my_field', 'text'); 196 | $this->assertSame($subject, $subject->setWidth('50%')); 197 | $this->assertArraySubset([ 198 | 'key' => 'field_my_field', 199 | 'name' => 'my_field', 200 | 'label' => 'My Field', 201 | 'type' => 'text', 202 | 'wrapper' => [ 203 | 'width' => '50%', 204 | ], 205 | ], $subject->build()); 206 | } 207 | 208 | public function testSetAttr() 209 | { 210 | $subject = new FieldBuilder('my_field', 'text'); 211 | $this->assertSame($subject, $subject->setAttr('data-my_attr', 'My Attr')); 212 | $this->assertArraySubset([ 213 | 'key' => 'field_my_field', 214 | 'name' => 'my_field', 215 | 'label' => 'My Field', 216 | 'type' => 'text', 217 | 'wrapper' => [ 218 | 'data-my_attr' => 'My Attr', 219 | ], 220 | ], $subject->build()); 221 | } 222 | 223 | public function testSetSelector() 224 | { 225 | $subject = new FieldBuilder('my_field', 'text'); 226 | // returns FieldBuilder. 227 | $this->assertSame($subject, $subject->setSelector('.my-class')); 228 | 229 | // only id. 230 | $subject->setSelector('#my-id'); 231 | $this->assertArraySubset([ 232 | 'id' => 'my-id', 233 | ], $subject->getWrapper()); 234 | 235 | // only class. 236 | $subject->setSelector('.my-class'); 237 | $this->assertArraySubset([ 238 | 'class' => 'my-class', 239 | ], $subject->getWrapper()); 240 | 241 | // only class multiple. 242 | $subject->setSelector('.class1.class2'); 243 | $this->assertArraySubset([ 244 | 'class' => 'class1 class2', 245 | ], $subject->getWrapper()); 246 | 247 | // id / class. 248 | $subject->setSelector('#my-id.my-class'); 249 | $this->assertArraySubset([ 250 | 'id' => 'my-id', 251 | 'class' => 'my-class', 252 | ], $subject->getWrapper()); 253 | 254 | // id / class multiple. 255 | $subject->setSelector('#my-id.my-class.more-class'); 256 | $this->assertArraySubset([ 257 | 'id' => 'my-id', 258 | 'class' => 'my-class more-class', 259 | ], $subject->getWrapper()); 260 | 261 | // class /id. 262 | $subject->setSelector('.my-class#my-id'); 263 | $this->assertArraySubset([ 264 | 'id' => 'my-id', 265 | 'class' => 'my-class', 266 | ], $subject->getWrapper()); 267 | 268 | // class multiple /id. 269 | $subject->setSelector('.my-class.more-class#my-id'); 270 | $this->assertArraySubset([ 271 | 'id' => 'my-id', 272 | 'class' => 'my-class more-class', 273 | ], $subject->getWrapper()); 274 | } 275 | 276 | public function testConditional() 277 | { 278 | $subject = new FieldBuilder('my_field', 'text', ['prepend' => '$']); 279 | $this->assertNotSame($subject, $subject->conditional('other_field', '==', '1')); 280 | 281 | $this->assertArraySubset([ 282 | 'key' => 'field_my_field', 283 | 'name' => 'my_field', 284 | 'label' => 'My Field', 285 | 'type' => 'text', 286 | 'prepend' => '$', 287 | 'conditional_logic' => [ 288 | [ 289 | [ 290 | 'field' => 'other_field', 291 | 'operator' => '==', 292 | 'value' => '1', 293 | ], 294 | ] 295 | ], 296 | ], $subject->build()); 297 | } 298 | 299 | public function testSetCustomKey() 300 | { 301 | $subject = new FieldBuilder('my_field', 'text'); 302 | $subject->setCustomKey('129384192384912384'); 303 | 304 | $this->assertArraySubset([ 305 | 'key' => '129384192384912384', 306 | '_has_custom_key' => true, 307 | 'name' => 'my_field', 308 | 'label' => 'My Field', 309 | 'type' => 'text', 310 | ], $subject->build()); 311 | 312 | $this->assertArrayHasKey('_has_custom_key', $subject->build()); 313 | $this->assertTrue($subject->hasCustomKey()); 314 | } 315 | 316 | 317 | public function testNotCustomKey() 318 | { 319 | $subject = new FieldBuilder('my_field', 'text'); 320 | $this->assertArrayNotHasKey('_has_custom_key', $subject->build()); 321 | $this->assertFalse($subject->hasCustomKey()); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /tests/FieldManagerTest.php: -------------------------------------------------------------------------------- 1 | testFields['test1'] = new FieldBuilder('test1', 'text'); 17 | $this->testFields['test2'] = new FieldBuilder('test2', 'text'); 18 | $this->testFields['test3'] = new FieldBuilder('test3', 'text'); 19 | $this->testFields['test4'] = new FieldBuilder('test4', 'text'); 20 | } 21 | 22 | public function testPushField() 23 | { 24 | $subject = new FieldManager(); 25 | 26 | $subject->pushField($this->testFields['test2']); 27 | 28 | $this->assertSame([ 29 | $this->testFields['test2'], 30 | ], $subject->getFields()); 31 | } 32 | 33 | public function testPushFieldWithExistingField() 34 | { 35 | $subject = new FieldManager([$this->testFields['test1']]); 36 | 37 | $subject->pushField($this->testFields['test2']); 38 | 39 | $this->assertSame([ 40 | $this->testFields['test1'], 41 | $this->testFields['test2'], 42 | ], $subject->getFields()); 43 | } 44 | 45 | public function testInsertingFields() 46 | { 47 | $subject = new FieldManager([ 48 | $this->testFields['test1'], 49 | $this->testFields['test2'], 50 | ]); 51 | 52 | $subject->insertFields($this->testFields['test3'], 1); 53 | 54 | $this->assertSame([ 55 | $this->testFields['test1'], 56 | $this->testFields['test3'], 57 | $this->testFields['test2'], 58 | ], $subject->getFields()); 59 | } 60 | 61 | public function testGetFieldCount() 62 | { 63 | $subject = new FieldManager([ 64 | $this->testFields['test1'], 65 | $this->testFields['test2'], 66 | $this->testFields['test3'], 67 | ]); 68 | 69 | $this->assertEquals(3, $subject->getCount()); 70 | } 71 | 72 | public function testRemovingField() 73 | { 74 | $subject = new FieldManager([ 75 | $this->testFields['test1'], 76 | $this->testFields['test2'], 77 | $this->testFields['test3'], 78 | ]); 79 | 80 | $subject->removeField('test2'); 81 | 82 | $this->assertEquals(2, $subject->getCount()); 83 | $this->assertSame([ 84 | $this->testFields['test1'], 85 | $this->testFields['test3'], 86 | ], $subject->getFields()); 87 | } 88 | 89 | public function testRemovingFieldNotFound() 90 | { 91 | $this->expectException(FieldNotFoundException::class); 92 | 93 | $subject = new FieldManager([ 94 | $this->testFields['test1'], 95 | $this->testFields['test2'], 96 | $this->testFields['test3'], 97 | ]); 98 | 99 | $subject->removeField('test4'); 100 | } 101 | 102 | public function testPopField() 103 | { 104 | $subject = new FieldManager([ 105 | $this->testFields['test1'], 106 | $this->testFields['test2'], 107 | ]); 108 | 109 | $this->assertSame($this->testFields['test2'], $subject->popField()); 110 | $this->assertEquals(1, $subject->getCount()); 111 | } 112 | 113 | public function testPopAndPushField() 114 | { 115 | $subject = new FieldManager([ 116 | $this->testFields['test1'], 117 | ]); 118 | 119 | $field = $subject->popField(); 120 | $subject->pushField($field); 121 | 122 | $this->assertSame([ 123 | $this->testFields['test1'], 124 | ], $subject->getFields()); 125 | $this->assertEquals(1, $subject->getCount()); 126 | } 127 | 128 | 129 | public function testPopFieldOnEmptyManter() 130 | { 131 | $this->expectException(\OutOfRangeException::class); 132 | $subject = new FieldManager(); 133 | 134 | $subject->popField(); 135 | } 136 | 137 | public function testReplaceField() 138 | { 139 | $subject = new FieldManager([ 140 | $this->testFields['test1'], 141 | $this->testFields['test2'], 142 | $this->testFields['test3'], 143 | ]); 144 | 145 | $subject->replaceField('test2', $this->testFields['test4']); 146 | $this->assertEquals(3, $subject->getCount()); 147 | $this->assertSame([ 148 | $this->testFields['test1'], 149 | $this->testFields['test4'], 150 | $this->testFields['test3'], 151 | ], $subject->getFields()); 152 | } 153 | 154 | public function testReplaceFieldWithMultipleFields() 155 | { 156 | $subject = new FieldManager([ 157 | $this->testFields['test1'], 158 | $this->testFields['test3'] 159 | ]); 160 | 161 | $subject->replaceField('test1', [ 162 | $this->testFields['test2'], 163 | $this->testFields['test4'], 164 | ]); 165 | 166 | $this->assertEquals(3, $subject->getCount()); 167 | $this->assertSame([ 168 | $this->testFields['test2'], 169 | $this->testFields['test4'], 170 | $this->testFields['test3'], 171 | ], $subject->getFields()); 172 | } 173 | 174 | public function testFieldNameExists() 175 | { 176 | $subject = new FieldManager([ 177 | $this->testFields['test1'], 178 | $this->testFields['test2'], 179 | $this->testFields['test3'], 180 | ]); 181 | 182 | $this->assertTrue($subject->fieldNameExists('test2')); 183 | $this->assertFalse($subject->fieldNameExists('test4')); 184 | } 185 | 186 | public function testGetField() 187 | { 188 | $subject = new FieldManager([ 189 | $this->testFields['test1'], 190 | $this->testFields['test2'], 191 | $this->testFields['test3'], 192 | ]); 193 | 194 | $this->assertSame($this->testFields['test3'], $subject->getField('test3')); 195 | } 196 | 197 | public function testModifyField() 198 | { 199 | $subject = new FieldManager([ 200 | $this->testFields['test1'], 201 | ]); 202 | 203 | $subject->modifyField('test1', ['label' => 'new label']); 204 | 205 | $this->assertEquals([ 206 | 'key' => 'field_test1', 207 | 'name' => 'test1', 208 | 'label' => 'new label', 209 | 'type' => 'text', 210 | ], $subject->getField('test1')->build()); 211 | } 212 | 213 | public function testValidateFieldName() 214 | { 215 | $this->expectException(FieldNameCollisionException::class); 216 | 217 | $subject = new FieldManager([ 218 | $this->testFields['test1'], 219 | $this->testFields['test2'], 220 | $this->testFields['test3'], 221 | ]); 222 | 223 | $subject->pushField($this->testFields['test1']); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /tests/FieldNameCollisionExceptionTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(class_exists('StoutLogic\AcfBuilder\FieldNameCollisionException')); 14 | } 15 | 16 | public function testExceptionThrownDuringFieldNameCollision() 17 | { 18 | $this->expectException(FieldNameCollisionException::class); 19 | $builder = new FieldsBuilder('Banner'); 20 | $builder 21 | ->addText('title') 22 | ->addWysiwyg('content') 23 | ->addTextarea('content'); 24 | } 25 | 26 | public function testExceptionThrownDuringFieldNameCollisionUsingRepeaters() 27 | { 28 | $this->expectException(FieldNameCollisionException::class); 29 | $builder = new FieldsBuilder('Banner'); 30 | $builder 31 | ->addText('title') 32 | ->addRepeater('slides') 33 | ->addRadio('slides') 34 | ->addChoices(1, 2, 3, 4) 35 | ->endRepeater() 36 | ->addRadio('slides') 37 | ->addChoices(1, 2, 3, 4); 38 | } 39 | 40 | public function testExceptionThrownDuringFieldNameCollisionUsingFlexibleContent() 41 | { 42 | $this->expectException(FieldNameCollisionException::class); 43 | $builder = new FieldsBuilder('Banner'); 44 | $builder 45 | ->addText('title') 46 | ->addFlexibleContent('content') 47 | ->addLayout('copy') 48 | ->addWysiwyg('content') 49 | ->addLayout('gallery') 50 | ->addRepeater('images') 51 | ->addImage('image') 52 | ->endFlexibleContent() 53 | ->addWysiwyg('content'); 54 | } 55 | 56 | public function testExceptionThrownDuringFieldNameCollisionUsingAddFields() 57 | { 58 | $this->expectException(FieldNameCollisionException::class); 59 | $builder = new FieldsBuilder('Banner'); 60 | $builder 61 | ->addText('title') 62 | ->addWysiwyg('content'); 63 | 64 | $builder2 = new FieldsBuilder('Section'); 65 | $builder2 66 | ->addText('headline') 67 | ->addWysiwyg('content'); 68 | 69 | $builder->addFields($builder2); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/FieldsBuilderCustomFieldKeysTest.php: -------------------------------------------------------------------------------- 1 | addText('title') 17 | ->setCustomKey('field_234123412341234'); 18 | 19 | $this->assertArraySubset([ 20 | 'name' => 'title', 21 | 'key' => 'field_234123412341234' 22 | ], $builder->build()); 23 | } 24 | 25 | public function testConditional() 26 | { 27 | $builder = (new FieldsBuilder('banner')); 28 | $builder 29 | ->addTrueFalse('enable') 30 | ->setCustomKey('234123412341234') 31 | ->addText('title') 32 | ->conditional('enable', '==', '1'); 33 | 34 | $this->assertArraySubset([ 35 | 'fields' => [ 36 | [ 37 | 'key' => '234123412341234', 38 | ], 39 | [ 40 | 'name' => 'title', 41 | 'conditional_logic' => [ 42 | [ 43 | [ 44 | 'field' => '234123412341234', 45 | 'operator' => '==', 46 | 'value' => '1', 47 | 48 | ], 49 | ], 50 | ], 51 | ], 52 | ] 53 | ], $builder->build()); 54 | } 55 | 56 | public function testFlexibleContent() 57 | { 58 | $builder = new FieldsBuilder('page_content'); 59 | $builder->addFlexibleContent('sections') 60 | ->addLayout('banner') 61 | ->addText('title') 62 | ->setCustomKey('my_custom_key') 63 | ->addWysiwyg('content') 64 | ->addLayout('content_columns') 65 | ->addRepeater('columns', ['min' => 1, 'max' => 2]) 66 | ->addWysiwyg('content'); 67 | 68 | $expectedConfig = [ 69 | 'fields' => [ 70 | [ 71 | 'key' => 'field_page_content_sections', 72 | 'name' => 'sections', 73 | 'label' => 'Sections', 74 | 'type' => 'flexible_content', 75 | 'button_label' => 'Add Section', 76 | 'layouts' => [ 77 | [ 78 | 'key' => 'field_page_content_sections_banner', 79 | 'name' => 'banner', 80 | 'label' => 'Banner', 81 | 'display' => 'block', 82 | 'sub_fields' => [ 83 | [ 84 | 'key' => 'my_custom_key', 85 | 'name' => 'title', 86 | 'type' => 'text', 87 | ], 88 | [ 89 | 'key' => 'field_page_content_sections_banner_content', 90 | 'name' => 'content', 91 | 'type' => 'wysiwyg', 92 | ] 93 | ] 94 | ], 95 | [ 96 | 'key' => 'field_page_content_sections_content_columns', 97 | 'name' => 'content_columns', 98 | 'label' => 'Content Columns', 99 | 'display' => 'block', 100 | 'sub_fields' => [ 101 | [ 102 | 'key' => 'field_page_content_sections_content_columns_columns', 103 | 'name' => 'columns', 104 | 'type' => 'repeater', 105 | 'min' => 1, 106 | 'max' => 2, 107 | 'sub_fields' => [ 108 | [ 109 | 'key' => 'field_page_content_sections_content_columns_columns_content', 110 | 'name' => 'content', 111 | 'type' => 'wysiwyg', 112 | ], 113 | ], 114 | ], 115 | ], 116 | ], 117 | ], 118 | ], 119 | ], 120 | ]; 121 | $this->assertArraySubset($expectedConfig, $builder->build()); 122 | } 123 | 124 | public function testRepeaterCollapse() 125 | { 126 | $builder = (new FieldsBuilder('banner')); 127 | $builder 128 | ->addRepeater('slides', ['collapsed' => 'title']) 129 | ->addText('title') 130 | ->setCustomKey('custom_title_key') 131 | ->addWysiwyg('content'); 132 | 133 | $this->assertArraySubset([ 134 | 'fields' => [ 135 | [ 136 | 'name' => 'slides', 137 | 'collapsed' => 'custom_title_key', 138 | 'sub_fields' => [ 139 | [ 140 | 'name' => 'title', 141 | 'key' => 'custom_title_key', 142 | ], 143 | [ 144 | 'name' => 'content', 145 | 'key' => 'field_banner_slides_content', 146 | ] 147 | ] 148 | ], 149 | ] 150 | ], $builder->build()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/FlexibleContentBuilderTest.php: -------------------------------------------------------------------------------- 1 | addLayout('banner') 18 | ->addText('title') 19 | ->addWysiwyg('content') 20 | ->addLayout('content_columns') 21 | ->addRepeater('columns', ['min' => 1, 'max' => 2]) 22 | ->addWysiwyg('content'); 23 | 24 | $expectedConfig = [ 25 | 'key' => 'field_content_areas', 26 | 'name' => 'content_areas', 27 | 'label' => 'Content Areas', 28 | 'type' => 'flexible_content', 29 | 'button_label' => 'Add Content Area', 30 | 'layouts' => [ 31 | [ 32 | 'key' => 'field_content_areas_banner', 33 | 'name' => 'banner', 34 | 'label' => 'Banner', 35 | 'display' => 'block', 36 | 'sub_fields' => [ 37 | [ 38 | 'key' => 'field_content_areas_banner_title', 39 | 'name' => 'title', 40 | 'type' => 'text', 41 | ], 42 | [ 43 | 'key' => 'field_content_areas_banner_content', 44 | 'name' => 'content', 45 | 'type' => 'wysiwyg', 46 | ] 47 | ] 48 | ], 49 | [ 50 | 'key' => 'field_content_areas_content_columns', 51 | 'name' => 'content_columns', 52 | 'label' => 'Content Columns', 53 | 'display' => 'block', 54 | 'sub_fields' => [ 55 | [ 56 | 'key' => 'field_content_areas_content_columns_columns', 57 | 'name' => 'columns', 58 | 'type' => 'repeater', 59 | 'min' => 1, 60 | 'max' => 2, 61 | 'sub_fields' => [ 62 | [ 63 | 'key' => 'field_content_areas_content_columns_columns_content', 64 | 'name' => 'content', 65 | 'type' => 'wysiwyg', 66 | ], 67 | ], 68 | ], 69 | ], 70 | ], 71 | ], 72 | ]; 73 | $this->assertArraySubset($expectedConfig, $builder->build()); 74 | $this->assertArrayNotHasKey('fields', $builder->build()); 75 | } 76 | 77 | public function testAddingFieldsBuilderAsLayout() 78 | { 79 | $banner = $this->getMockBuilder('StoutLogic\AcfBuilder\FieldsBuilder') 80 | ->setConstructorArgs(['parent']) 81 | ->getMock(); 82 | 83 | $banner->expects($this->once())->method('build')->willReturn([ 84 | 'key' => 'group_banner', 85 | 'name' => 'banner', 86 | 'title' => 'Banner', 87 | 'display' => 'block', 88 | 'fields' => [ 89 | [ 90 | 'key' => 'field_banner_title', 91 | 'name' => 'title', 92 | 'type' => 'text', 93 | ], 94 | [ 95 | 'name' => 'content', 96 | 'type' => 'wysiwyg', 97 | ] 98 | ] 99 | ]); 100 | 101 | $builder = new FlexibleContentBuilder('content_areas'); 102 | $builder->addLayout($banner); 103 | 104 | $expectedConfig = [ 105 | 'key' => 'field_content_areas', 106 | 'name' => 'content_areas', 107 | 'label' => 'Content Areas', 108 | 'type' => 'flexible_content', 109 | 'button_label' => 'Add Content Area', 110 | 'layouts' => [ 111 | [ 112 | 'key' => 'field_content_areas_banner', 113 | 'name' => 'banner', 114 | 'label' => 'Banner', 115 | 'display' => 'block', 116 | 'sub_fields' => [ 117 | [ 118 | 'key' => 'field_content_areas_banner_title', 119 | 'name' => 'title', 120 | 'type' => 'text', 121 | ], 122 | [ 123 | 'name' => 'content', 124 | 'type' => 'wysiwyg', 125 | ] 126 | ] 127 | ], 128 | ] 129 | ]; 130 | 131 | $config = $builder->build(); 132 | $this->assertArraySubset($expectedConfig, $config); 133 | } 134 | 135 | public function testEndFlexibleContent() 136 | { 137 | $fieldsBuilder = $this->getMockBuilder('StoutLogic\AcfBuilder\FieldsBuilder') 138 | ->setConstructorArgs(['parent']) 139 | ->getMock(); 140 | 141 | $builder = new FlexibleContentBuilder('content_areas'); 142 | $builder->setParentContext($fieldsBuilder); 143 | $fieldsBuilder->expects($this->once())->method('addText'); 144 | 145 | $builder 146 | ->addLayout('banner') 147 | ->addText('title') 148 | ->addWysiwyg('content') 149 | ->addLayout('content_columns') 150 | ->addRepeater('columns', ['min' => 1, 'max' => 2]) 151 | ->addWysiwyg('content') 152 | ->endFlexibleContent() 153 | ->addText('parent_title'); 154 | } 155 | 156 | public function testSetLocation() 157 | { 158 | $fieldsBuilder = $this->getMockBuilder('StoutLogic\AcfBuilder\FieldsBuilder') 159 | ->setConstructorArgs(['parent']) 160 | ->getMock(); 161 | 162 | $builder = new FlexibleContentBuilder('content_areas'); 163 | $builder->setParentContext($fieldsBuilder); 164 | $fieldsBuilder->expects($this->once())->method('setLocation'); 165 | 166 | $builder 167 | ->addLayout('banner') 168 | ->addText('title') 169 | ->addWysiwyg('content') 170 | ->addLayout('content_columns') 171 | ->addRepeater('columns', ['min' => 1, 'max' => 2]) 172 | ->addWysiwyg('content') 173 | ->setLocation('post_type', '==', 'page'); 174 | } 175 | 176 | public function testOverrideButton() 177 | { 178 | 179 | $builder = new FlexibleContentBuilder('content_areas', 'flexible_content', ['button_label' => 'Add Area']); 180 | 181 | 182 | $expectedConfig = [ 183 | 'name' => 'content_areas', 184 | 'type' => 'flexible_content', 185 | 'button_label' => 'Add Area', 186 | ]; 187 | 188 | $this->assertArraySubset($expectedConfig, $builder->build()); 189 | } 190 | 191 | public function testOverrideLayoutLabel() 192 | { 193 | $builder = new FlexibleContentBuilder('content_areas', 'flexible_content'); 194 | 195 | $builder->addLayout('images_multiple', [ 196 | 'label' => 'Custom Label', 197 | ]); 198 | 199 | $expectedConfig = [ 200 | 'layouts' => [ 201 | [ 202 | 'name' => 'images_multiple', 203 | 'label' => 'Custom Label', 204 | ], 205 | ] 206 | ]; 207 | 208 | $this->assertArraySubset($expectedConfig, $builder->build()); 209 | } 210 | 211 | public function testAddLayouts() 212 | { 213 | $banner = $this->getMockBuilder('StoutLogic\AcfBuilder\FieldsBuilder') 214 | ->setConstructorArgs(['parent']) 215 | ->getMock(); 216 | 217 | $banner->expects($this->once())->method('build')->willReturn([ 218 | 'key' => 'group_banner', 219 | 'name' => 'banner', 220 | 'title' => 'Banner', 221 | 'display' => 'block', 222 | 'fields' => [ 223 | [ 224 | 'key' => 'field_banner_title', 225 | 'name' => 'title', 226 | 'type' => 'text', 227 | ], 228 | [ 229 | 'name' => 'content', 230 | 'type' => 'wysiwyg', 231 | ] 232 | ] 233 | ]); 234 | 235 | $builder = new FlexibleContentBuilder('content_areas'); 236 | $builder->addLayouts([$banner, 'header']); 237 | 238 | $expectedConfig = [ 239 | 'key' => 'field_content_areas', 240 | 'name' => 'content_areas', 241 | 'label' => 'Content Areas', 242 | 'type' => 'flexible_content', 243 | 'button_label' => 'Add Content Area', 244 | 'layouts' => [ 245 | [ 246 | 'key' => 'field_content_areas_banner', 247 | 'name' => 'banner', 248 | 'label' => 'Banner', 249 | 'display' => 'block', 250 | 'sub_fields' => [ 251 | [ 252 | 'key' => 'field_content_areas_banner_title', 253 | 'name' => 'title', 254 | 'type' => 'text', 255 | ], 256 | [ 257 | 'name' => 'content', 258 | 'type' => 'wysiwyg', 259 | ] 260 | ] 261 | ], 262 | [ 263 | 'key' => 'field_content_areas_header', 264 | 'name' => 'header', 265 | 'label' => 'Header', 266 | ] 267 | ] 268 | ]; 269 | 270 | $config = $builder->build(); 271 | $this->assertArraySubset($expectedConfig, $config); 272 | } 273 | 274 | public function testRemoveFieldFromLayout() 275 | { 276 | $banner = new FieldsBuilder('banner'); 277 | $banner 278 | ->addText('title') 279 | ->addWysiwyg('content'); 280 | 281 | $builder = new FlexibleContentBuilder('content_areas'); 282 | $builder 283 | ->addLayouts([$banner, 'header', 'footer']); 284 | 285 | $expectedConfig = [ 286 | 'key' => 'field_content_areas', 287 | 'name' => 'content_areas', 288 | 'label' => 'Content Areas', 289 | 'type' => 'flexible_content', 290 | 'button_label' => 'Add Content Area', 291 | 'layouts' => [ 292 | [ 293 | 'key' => 'field_content_areas_banner', 294 | 'name' => 'banner', 295 | 'label' => 'Banner', 296 | 'display' => 'block', 297 | 'sub_fields' => [ 298 | [ 299 | 'key' => 'field_content_areas_banner_title', 300 | 'name' => 'title', 301 | 'type' => 'text', 302 | ], 303 | [ 304 | 'name' => 'content', 305 | 'type' => 'wysiwyg', 306 | ] 307 | ] 308 | ], 309 | [ 310 | 'key' => 'field_content_areas_header', 311 | 'name' => 'header', 312 | 'label' => 'Header', 313 | ] 314 | ] 315 | ]; 316 | 317 | 318 | $config = $builder->build(); 319 | $this->assertCount(2, $config['layouts'][0]['sub_fields']); 320 | 321 | $builder->getLayout('banner')->removeField('title'); 322 | $config = $builder->build(); 323 | $this->assertCount(1, $config['layouts'][0]['sub_fields']); 324 | } 325 | 326 | public function testRemoveFieldFromLayoutWithDeepNesting() 327 | { 328 | $banner = new FieldsBuilder('banner'); 329 | $banner 330 | ->addText('title') 331 | ->addWysiwyg('content'); 332 | 333 | $builder = new FieldsBuilder('test'); 334 | $builder 335 | ->addFlexibleContent('sections') 336 | ->addLayouts([$banner, 'header', 'footer']); 337 | 338 | 339 | $config = $builder->build(); 340 | $this->assertCount(2, $config['fields'][0]['layouts'][0]['sub_fields']); 341 | 342 | $builder->removeField('sections->banner->title'); 343 | $config = $builder->build(); 344 | $this->assertCount(1, $config['fields'][0]['layouts'][0]['sub_fields']); 345 | } 346 | 347 | public function testModifyingFieldFromLayoutWithDeepNesting() 348 | { 349 | $banner = new FieldsBuilder('banner'); 350 | $banner 351 | ->addText('title') 352 | ->addWysiwyg('content'); 353 | 354 | $builder = new FieldsBuilder('test'); 355 | $builder 356 | ->addFlexibleContent('sections') 357 | ->addLayouts([$banner, 'header', 'footer']); 358 | 359 | 360 | $config = $builder->build(); 361 | $this->assertSame('Title', $config['fields'][0]['layouts'][0]['sub_fields'][0]['label']); 362 | 363 | $builder->modifyField('sections->banner->title', ['label' => 'Headline']); 364 | 365 | $config = $builder->build(); 366 | $this->assertSame('Headline', $config['fields'][0]['layouts'][0]['sub_fields'][0]['label']); 367 | } 368 | 369 | public function testModifyingLayoutWithDeepNesting() 370 | { 371 | $banner = new FieldsBuilder('banner'); 372 | $banner 373 | ->addText('title') 374 | ->addWysiwyg('content'); 375 | 376 | $builder = new FieldsBuilder('test'); 377 | $builder 378 | ->addFlexibleContent('sections') 379 | ->addLayouts([$banner, 'header', 'footer']); 380 | 381 | 382 | $config = $builder->build(); 383 | $this->assertSame('Sections', $config['fields'][0]['label']); 384 | 385 | $builder->modifyField('sections', ['label' => 'Blocks']); 386 | $builder->modifyField('sections->banner', [ 387 | 'name' => 'hero', 388 | 'key' => 'hero' 389 | ]); 390 | 391 | $config = $builder->build(); 392 | 393 | $this->assertSame('Blocks', $config['fields'][0]['label']); 394 | $this->assertSame('hero', $config['fields'][0]['layouts'][0]['name']); 395 | } 396 | 397 | public function testRemoveLayout() 398 | { 399 | $banner = new FieldsBuilder('banner'); 400 | $banner 401 | ->addText('title') 402 | ->addWysiwyg('content'); 403 | 404 | $builder = new FlexibleContentBuilder('content_areas'); 405 | $builder 406 | ->addLayouts([$banner, 'header', 'footer']); 407 | 408 | $expectedConfig = [ 409 | 'key' => 'field_content_areas', 410 | 'name' => 'content_areas', 411 | 'label' => 'Content Areas', 412 | 'type' => 'flexible_content', 413 | 'button_label' => 'Add Content Area', 414 | 'layouts' => [ 415 | [ 416 | 'key' => 'field_content_areas_banner', 417 | 'name' => 'banner', 418 | 'label' => 'Banner', 419 | 'display' => 'block', 420 | 'sub_fields' => [ 421 | [ 422 | 'key' => 'field_content_areas_banner_title', 423 | 'name' => 'title', 424 | 'type' => 'text', 425 | ], 426 | [ 427 | 'name' => 'content', 428 | 'type' => 'wysiwyg', 429 | ] 430 | ] 431 | ], 432 | [ 433 | 'key' => 'field_content_areas_header', 434 | 'name' => 'header', 435 | 'label' => 'Header', 436 | ] 437 | ] 438 | ]; 439 | 440 | 441 | $config = $builder->build(); 442 | $this->assertCount(3, $config['layouts']); 443 | 444 | $builder->removeLayout('footer'); 445 | 446 | $config = $builder->build(); 447 | $this->assertCount(2, $config['layouts']); 448 | } 449 | 450 | public function testRemoveLayoutWithDeepNesting() 451 | { 452 | $banner = new FieldsBuilder('banner'); 453 | $banner 454 | ->addText('title') 455 | ->addWysiwyg('content'); 456 | 457 | $builder = new FieldsBuilder('test'); 458 | $builder 459 | ->addFlexibleContent('sections') 460 | ->addLayouts([$banner, 'header', 'footer']); 461 | 462 | 463 | $config = $builder->build(); 464 | $this->assertCount(3, $config['fields'][0]['layouts']); 465 | 466 | $builder->removeField('sections->banner'); 467 | 468 | $config = $builder->build(); 469 | $this->assertCount(2, $config['fields'][0]['layouts']); 470 | } 471 | 472 | public function testLayoutExists() 473 | { 474 | $banner = new FieldsBuilder('banner'); 475 | $banner 476 | ->addText('title') 477 | ->addWysiwyg('content'); 478 | 479 | $builder = new FlexibleContentBuilder('content_areas'); 480 | $builder 481 | ->addLayouts([$banner, 'header', 'footer']); 482 | 483 | $this->assertTrue($builder->layoutExists('header')); 484 | $this->assertFalse($builder->layoutExists('carousel')); 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /tests/GroupBuilderTest.php: -------------------------------------------------------------------------------- 1 | addColorPicker('color'); 20 | 21 | $expectedConfig = [ 22 | 'name' => 'background', 23 | 'type' => 'group', 24 | 'sub_fields' => [ 25 | [ 26 | 'type' => 'color_picker', 27 | 'name' => 'color', 28 | ], 29 | ], 30 | 31 | ]; 32 | 33 | $this->assertArraySubset($expectedConfig, $builder->build()); 34 | } 35 | 36 | public function testGroupBuilderAddFieldsGroupFields() 37 | { 38 | $size = new FieldsBuilder('size'); 39 | $size 40 | ->addNumber('width') 41 | ->addNumber('height'); 42 | 43 | $builder = new GroupBuilder('background'); 44 | 45 | $builder 46 | ->addColorPicker('color') 47 | ->addFields($size); 48 | 49 | $expectedConfig = [ 50 | 'name' => 'background', 51 | 'type' => 'group', 52 | 'sub_fields' => [ 53 | [ 54 | 'type' => 'color_picker', 55 | 'name' => 'color', 56 | ], 57 | [ 58 | 'type' => 'number', 59 | 'name' => 'width', 60 | ], 61 | [ 62 | 'type' => 'number', 63 | 'name' => 'height', 64 | ], 65 | ], 66 | 67 | ]; 68 | 69 | $this->assertArraySubset($expectedConfig, $builder->build()); 70 | } 71 | 72 | public function testModifyGroup() 73 | { 74 | $subject = new GroupBuilder('test1'); 75 | 76 | $subject->addText('text'); 77 | 78 | $subject->modifyField('text', ['label' => 'new label']); 79 | 80 | $this->assertEquals([ 81 | 'key' => 'field_test1', 82 | 'name' => 'test1', 83 | 'label' => 'Test1', 84 | 'type' => 'group', 85 | 'sub_fields' => [ 86 | [ 87 | 'key' => 'field_test1_text', 88 | 'type' => 'text', 89 | 'name' => 'text', 90 | 'label' => 'new label' 91 | ] 92 | ] 93 | ], $subject->build()); 94 | } 95 | 96 | public function testDeepModifyGroupWithArray() { 97 | $subject = new FieldsBuilder('test'); 98 | 99 | $partial = new FieldsBuilder('test-partial'); 100 | 101 | $partial 102 | ->addRepeater('items') 103 | ->addText('headline') 104 | ->endRepeater(); 105 | 106 | $subject->addFields($partial); 107 | 108 | $subject->modifyField('items->headline', [ 109 | 'wrapper' => [ 110 | 'width' => '77%' 111 | ] 112 | ]); 113 | 114 | $this->assertEquals([ 115 | 'key' => 'group_test', 116 | 'title' => 'Test', 117 | 'fields' => [ 118 | [ 119 | 'type' => 'repeater', 120 | 'name' => 'items', 121 | 'label' => 'Items', 122 | 'key' => 'field_test_items', 123 | 'button_label' => 'Add Item', 124 | 'sub_fields' => [ 125 | [ 126 | 'type' => 'text', 127 | 'label' => 'Headline', 128 | 'name' => 'headline', 129 | 'key' => 'field_test_items_headline', 130 | 'wrapper' => [ 131 | 'width' => '77%' 132 | ] 133 | ] 134 | ] 135 | ] 136 | ], 137 | 'location' => null 138 | ], $subject->build()); 139 | } 140 | 141 | 142 | public function testModifyGroupWithArrayWithoutDeepLinking() { 143 | $subject = new FieldsBuilder('test'); 144 | $subject 145 | ->addRepeater('items') 146 | ->addText('headline'); 147 | 148 | $subject->getField('items')->modifyField('headline', [ 149 | 'wrapper' => [ 150 | 'width' => '77%' 151 | ] 152 | ]); 153 | 154 | $this->assertEquals([ 155 | 'key' => 'group_test', 156 | 'title' => 'Test', 157 | 'fields' => [ 158 | [ 159 | 'type' => 'repeater', 160 | 'name' => 'items', 161 | 'label' => 'Items', 162 | 'key' => 'field_test_items', 163 | 'button_label' => 'Add Item', 164 | 'sub_fields' => [ 165 | [ 166 | 'type' => 'text', 167 | 'label' => 'Headline', 168 | 'name' => 'headline', 169 | 'key' => 'field_test_items_headline', 170 | 'wrapper' => [ 171 | 'width' => '77%' 172 | ] 173 | ] 174 | ] 175 | ] 176 | ], 177 | 'location' => null 178 | ], $subject->build()); 179 | } 180 | 181 | public function testDeepModifyThreeLevelsGroupWithArray() { 182 | $subject = new FieldsBuilder('test'); 183 | 184 | $subject 185 | ->addGroup('slides') 186 | ->addRepeater('slide')->setWidth("25%") 187 | ->addText('headline')->setWidth('100%') 188 | ->addTextarea('content'); 189 | 190 | $subject->modifyField('slides->slide->headline', [ 191 | 'wrapper' => [ 192 | 'width' => '50%' 193 | ] 194 | ]); 195 | 196 | $this->assertArraySubset([ 197 | 'key' => 'group_test', 198 | 'title' => 'Test', 199 | 'fields' => [ 200 | [ 201 | 'name' => 'slides', 202 | 'type' => 'group', 203 | 'sub_fields' => [ 204 | [ 205 | 'type' => 'repeater', 206 | 'name' => 'slide', 207 | 'wrapper' => [ 208 | 'width' => '25%' 209 | ], 210 | 'sub_fields' => [ 211 | [ 212 | 'type' => 'text', 213 | 'name' => 'headline', 214 | 'wrapper' => [ 215 | 'width' => '50%' 216 | ] 217 | ], 218 | [ 219 | 'type' => 'textarea', 220 | 'name' => 'content', 221 | ] 222 | ] 223 | ] 224 | ] 225 | ] 226 | ] 227 | ], $subject->build()); 228 | } 229 | 230 | public function testDeepModifyGroupWithClosure() { 231 | $subject = new FieldsBuilder('test'); 232 | 233 | $subject 234 | ->addGroup('slides') 235 | ->addRepeater('slide')->setWidth("25%") 236 | ->addText('headline')->setWidth('100%') 237 | ->addTextarea('content'); 238 | 239 | $subject->modifyField('slides->slide', function(FieldsBuilder $builder) { 240 | $builder 241 | ->getField('slide') 242 | ->setWidth("50%"); 243 | 244 | $builder->addLink('cta'); 245 | 246 | return $builder; 247 | }); 248 | 249 | $this->assertArraySubset([ 250 | 'key' => 'group_test', 251 | 'title' => 'Test', 252 | 'fields' => [ 253 | [ 254 | 'name' => 'slides', 255 | 'type' => 'group', 256 | 'sub_fields' => [ 257 | [ 258 | 'type' => 'repeater', 259 | 'name' => 'slide', 260 | 'wrapper' => [ 261 | 'width' => '50%' 262 | ], 263 | 'sub_fields' => [ 264 | [ 265 | 'type' => 'text', 266 | 'name' => 'headline', 267 | 'wrapper' => [ 268 | 'width' => '100%' 269 | ] 270 | ], 271 | [ 272 | 'type' => 'textarea', 273 | 'name' => 'content', 274 | ], 275 | ] 276 | ], 277 | [ 278 | 'type' => 'link', 279 | 'name' => 'cta', 280 | ], 281 | ] 282 | ] 283 | ] 284 | ], $subject->build()); 285 | } 286 | 287 | public function testDeepModifyThreeLevelsGroupWithClosure() { 288 | $subject = new FieldsBuilder('test'); 289 | 290 | $subject 291 | ->addGroup('slides') 292 | ->addRepeater('slide')->setWidth("25%") 293 | ->addText('headline')->setWidth('100%') 294 | ->addTextarea('content'); 295 | 296 | $subject->modifyField('slides->slide->headline', function(FieldsBuilder $builder) { 297 | $builder->addLink('cta'); 298 | return $builder; 299 | }); 300 | 301 | $this->assertArraySubset([ 302 | 'key' => 'group_test', 303 | 'title' => 'Test', 304 | 'fields' => [ 305 | [ 306 | 'name' => 'slides', 307 | 'type' => 'group', 308 | 'sub_fields' => [ 309 | [ 310 | 'type' => 'repeater', 311 | 'name' => 'slide', 312 | 'wrapper' => [ 313 | 'width' => '25%' 314 | ], 315 | 'sub_fields' => [ 316 | [ 317 | 'type' => 'text', 318 | 'name' => 'headline', 319 | 'wrapper' => [ 320 | 'width' => '100%' 321 | ] 322 | ], 323 | [ 324 | 'type' => 'link', 325 | 'name' => 'cta', 326 | ], 327 | [ 328 | 'type' => 'textarea', 329 | 'name' => 'content', 330 | ], 331 | ] 332 | ] 333 | ] 334 | ] 335 | ] 336 | ], $subject->build()); 337 | } 338 | 339 | public function testRemovingGroup() 340 | { 341 | $subject = new GroupBuilder('test1'); 342 | 343 | $subject->addText('text'); 344 | $subject->addText('text2'); 345 | $subject->addText('text3'); 346 | 347 | $subject->removeField('text2'); 348 | 349 | $builtSubject = $subject->build(); 350 | 351 | $this->assertCount(2, $builtSubject['sub_fields']); 352 | $this->assertEquals([ 353 | [ 354 | 'type' => 'text', 355 | 'name' => 'text', 356 | 'label' => 'Text', 357 | 'key' => 'field_test1_text' 358 | ], 359 | [ 360 | 'type' => 'text', 361 | 'name' => 'text3', 362 | 'label' => 'Text3', 363 | 'key' => 'field_test1_text3' 364 | ], 365 | ], $builtSubject['sub_fields']); 366 | } 367 | 368 | public function testDeeplyNestedRemoveField() { 369 | $subject = new FieldsBuilder('test'); 370 | 371 | $subject 372 | ->addGroup('slides') 373 | ->addRepeater('slide')->setWidth("25%") 374 | ->addText('headline')->setWidth('100%') 375 | ->addTextarea('content') 376 | ->addLink('cta'); 377 | 378 | $preConfig = $subject->build(); 379 | $this->assertCount(3, $preConfig['fields'][0]['sub_fields'][0]['sub_fields']); 380 | 381 | $subject->removeField('slides->slide->content'); 382 | 383 | $postConfig = $subject->build(); 384 | $this->assertCount(2, $postConfig['fields'][0]['sub_fields'][0]['sub_fields']); 385 | } 386 | 387 | public function testRemovingGroupNotFound() 388 | { 389 | $this->expectException(FieldNotFoundException::class); 390 | 391 | $subject = new GroupBuilder('test1'); 392 | 393 | $subject->addText('text'); 394 | $subject->addText('text2'); 395 | $subject->addText('text3'); 396 | 397 | $subject->removeField('text4'); 398 | } 399 | 400 | 401 | } 402 | -------------------------------------------------------------------------------- /tests/LocationBuilderTest.php: -------------------------------------------------------------------------------- 1 | 'post_type', 21 | 'operator' => '==', 22 | 'value' => 'post', 23 | ], 24 | ] 25 | ]; 26 | 27 | $this->assertArraySubset($expectedConfig, $builder->build()); 28 | } 29 | 30 | public function testAnd() 31 | { 32 | $builder = new LocationBuilder('post_type', '==', 'post'); 33 | $builder->and('post_id', '!=', '1'); 34 | 35 | $expectedConfig = [ 36 | [ 37 | [ 38 | 'param' => 'post_type', 39 | 'operator' => '==', 40 | 'value' => 'post', 41 | ], 42 | [ 43 | 'param' => 'post_id', 44 | 'operator' => '!=', 45 | 'value' => '1', 46 | ], 47 | ] 48 | ]; 49 | 50 | $this->assertArraySubset($expectedConfig, $builder->build()); 51 | } 52 | 53 | public function testOr() 54 | { 55 | $builder = new LocationBuilder('post_type', '==', 'post'); 56 | $builder->or('post_type', '==', 'page') 57 | ->and('post_id', '!=', '10') 58 | ->and('post_id', '!=', '11'); 59 | 60 | $expectedConfig = [ 61 | [ 62 | [ 63 | 'param' => 'post_type', 64 | 'operator' => '==', 65 | 'value' => 'post', 66 | ], 67 | ], 68 | [ 69 | [ 70 | 'param' => 'post_type', 71 | 'operator' => '==', 72 | 'value' => 'page', 73 | ], 74 | [ 75 | 'param' => 'post_id', 76 | 'operator' => '!=', 77 | 'value' => '10', 78 | ], 79 | [ 80 | 'param' => 'post_id', 81 | 'operator' => '!=', 82 | 'value' => '11', 83 | ], 84 | ], 85 | ]; 86 | 87 | $this->assertArraySubset($expectedConfig, $builder->build()); 88 | } 89 | } -------------------------------------------------------------------------------- /tests/ParentDelegationBuilderTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('StoutLogic\AcfBuilder\ParentDelegationBuilder') 14 | ->setMethods(['parentMethod', 'build']) 15 | ->getMockForAbstractClass(); 16 | $child = $this->getMockForAbstractClass('StoutLogic\AcfBuilder\ParentDelegationBuilder'); 17 | $child->setParentContext($parent); 18 | 19 | $parent->expects($this->once())->method('parentMethod'); 20 | $child->parentMethod(); 21 | } 22 | 23 | public function testThrowingException() 24 | { 25 | $parent = $this->getMockForAbstractClass('StoutLogic\AcfBuilder\ParentDelegationBuilder'); 26 | $child = $this->getMockForAbstractClass('StoutLogic\AcfBuilder\ParentDelegationBuilder'); 27 | $child->setParentContext($parent); 28 | 29 | $this->expectException('\Exception'); 30 | $child->nonExistantParentMethod(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/RepeaterBuilderTest.php: -------------------------------------------------------------------------------- 1 | addText('title') 18 | ->addWysiwyg('content'); 19 | 20 | $expectedConfig = [ 21 | 'name' => 'slides', 22 | 'type' => 'repeater', 23 | 'button_label' => 'Add Slide', 24 | 'sub_fields' => [ 25 | [ 26 | 'name' => 'title', 27 | ], 28 | [ 29 | 'name' => 'content', 30 | ], 31 | ], 32 | ]; 33 | $this->assertArraySubset($expectedConfig, $builder->build()); 34 | $this->assertArrayNotHasKey('fields', $builder->build()); 35 | } 36 | 37 | public function testEndRepeater() 38 | { 39 | $fieldsBuilder = $this->getMockBuilder('StoutLogic\AcfBuilder\FieldsBuilder') 40 | ->setConstructorArgs(['parent']) 41 | ->getMock(); 42 | 43 | $repeaterBuilder = new RepeaterBuilder('slides'); 44 | $repeaterBuilder->setParentContext($fieldsBuilder); 45 | 46 | $fieldsBuilder->expects($this->once())->method('addText'); 47 | 48 | $repeaterBuilder->addText('title') 49 | ->addWysiwyg('content') 50 | ->endRepeater() 51 | ->addText('parent_title'); 52 | } 53 | 54 | public function testSetLocation() 55 | { 56 | $fieldsBuilder = $this->getMockBuilder('StoutLogic\AcfBuilder\FieldsBuilder') 57 | ->setConstructorArgs(['parent']) 58 | ->getMock(); 59 | 60 | $repeaterBuilder = new RepeaterBuilder('slides'); 61 | $repeaterBuilder->setParentContext($fieldsBuilder); 62 | 63 | $fieldsBuilder->expects($this->once())->method('setLocation'); 64 | 65 | $repeaterBuilder->addText('title') 66 | ->addWysiwyg('content') 67 | ->setLocation('post_type', '==', 'page'); 68 | } 69 | 70 | public function testAddFields() 71 | { 72 | $banner = new FieldsBuilder('banner'); 73 | $banner 74 | ->addText('title') 75 | ->addWysiwyg('content'); 76 | 77 | $repeaterBuilder = new RepeaterBuilder('slides'); 78 | $repeaterBuilder 79 | ->addFields($banner) 80 | ->addImage('thumbnail'); 81 | 82 | $expectedConfig = [ 83 | 'name' => 'slides', 84 | 'type' => 'repeater', 85 | 'sub_fields' => [ 86 | [ 87 | 'name' => 'title', 88 | ], 89 | [ 90 | 'name' => 'content', 91 | ], 92 | [ 93 | 'name' => 'thumbnail', 94 | ], 95 | ], 96 | ]; 97 | $this->assertArraySubset($expectedConfig, $repeaterBuilder->build()); 98 | 99 | $repeaterBuilder = new RepeaterBuilder('slides'); 100 | $repeaterBuilder 101 | ->addFields([ 102 | $banner->getField('title'), 103 | $banner->getField('content'), 104 | ]) 105 | ->addImage('thumbnail'); 106 | $this->assertArraySubset($expectedConfig, $repeaterBuilder->build()); 107 | } 108 | 109 | public function testOverrideButton() 110 | { 111 | $builder = new RepeaterBuilder('slides', 'repeater', ['button_label' => 'Add New Slide']); 112 | $builder->addText('title') 113 | ->addWysiwyg('content'); 114 | 115 | $expectedConfig = [ 116 | 'name' => 'slides', 117 | 'type' => 'repeater', 118 | 'button_label' => 'Add New Slide', 119 | 'sub_fields' => [ 120 | [ 121 | 'name' => 'title', 122 | ], 123 | [ 124 | 'name' => 'content', 125 | ], 126 | ], 127 | ]; 128 | 129 | $this->assertArraySubset($expectedConfig, $builder->build()); 130 | $this->assertArrayNotHasKey('fields', $builder->build()); 131 | } 132 | 133 | public function testCollapseSetting() 134 | { 135 | $builder = new RepeaterBuilder('slides', 'repeater', ['collapsed' => 'title']); 136 | $builder->addText('title') 137 | ->addWysiwyg('content'); 138 | 139 | $expectedConfig = [ 140 | 'name' => 'slides', 141 | 'type' => 'repeater', 142 | 'collapsed' => 'slides_title', 143 | 'sub_fields' => [ 144 | [ 145 | 'key' => 'field_slides_title', 146 | 'name' => 'title', 147 | ], 148 | [ 149 | 'name' => 'content', 150 | ], 151 | ], 152 | ]; 153 | 154 | $this->assertArraySubset($expectedConfig, $builder->build()); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /tests/TabBuilderTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(class_exists('StoutLogic\AcfBuilder\TabBuilder')); 16 | } 17 | 18 | public function testCreateTabBuilder() 19 | { 20 | $subject = new TabBuilder('settings'); 21 | $this->assertArraySubset([ 22 | 'key' => 'field_settings_tab', 23 | 'name' => 'settings_tab', 24 | 'label' => 'Settings', 25 | 'type' => 'tab', 26 | ], $subject->build()); 27 | } 28 | 29 | public function testEndpoint() 30 | { 31 | $subject = new TabBuilder('settings'); 32 | $this->assertSame($subject, $subject->endpoint()); 33 | $this->assertArraySubset([ 34 | 'key' => 'field_settings_tab', 35 | 'name' => 'settings_tab', 36 | 'label' => 'Settings', 37 | 'type' => 'tab', 38 | 'endpoint' => 1, 39 | ], $subject->build()); 40 | 41 | $this->assertSame($subject, $subject->removeEndpoint()); 42 | $this->assertArraySubset([ 43 | 'key' => 'field_settings_tab', 44 | 'name' => 'settings_tab', 45 | 'label' => 'Settings', 46 | 'type' => 'tab', 47 | 'endpoint' => 0, 48 | ], $subject->build()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/TestUtils.php: -------------------------------------------------------------------------------- 1 | getMethod($name); 10 | $method->setAccessible(true); 11 | return $method->invokeArgs($obj, $args); 12 | } 13 | } -------------------------------------------------------------------------------- /tests/Transform/ConditionalFieldTest.php: -------------------------------------------------------------------------------- 1 | prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 16 | $transform = new Transform\ConditionalField($builder->reveal()); 17 | $this->assertInstanceOf('\StoutLogic\AcfBuilder\Transform\RecursiveTransform', $transform); 18 | } 19 | 20 | public function testGetKeys() 21 | { 22 | $builder = $this->prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 23 | $transform = new Transform\ConditionalField($builder->reveal()); 24 | $this->assertSame(['field'], $transform->getKeys()); 25 | } 26 | 27 | public function testTransformValue() 28 | { 29 | $field = $this->prophesize('\StoutLogic\AcfBuilder\FieldBuilder'); 30 | $field 31 | ->getKey() 32 | ->willReturn('field_key'); 33 | 34 | $builder = $this->prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 35 | $builder 36 | ->getField('value') 37 | ->willReturn($field->reveal()); 38 | 39 | $builder 40 | ->fieldExists('value') 41 | ->willReturn(true); 42 | 43 | $transform = new Transform\ConditionalField($builder->reveal()); 44 | $this->assertSame('field_key', $transform->transformValue('value')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Transform/ConditionalLogicTest.php: -------------------------------------------------------------------------------- 1 | prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 18 | $transform = new Transform\ConditionalLogic($builder->reveal()); 19 | $this->assertInstanceOf('\StoutLogic\AcfBuilder\Transform\IterativeTransform', $transform); 20 | } 21 | 22 | public function testGetKeys() 23 | { 24 | $builder = $this->prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 25 | $transform = new Transform\ConditionalLogic($builder->reveal()); 26 | $this->assertSame(['conditional_logic'], $transform->getKeys()); 27 | } 28 | 29 | public function testTransformValue() 30 | { 31 | $field = $this->prophesize('\StoutLogic\AcfBuilder\FieldBuilder'); 32 | $field 33 | ->getKey() 34 | ->willReturn('field_name'); 35 | $field 36 | ->hasCustomKey() 37 | ->willReturn(false); 38 | 39 | 40 | $builder = $this->prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 41 | $builder 42 | ->getField('name') 43 | ->willReturn($field->reveal()); 44 | 45 | $builder 46 | ->fieldExists('name') 47 | ->willReturn(true); 48 | 49 | $transform = new Transform\ConditionalLogic($builder->reveal()); 50 | $this->assertSame([[[ 51 | 'field' => 'field_name', 52 | 'operator' => '==', 53 | 'value' => 1, 54 | ]]], $transform->transformValue([[[ 55 | 'field' => 'name', 56 | 'operator' => '==', 57 | 'value' => 1, 58 | ]]])); 59 | } 60 | 61 | public function testOnlyGetsAppliedOncePerLevel() 62 | { 63 | $builder = new \StoutLogic\AcfBuilder\FlexibleContentBuilder('name'); 64 | $builder 65 | ->addLayout('hero') 66 | ->addImage( 'hero_image' ) 67 | ->addWysiwyg( 'hero_text' ) 68 | ->addRepeater('cta') 69 | ->addSelect('link_type') 70 | ->addChoices('internal', 'external', 'text') 71 | ->addPageLink('cta_link', [ 72 | 'post_type' => [ 73 | 'page', 74 | 'post', 75 | 'product', 76 | ], 77 | ]) 78 | ->conditional('link_type', '==', 'internal'); 79 | 80 | 81 | $expectedConfig = [ 82 | 'layouts' => [ 83 | [ 84 | 'name' => 'hero', 85 | 'sub_fields' => [ 86 | [ 87 | 'name' => 'hero_image' 88 | ], 89 | [ 90 | 'name' => 'hero_text' 91 | ], 92 | [ 93 | 'name' => 'cta', 94 | 'sub_fields' => [ 95 | [ 96 | 'key' => 'field_name_hero_cta_link_type', 97 | 'name' => 'link_type' 98 | ], 99 | [ 100 | 'key' => 'field_name_hero_cta_cta_link', 101 | 'conditional_logic' => [ 102 | [ 103 | [ 104 | 'field' => 'field_name_hero_cta_link_type', 105 | ] 106 | ] 107 | ] 108 | 109 | ] 110 | ] 111 | ] 112 | ] 113 | ], 114 | ] 115 | ]; 116 | 117 | $config = $builder->build(); 118 | $this->assertArraySubset($expectedConfig, $config); 119 | } 120 | 121 | 122 | public function testAllowConditionBasedOnParentFieldWithCustomKey() 123 | { 124 | $builder = new \StoutLogic\AcfBuilder\FlexibleContentBuilder('name'); 125 | $builder 126 | ->addLayout('hero') 127 | ->addSelect('hero_type') 128 | ->addChoices('fullscreen', 'standard') 129 | ->setCustomKey('my_custom_key') 130 | ->addImage( 'hero_image' ) 131 | ->addWysiwyg( 'hero_text' ) 132 | ->addRepeater('cta') 133 | ->addSelect('link_type') 134 | ->addChoices('internal', 'external', 'text') 135 | ->addTrueFalse('cta_animated') 136 | ->conditional('my_custom_key', '==', '1'); 137 | 138 | 139 | $expectedConfig = [ 140 | 'layouts' => [ 141 | [ 142 | 'name' => 'hero', 143 | 'sub_fields' => [ 144 | [ 145 | 'name' => 'hero_type', 146 | 'key' => 'my_custom_key', 147 | ], 148 | [ 149 | 'name' => 'hero_image' 150 | ], 151 | [ 152 | 'name' => 'hero_text' 153 | ], 154 | [ 155 | 'name' => 'cta', 156 | 'sub_fields' => [ 157 | [ 158 | 'name' => 'link_type' 159 | ], 160 | [ 161 | 'key' => 'field_name_hero_cta_cta_animated', 162 | 'conditional_logic' => [ 163 | [ 164 | [ 165 | 'field' => 'my_custom_key', 166 | ] 167 | ] 168 | ] 169 | 170 | ] 171 | ] 172 | ] 173 | ] 174 | ], 175 | ], 176 | ]; 177 | 178 | $config = $builder->build(); 179 | $this->assertArraySubset($expectedConfig, $config); 180 | 181 | } 182 | 183 | public function testAllowConditionBasedOnParentField() 184 | { 185 | $builder = new \StoutLogic\AcfBuilder\FlexibleContentBuilder('name'); 186 | $builder 187 | ->addLayout('hero') 188 | ->addSelect('hero_type') 189 | ->addChoices('fullscreen', 'standard') 190 | ->addImage( 'hero_image' ) 191 | ->addWysiwyg( 'hero_text' ) 192 | ->addRepeater('cta') 193 | ->addSelect('link_type') 194 | ->addChoices('internal', 'external', 'text') 195 | ->addTrueFalse('cta_animated') 196 | ->conditional('hero_type', '==', '1'); 197 | 198 | 199 | $expectedConfig = [ 200 | 'layouts' => [ 201 | [ 202 | 'name' => 'hero', 203 | 'sub_fields' => [ 204 | [ 205 | 'name' => 'hero_type', 206 | 'key' => 'field_name_hero_hero_type' 207 | ], 208 | [ 209 | 'name' => 'hero_image' 210 | ], 211 | [ 212 | 'name' => 'hero_text' 213 | ], 214 | [ 215 | 'name' => 'cta', 216 | 'sub_fields' => [ 217 | [ 218 | 'name' => 'link_type' 219 | ], 220 | [ 221 | 'key' => 'field_name_hero_cta_cta_animated', 222 | 'conditional_logic' => [ 223 | [ 224 | [ 225 | 'field' => 'field_name_hero_hero_type', 226 | ] 227 | ] 228 | ] 229 | 230 | ] 231 | ] 232 | ] 233 | ] 234 | ], 235 | ], 236 | ]; 237 | 238 | $config = $builder->build(); 239 | $this->assertArraySubset($expectedConfig, $config); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /tests/Transform/FlexibleContentLayoutTest.php: -------------------------------------------------------------------------------- 1 | prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 16 | $builder 17 | ->getName() 18 | ->willReturn('Fields Builder Name'); 19 | 20 | $transform = new Transform\FlexibleContentLayout($builder->reveal()); 21 | 22 | $expected = [ 23 | 'sub_fields' => 'fields', 24 | 'label' => 'title', 25 | ]; 26 | 27 | $actual = $transform->transform([ 28 | 'fields' => 'fields', 29 | 'title' => 'title', 30 | ]); 31 | 32 | $this->assertSame($expected, $actual); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Transform/NamespaceFieldKeyTest.php: -------------------------------------------------------------------------------- 1 | prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 16 | $transform = new Transform\NamespaceFieldKey($builder->reveal()); 17 | $this->assertInstanceOf('\StoutLogic\AcfBuilder\Transform\RecursiveTransform', $transform); 18 | } 19 | 20 | public function testGetKeys() 21 | { 22 | $builder = $this->prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 23 | $transform = new Transform\NamespaceFieldKey($builder->reveal()); 24 | $this->assertSame(['key', 'field', 'collapsed'], $transform->getKeys()); 25 | } 26 | 27 | public function testTransformValue() 28 | { 29 | $builder = $this->prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 30 | $builder 31 | ->getName() 32 | ->willReturn('Fields Builder Name'); 33 | 34 | $transform = new Transform\NamespaceFieldKey($builder->reveal()); 35 | $this->assertSame('field_fields_builder_name_value', $transform->transformValue('field_value')); 36 | $this->assertSame('field_fields_builder_name_value', $transform->transformValue('group_value')); 37 | } 38 | 39 | 40 | 41 | public function testShouldTransformValue() 42 | { 43 | $builder = $this->prophesize('\StoutLogic\AcfBuilder\FieldsBuilder'); 44 | $builder 45 | ->getName() 46 | ->willReturn('Fields Builder Name'); 47 | 48 | $transform = new Transform\NamespaceFieldKey($builder->reveal()); 49 | 50 | $this->assertTrue($transform->shouldTransformValue('key', [ 51 | 'key' => 'field_name', 52 | ])); 53 | 54 | $this->assertFalse($transform->shouldTransformValue('key', [ 55 | 'key' => '1234859849584545', 56 | '_has_custom_key' => true, 57 | ])); 58 | 59 | $this->assertTrue($transform->shouldTransformValue('key', [ 60 | 'key' => 'field_name', 61 | '_has_custom_key' => false, 62 | ])); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/test_autoload.php: -------------------------------------------------------------------------------- 1 | addRepeater('slides') 9 | ->addText('title') 10 | ->addTextarea('content') 11 | ->setLocation('post_type', '==', 'page'); 12 | 13 | print_r($builder->build()); 14 | --------------------------------------------------------------------------------