├── LICENSE ├── README.md ├── composer.json ├── config ├── callbacks.yaml ├── events.yaml ├── hooks.yaml └── services.yaml ├── skeleton ├── content-element │ ├── ContentElement.tpl.php │ ├── content_element.tpl.html.twig │ └── tl_content.tpl.php ├── dca-callback │ └── Callback.tpl.php ├── event-listener │ └── EventListener.tpl.php ├── frontend-module │ ├── FrontendModule.tpl.php │ ├── frontend_module.tpl.html.twig │ └── tl_module.tpl.php └── hook │ └── Hook.tpl.php └── src ├── ContaoMakerBundle.php ├── ContaoManager └── Plugin.php ├── DependencyInjection └── ContaoMakerExtension.php ├── Generator ├── ClassGenerator.php ├── DcaGenerator.php ├── GeneratorInterface.php ├── LanguageFileGenerator.php └── TemplateGenerator.php ├── Maker ├── AbstractFragmentMaker.php ├── MakeContentElement.php ├── MakeDcaCallback.php ├── MakeEventListener.php ├── MakeFrontendModule.php └── MakeHook.php └── Reflection ├── ImportExtractor.php ├── MethodDefinition.php └── SignatureGenerator.php /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contao maker bundle 2 | 3 | [![](https://img.shields.io/packagist/v/contao/maker-bundle.svg?style=flat-square)](https://packagist.org/packages/contao/maker-bundle) 4 | [![](https://img.shields.io/packagist/dt/contao/maker-bundle.svg?style=flat-square)](https://packagist.org/packages/contao/maker-bundle) 5 | 6 | The maker bundle allows you to generate content elements, front end modules, event listener, callbacks and hooks using 7 | interactive commands. 8 | 9 | Contao is an Open Source PHP Content Management System for people who want a professional website that is easy to 10 | maintain. Visit the [project website][1] for more information. 11 | 12 | ## Installation 13 | 14 | Run this command to install and enable the bundle in your application: 15 | 16 | ``` 17 | composer require contao/maker-bundle --dev 18 | ``` 19 | 20 | ## Usage 21 | 22 | This bundle provides several commands under the `make:` namespace. You can list them all with the following command: 23 | 24 | ``` 25 | php bin/console list make:contao 26 | 27 | Available commands for the "make:contao" namespace: 28 | make:contao:content-element Creates a new content element 29 | make:contao:dca-callback Creates a new DCA callback listener 30 | make:contao:event-listener Creates a new event listener 31 | make:contao:frontend-module Creates a new front end module 32 | make:contao:hook Creates a new hook listener 33 | ``` 34 | 35 | ## License 36 | 37 | Contao is licensed under the terms of the LGPLv3. 38 | 39 | ## Getting support 40 | 41 | Visit the [support page][2] to learn about the available support options. 42 | 43 | [1]: https://contao.org 44 | [2]: https://to.contao.org/support 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contao/maker-bundle", 3 | "description": "Allows you to create content elements, modules, hooks and more", 4 | "license": "LGPL-3.0-or-later", 5 | "type": "contao-bundle", 6 | "authors": [ 7 | { 8 | "name": "Leo Feyer", 9 | "homepage": "https://github.com/leofeyer" 10 | }, 11 | { 12 | "name": "Contao Community", 13 | "homepage": "https://contao.org/contributors" 14 | } 15 | ], 16 | "homepage": "https://contao.org", 17 | "support": { 18 | "issues": "https://github.com/contao/contao/issues", 19 | "forum": "https://community.contao.org", 20 | "source": "https://github.com/contao/maker-bundle", 21 | "docs": "https://docs.contao.org" 22 | }, 23 | "funding": [ 24 | { 25 | "type": "other", 26 | "url": "https://to.contao.org/donate" 27 | } 28 | ], 29 | "require": { 30 | "php": "^8.2", 31 | "contao/core-bundle": "self.version", 32 | "symfony/filesystem": "^6.4 || ^7.0", 33 | "symfony/maker-bundle": "^1.1", 34 | "symfony/options-resolver": "^6.4 || ^7.0", 35 | "symfony/yaml": "^6.4 || ^7.0" 36 | }, 37 | "require-dev": { 38 | "contao/manager-plugin": "^2.3.1", 39 | "phpunit/phpunit": "^11.5" 40 | }, 41 | "conflict": { 42 | "contao/core": "*", 43 | "contao/manager-plugin": "<2.0 || >=3.0" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Contao\\MakerBundle\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Contao\\MakerBundle\\Fixtures\\": "tests/Fixtures/src/", 53 | "Contao\\MakerBundle\\Tests\\": "tests/" 54 | } 55 | }, 56 | "config": { 57 | "allow-plugins": { 58 | "contao-components/installer": true, 59 | "contao/manager-plugin": true, 60 | "php-http/discovery": false 61 | } 62 | }, 63 | "extra": { 64 | "contao-manager-plugin": "Contao\\MakerBundle\\ContaoManager\\Plugin" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/callbacks.yaml: -------------------------------------------------------------------------------- 1 | callbacks: 2 | config.onload: 3 | return_type: void 4 | arguments: 5 | dataContainer: Contao\DataContainer 6 | 7 | config.oncreate: 8 | return_type: void 9 | arguments: 10 | table: string 11 | insertId: int 12 | fields: array 13 | dataContainer: Contao\DataContainer 14 | 15 | config.onsubmit: 16 | return_type: void 17 | # Since there are multiple parameters for multiple calls, we cannot 18 | # safely assume the correct parameter names and types 19 | arguments: [] 20 | 21 | config.ondelete: 22 | return_type: void 23 | arguments: 24 | dataContainer: Contao\DataContainer 25 | id: int 26 | 27 | config.oncut: 28 | return_type: void 29 | arguments: 30 | dataContainer: Contao\DataContainer 31 | 32 | config.oncopy: 33 | return_type: void 34 | arguments: 35 | id: int 36 | dataContainer: Contao\DataContainer 37 | 38 | config.oncreate_version: 39 | return_type: void 40 | arguments: 41 | table: string 42 | pid: int 43 | versionNumber: int 44 | recordData: array 45 | 46 | config.onrestore_version: 47 | return_type: void 48 | arguments: 49 | table: string 50 | pid: int 51 | versionNumber: int 52 | recordData: array 53 | 54 | config.onundo: 55 | return_type: void 56 | arguments: 57 | table: string 58 | recordData: array 59 | dataContainer: Contao\DataContainer 60 | 61 | config.oninvalidate_cache_tags: 62 | return_type: array 63 | arguments: 64 | dataContainer: Contao\DataContainer 65 | tags: array 66 | 67 | config.onshow: 68 | return_type: array 69 | arguments: 70 | modalData: array 71 | recordData: array 72 | dataContainer: Contao\DataContainer 73 | 74 | list.sorting.paste_button: 75 | return_type: string 76 | arguments: 77 | dataContainer: Contao\DataContainer 78 | recordData: array 79 | table: string 80 | isCircularReference: bool 81 | clipboardData: array 82 | children: array 83 | previousLabel: string 84 | nextLabel: string 85 | 86 | list.sorting.child_record: 87 | return_type: string 88 | arguments: 89 | recordData: array 90 | 91 | list.sorting.header: 92 | return_type: array 93 | arguments: 94 | currentHeaderLabels: array 95 | dataContainer: Contao\DataContainer 96 | 97 | list.sorting.panel_callback.subpanel: 98 | return_type: string 99 | arguments: 100 | dataContainer: Contao\DataContainer 101 | 102 | list.label.group: 103 | return_type: string 104 | arguments: 105 | group: string 106 | mode: string 107 | field: string 108 | recordData: array 109 | dataContainer: Contao\DataContainer 110 | 111 | list.label.label: 112 | return_type: array 113 | arguments: 114 | recordData: array 115 | currentLabel: string 116 | dataContainer: Contao\DataContainer 117 | # Since there are multiple parameters for multiple calls, we cannot 118 | # safely assume the following correct parameter names and types 119 | 120 | list.global_operations.{operation}.button: 121 | return_type: string 122 | arguments: 123 | buttonHref: ?string 124 | label: string 125 | title: string 126 | className: string 127 | htmlAttributes: string 128 | table: string 129 | rootRecordIds: array 130 | 131 | list.operations.{operation}.button: 132 | return_type: string 133 | arguments: 134 | recordData: array 135 | buttonHref: ?string 136 | label: string 137 | title: string 138 | icon: ?string 139 | htmlAttributes: string 140 | table: string 141 | rootRecordIds: array 142 | childRecordIds: array 143 | isCircularReference: bool 144 | previousLabel: string 145 | nextLabel: string 146 | dataContainer: Contao\DataContainer 147 | 148 | fields.{field}.options: 149 | return_type: array 150 | arguments: 151 | dataContainer: Contao\DataContainer 152 | 153 | fields.{field}.attributes: 154 | return_type: array 155 | arguments: 156 | attributes: array 157 | dataContainer: Contao\DataContainer 158 | body: > 159 | // Do something 160 | return $attributes; 161 | 162 | fields.{field}.input_field: 163 | return_type: string 164 | arguments: 165 | dataContainer: Contao\DataContainer 166 | 167 | fields.{field}.load: 168 | return_type: ~ 169 | arguments: 170 | value: ~ 171 | # Since there are multiple parameters for multiple calls, we cannot 172 | # safely assume the following correct parameter names and types 173 | body: > 174 | return $value; 175 | 176 | fields.{field}.save: 177 | return_type: ~ 178 | arguments: 179 | value: ~ 180 | # Since there are multiple parameters for multiple calls, we cannot 181 | # safely assume the following correct parameter names and types 182 | body: > 183 | return $value; 184 | 185 | fields.{field}.wizard: 186 | return_type: string 187 | arguments: 188 | dataContainer: Contao\DataContainer 189 | 190 | fields.{field}.xlabel: 191 | return_type: string 192 | arguments: 193 | dataContainer: Contao\DataContainer 194 | -------------------------------------------------------------------------------- /config/events.yaml: -------------------------------------------------------------------------------- 1 | events: 2 | contao.backend_menu_build: 3 | return_type: void 4 | arguments: 5 | event: Contao\CoreBundle\Event\MenuEvent 6 | 7 | contao.generate_symlinks: 8 | return_type: void 9 | arguments: 10 | event: Contao\CoreBundle\Event\GenerateSymlinksEvent 11 | 12 | contao.image_sizes_all: 13 | return_type: void 14 | arguments: 15 | event: Contao\CoreBundle\Event\ImageSizesEvent 16 | 17 | contao.image_sizes_user: 18 | return_type: void 19 | arguments: 20 | event: Contao\CoreBundle\Event\ImageSizesEvent 21 | 22 | contao.preview_url_create: 23 | return_type: void 24 | arguments: 25 | event: Contao\CoreBundle\Event\PreviewUrlCreateEvent 26 | 27 | contao.preview_url_convert: 28 | return_type: void 29 | arguments: 30 | event: Contao\CoreBundle\Event\PreviewUrlConvertEvent 31 | 32 | contao.robots_txt: 33 | return_type: void 34 | arguments: 35 | event: Contao\CoreBundle\Event\RobotsTxtEvent 36 | 37 | contao.slug_valid_characters: 38 | return_type: void 39 | arguments: 40 | event: Contao\CoreBundle\Event\SlugValidCharactersEvent 41 | 42 | Contao\CoreBundle\Event\FilterPageTypeEvent: 43 | return_type: void 44 | arguments: 45 | event: Contao\CoreBundle\Event\FilterPageTypeEvent 46 | -------------------------------------------------------------------------------- /config/hooks.yaml: -------------------------------------------------------------------------------- 1 | hooks: 2 | activateAccount: 3 | return_type: void 4 | arguments: 5 | member: Contao\MemberModel 6 | module: Contao\Module 7 | 8 | activateRecipient: 9 | return_type: void 10 | arguments: 11 | mail: string 12 | recipientIds: array 13 | channelIds: array 14 | 15 | addComment: 16 | return_type: void 17 | arguments: 18 | commentId: int 19 | commentData: array 20 | comments: Contao\Comments 21 | 22 | addCustomRegexp: 23 | return_type: bool 24 | arguments: 25 | regexp: string 26 | input: '' 27 | widget: Contao\Widget 28 | 29 | closeAccount: 30 | return_type: void 31 | arguments: 32 | userId: int 33 | mode: string 34 | module: Contao\Module 35 | 36 | colorizeLogEntries: 37 | return_type: string 38 | arguments: 39 | row: array 40 | label: string 41 | 42 | compareThemeFiles: 43 | return_type: string 44 | arguments: 45 | xml: DomDocument 46 | zip: Contao\ZipReader 47 | 48 | compileArticle: 49 | return_type: void 50 | arguments: 51 | template: Contao\FrontendTemplate 52 | data: array 53 | module: Contao\Module 54 | 55 | compileDefinition: 56 | return_type: string 57 | arguments: 58 | row: array 59 | writeToFile: bool 60 | vars: array 61 | parent: array 62 | 63 | compileFormFields: 64 | return_type: array 65 | arguments: 66 | fields: array 67 | formId: string 68 | form: Contao\Form 69 | 70 | createDefinition: 71 | return_type: array 72 | arguments: 73 | key: string 74 | value: string 75 | definition: string 76 | '&dataSet': array 77 | 78 | createNewUser: 79 | return_type: void 80 | arguments: 81 | userId: int 82 | userData: array 83 | module: Contao\Module 84 | 85 | customizeSearch: 86 | return_type: void 87 | arguments: 88 | '&pageIds': array 89 | keywords: string 90 | queryType: string 91 | fuzzy: bool 92 | module: Contao\Module 93 | 94 | executePostActions: 95 | return_type: void 96 | arguments: 97 | action: string 98 | dc: Contao\DataContainer 99 | 100 | executePreActions: 101 | return_type: void 102 | arguments: 103 | action: string 104 | 105 | executeResize: 106 | return_type: ?string 107 | arguments: 108 | image: Contao\Image 109 | 110 | exportTheme: 111 | return_type: void 112 | arguments: 113 | xml: DomDocument 114 | zipArchive: Contao\ZipWriter 115 | themeId: int 116 | 117 | extractThemeFiles: 118 | return_type: void 119 | arguments: 120 | xml: DomDocument 121 | zipArchive: Contao\ZipReader 122 | themeId: int 123 | mapper: array 124 | 125 | generateBreadcrumb: 126 | return_type: array 127 | arguments: 128 | items: array 129 | module: Contao\Module 130 | 131 | generateFrontendUrl: 132 | return_type: string 133 | arguments: 134 | page: array 135 | params: string 136 | url: string 137 | 138 | generatePage: 139 | return_type: void 140 | arguments: 141 | pageModel: Contao\PageModel 142 | layout: Contao\LayoutModel 143 | pageRegular: Contao\PageRegular 144 | 145 | generateXmlFiles: 146 | return_type: void 147 | arguments: [] 148 | 149 | getAllEvents: 150 | return_type: array 151 | arguments: 152 | events: array 153 | calendars: array 154 | timeStart: int 155 | timeEnd: int 156 | module: Contao\Module 157 | 158 | getArticle: 159 | return_type: void 160 | arguments: 161 | article: Contao\ArticleModel 162 | 163 | getArticles: 164 | return_type: ?string 165 | arguments: 166 | pageId: int 167 | column: string 168 | 169 | getAttributesFromDca: 170 | return_type: array 171 | arguments: 172 | attributes: array 173 | dc: [Contao\DataContainer, 'null'] 174 | 175 | getCombinedFile: 176 | return_type: string 177 | arguments: 178 | content: string 179 | key: string 180 | mode: string 181 | file: array 182 | 183 | getContentElement: 184 | return_type: string 185 | arguments: 186 | contentModel: Contao\ContentModel 187 | buffer: string 188 | contentElement: Contao\ContentElement 189 | 190 | getCountries: 191 | return_type: void 192 | arguments: 193 | '&translatedCountries': array 194 | allCountries: array 195 | 196 | getForm: 197 | return_type: string 198 | arguments: 199 | form: Contao\FormModel 200 | buffer: string 201 | 202 | getFrontendModule: 203 | return_type: string 204 | arguments: 205 | moduleModel: Contao\ModuleModel 206 | buffer: string 207 | module: Contao\Module 208 | 209 | getImage: 210 | return_type: ?string 211 | arguments: 212 | originalPath: string 213 | width: int 214 | height: int 215 | mode: string 216 | cacheName: string 217 | file: Contao\File 218 | targetPath: string 219 | imageObject: Contao\Image 220 | 221 | getLanguages: 222 | return_type: void 223 | arguments: 224 | '&compiledLanguages': array 225 | languages: array 226 | langsNative: array 227 | installedOnly: bool 228 | 229 | getPageLayout: 230 | return_type: void 231 | arguments: 232 | pageModel: Contao\PageModel 233 | layout: Contao\LayoutModel 234 | pageRegular: Contao\PageRegular 235 | 236 | getPageStatusIcon: 237 | return_type: string 238 | arguments: 239 | page: object 240 | image: string 241 | 242 | getSystemMessages: 243 | return_type: string 244 | arguments: [] 245 | 246 | getUserNavigation: 247 | return_type: array 248 | arguments: 249 | modules: array 250 | showAll: bool 251 | 252 | indexPage: 253 | return_type: void 254 | arguments: 255 | content: string 256 | pageData: array 257 | '&indexData': array 258 | 259 | initializeSystem: 260 | return_type: void 261 | arguments: [] 262 | 263 | insertTagFlags: 264 | return_type: ~ 265 | arguments: 266 | flag: string 267 | tag: string 268 | cachedValue: string 269 | flags: array 270 | useCache: bool 271 | tags: array 272 | cache: array 273 | _rit: int 274 | _cnt: int 275 | 276 | isAllowedToEditComment: 277 | return_type: bool 278 | arguments: 279 | parentId: int 280 | parentTable: string 281 | 282 | isVisibleElement: 283 | return_type: bool 284 | arguments: 285 | element: Contao\Model 286 | isVisible: bool 287 | 288 | listComments: 289 | return_type: string 290 | arguments: 291 | comments: array 292 | 293 | loadDataContainer: 294 | return_type: void 295 | arguments: 296 | table: string 297 | 298 | loadFormField: 299 | return_type: Contao\Widget 300 | arguments: 301 | widget: Contao\Widget 302 | formId: string 303 | formData: array 304 | form: Contao\Form 305 | 306 | loadLanguageFile: 307 | return_type: void 308 | arguments: 309 | name: string 310 | currentLanguage: string 311 | cacheKey: string 312 | 313 | loadPageDetails: 314 | return_type: void 315 | arguments: 316 | parentModels: array 317 | page: Contao\PageModel 318 | 319 | modifyFrontendPage: 320 | return_type: string 321 | arguments: 322 | buffer: string 323 | templateName: string 324 | 325 | newsListCountItems: 326 | return_type: ~ 327 | arguments: 328 | newsArchives: array 329 | featuredOnly: bool 330 | module: Contao\Module 331 | 332 | newsListFetchItems: 333 | return_type: ~ 334 | arguments: 335 | newsArchives: array 336 | featuredOnly: ?bool 337 | limit: int 338 | offset: int 339 | module: Contao\Module 340 | 341 | outputBackendTemplate: 342 | return_type: string 343 | arguments: 344 | buffer: string 345 | template: string 346 | 347 | outputFrontendTemplate: 348 | return_type: string 349 | arguments: 350 | buffer: string 351 | template: string 352 | 353 | parseArticles: 354 | return_type: void 355 | arguments: 356 | template: Contao\FrontendTemplate 357 | newsEntry: array 358 | module: Contao\Module 359 | 360 | parseDate: 361 | return_type: string 362 | arguments: 363 | formattedDate: string 364 | format: string 365 | timestamp: ?int 366 | 367 | parseFrontendTemplate: 368 | return_type: string 369 | arguments: 370 | buffer: string 371 | templateName: string 372 | template: Contao\FrontendTemplate 373 | 374 | parseTemplate: 375 | return_type: void 376 | arguments: 377 | template: Contao\Template 378 | 379 | parseWidget: 380 | return_type: string 381 | arguments: 382 | buffer: string 383 | widget: Contao\Widget 384 | 385 | postDownload: 386 | return_type: void 387 | arguments: 388 | file: string 389 | 390 | postUpload: 391 | return_type: void 392 | arguments: 393 | files: array 394 | 395 | prepareFormData: 396 | return_type: void 397 | arguments: 398 | '&submittedData': array 399 | labels: array 400 | fields: array 401 | form: Contao\Form 402 | 403 | printArticleAsPdf: 404 | return_type: void 405 | arguments: 406 | articleContent: string 407 | module: Contao\ModuleArticle 408 | 409 | processFormData: 410 | return_type: void 411 | arguments: 412 | submittedData: array 413 | formData: array 414 | files: ?array 415 | labels: array 416 | form: Contao\Form 417 | 418 | removeOldFeeds: 419 | return_type: array 420 | arguments: [] 421 | 422 | removeRecipient: 423 | return_type: void 424 | arguments: 425 | email: string 426 | channels: array 427 | 428 | replaceDynamicScriptTags: 429 | return_type: string 430 | arguments: 431 | buffer: string 432 | 433 | replaceInsertTags: 434 | return_type: ~ 435 | arguments: 436 | insertTag: string 437 | useCache: bool 438 | cachedValue: string 439 | flags: array 440 | tags: array 441 | cache: array 442 | _rit: int 443 | _cnt: int 444 | 445 | reviseTable: 446 | return_type: bool 447 | arguments: 448 | table: string 449 | newRecords: ?array 450 | parentTable: ?string 451 | childTables: ?array 452 | 453 | sendNewsletter: 454 | return_type: void 455 | arguments: 456 | email: Contao\Email 457 | newsletter: Contao\Database\Result 458 | recipient: array 459 | text: string 460 | html: string 461 | 462 | setCookie: 463 | return_type: object 464 | arguments: 465 | cookie: object 466 | 467 | setNewPassword: 468 | return_type: void 469 | arguments: 470 | member: ~ 471 | password: string 472 | module: [Contao\Module, 'null'] 473 | 474 | sqlCompileCommands: 475 | return_type: array 476 | arguments: 477 | sql: array 478 | 479 | sqlGetFromDB: 480 | return_type: array 481 | arguments: 482 | sql: array 483 | 484 | sqlGetFromDca: 485 | return_type: array 486 | arguments: 487 | sql: array 488 | 489 | sqlGetFromFile: 490 | return_type: array 491 | arguments: 492 | sql: array 493 | 494 | storeFormData: 495 | return_type: array 496 | arguments: 497 | data: array 498 | form: Contao\Form 499 | 500 | updatePersonalData: 501 | return_type: void 502 | arguments: 503 | member: Contao\FrontendUser 504 | data: array 505 | module: Contao\Module 506 | 507 | validateFormField: 508 | return_type: Contao\Widget 509 | arguments: 510 | widget: Contao\Widget 511 | formId: string 512 | formData: array 513 | form: Contao\Form 514 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | contao_maker.generator.class: 3 | class: Contao\MakerBundle\Generator\ClassGenerator 4 | arguments: 5 | - '@maker.generator' 6 | 7 | contao_maker.generator.dca: 8 | class: Contao\MakerBundle\Generator\DcaGenerator 9 | arguments: 10 | - '@maker.file_manager' 11 | - '%kernel.project_dir%' 12 | 13 | contao_maker.generator.language_file: 14 | class: Contao\MakerBundle\Generator\LanguageFileGenerator 15 | arguments: 16 | - '@maker.file_manager' 17 | - '%kernel.project_dir%' 18 | 19 | contao_maker.generator.template: 20 | class: Contao\MakerBundle\Generator\TemplateGenerator 21 | arguments: 22 | - '@maker.generator' 23 | 24 | contao_maker.maker.make_content_element: 25 | class: Contao\MakerBundle\Maker\MakeContentElement 26 | arguments: 27 | - '@contao.framework' 28 | - '@contao_maker.generator.template' 29 | - '@contao_maker.generator.class' 30 | - '@contao_maker.generator.dca' 31 | - '@contao_maker.generator.language_file' 32 | - '@maker.file_manager' 33 | - '%kernel.project_dir%' 34 | tags: 35 | - maker.command 36 | 37 | contao_maker.maker.make_dca_callback: 38 | class: Contao\MakerBundle\Maker\MakeDcaCallback 39 | arguments: 40 | - '@contao.framework' 41 | - '@contao_maker.generator.class' 42 | - '@contao.resource_finder' 43 | - '@contao_maker.reflection.signature_generator' 44 | - '@contao_maker.reflection.import_extractor' 45 | tags: 46 | - maker.command 47 | 48 | contao_maker.maker.make_event_listener: 49 | class: Contao\MakerBundle\Maker\MakeEventListener 50 | arguments: 51 | - '@contao_maker.generator.class' 52 | - '@contao_maker.reflection.signature_generator' 53 | - '@contao_maker.reflection.import_extractor' 54 | tags: 55 | - maker.command 56 | 57 | contao_maker.maker.make_frontend_module: 58 | class: Contao\MakerBundle\Maker\MakeFrontendModule 59 | arguments: 60 | - '@contao.framework' 61 | - '@contao_maker.generator.template' 62 | - '@contao_maker.generator.class' 63 | - '@contao_maker.generator.dca' 64 | - '@contao_maker.generator.language_file' 65 | - '@maker.file_manager' 66 | - '%kernel.project_dir%' 67 | tags: 68 | - maker.command 69 | 70 | contao_maker.maker.make_hook: 71 | class: Contao\MakerBundle\Maker\MakeHook 72 | arguments: 73 | - '@contao_maker.generator.class' 74 | - '@contao_maker.reflection.signature_generator' 75 | - '@contao_maker.reflection.import_extractor' 76 | tags: 77 | - maker.command 78 | 79 | contao_maker.reflection.import_extractor: 80 | class: Contao\MakerBundle\Reflection\ImportExtractor 81 | 82 | contao_maker.reflection.signature_generator: 83 | class: Contao\MakerBundle\Reflection\SignatureGenerator 84 | -------------------------------------------------------------------------------- /skeleton/content-element/ContentElement.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | declare(strict_types=1); 4 | 5 | namespace App\Controller\ContentElement; 6 | 7 | use Contao\ContentModel; 8 | use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController; 9 | use Contao\CoreBundle\DependencyInjection\Attribute\AsContentElement; 10 | use Contao\CoreBundle\Twig\FragmentTemplate; 11 | use Symfony\Component\HttpFoundation\Request; 12 | use Symfony\Component\HttpFoundation\Response; 13 | 14 | #[AsContentElement(category: '')] 15 | class extends AbstractContentElementController 16 | { 17 | protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response 18 | { 19 | return $template->getResponse(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /skeleton/content-element/content_element.tpl.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "@Contao/content_element/_base.html.twig" %} 2 | 3 | {% block content %} 4 |

Put your content here.

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /skeleton/content-element/tl_content.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | declare(strict_types=1); 5 | 6 | 7 | $GLOBALS['TL_DCA']['tl_content']['palettes'][''] = ' 8 | {type_legend},type,headline; 9 | {template_legend:hide},customTpl; 10 | {protected_legend:hide},protected; 11 | {expert_legend:hide},cssID; 12 | {invisible_legend:hide},invisible,start,stop 13 | '; 14 | -------------------------------------------------------------------------------- /skeleton/dca-callback/Callback.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | declare(strict_types=1); 4 | 5 | namespace App\EventListener\DataContainer; 6 | 7 | use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback; 8 | 9 | use ; 10 | 11 | 12 | #[AsCallback(table: '', target: '')] 13 | class 14 | { 15 | 16 | { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /skeleton/event-listener/EventListener.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | declare(strict_types=1); 4 | 5 | namespace App\EventListener; 6 | 7 | 8 | use ; 9 | 10 | use Symfony\Component\EventDispatcher\Attribute\AsEventListener; 11 | 12 | #[AsEventListener('')] 13 | class 14 | { 15 | 16 | { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /skeleton/frontend-module/FrontendModule.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | declare(strict_types=1); 4 | 5 | namespace App\Controller\FrontendModule; 6 | 7 | use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController; 8 | use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule; 9 | use Contao\ModuleModel; 10 | use Contao\CoreBundle\Twig\FragmentTemplate; 11 | use Symfony\Component\HttpFoundation\Request; 12 | use Symfony\Component\HttpFoundation\Response; 13 | 14 | #[AsFrontendModule(category: '')] 15 | class extends AbstractFrontendModuleController 16 | { 17 | protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response 18 | { 19 | return $template->getResponse(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /skeleton/frontend-module/frontend_module.tpl.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "@Contao/frontend_module/_base.html.twig" %} 2 | 3 | {% block content %} 4 |

Put your content here.

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /skeleton/frontend-module/tl_module.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | declare(strict_types=1); 5 | 6 | 7 | $GLOBALS['TL_DCA']['tl_module']['palettes'][''] = ' 8 | {title_legend},name,headline,type; 9 | {template_legend:hide},customTpl; 10 | {protected_legend:hide},protected; 11 | {expert_legend:hide},cssID 12 | '; 13 | -------------------------------------------------------------------------------- /skeleton/hook/Hook.tpl.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | declare(strict_types=1); 4 | 5 | namespace App\EventListener; 6 | 7 | use Contao\CoreBundle\DependencyInjection\Attribute\AsHook; 8 | 9 | use ; 10 | 11 | 12 | #[AsHook('')] 13 | class 14 | { 15 | 16 | { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ContaoMakerBundle.php: -------------------------------------------------------------------------------- 1 | setLoadAfter([MakerBundle::class]), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DependencyInjection/ContaoMakerExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yaml'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Generator/ClassGenerator.php: -------------------------------------------------------------------------------- 1 | getOptionsResolver()->resolve($options); 28 | 29 | return $this->generator->generateClass( 30 | $options['fqcn'], 31 | $this->getSourcePath($options['source']), 32 | $options['variables'], 33 | ); 34 | } 35 | 36 | private function getOptionsResolver(): OptionsResolver 37 | { 38 | $resolver = new OptionsResolver(); 39 | $resolver->setRequired(['fqcn', 'source']); 40 | $resolver->setDefaults(['variables' => []]); 41 | 42 | return $resolver; 43 | } 44 | 45 | private function getSourcePath(string $path): string 46 | { 47 | return Path::join(__DIR__.'/../../skeleton', $path); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Generator/DcaGenerator.php: -------------------------------------------------------------------------------- 1 | getOptionsResolver()->resolve($options); 30 | 31 | $source = $this->getSourcePath($options['source']); 32 | $target = Path::join($this->projectDir, 'contao/dca', $options['domain'].'.php'); 33 | $fileExists = $this->fileManager->fileExists($target); 34 | 35 | $variables = [ 36 | 'append' => $fileExists, 37 | 'element_name' => $options['element'], 38 | ...$options['variables'], 39 | ]; 40 | 41 | $contents = $this->fileManager->parseTemplate($source, $variables); 42 | $contents = ltrim($contents); 43 | 44 | if ($fileExists) { 45 | $contents = file_get_contents($target)."\n".rtrim($contents)."\n"; 46 | } 47 | 48 | $this->fileManager->dumpFile($target, $contents); 49 | 50 | return Path::join('contao/dca', $options['domain'].'.php'); 51 | } 52 | 53 | private function getOptionsResolver(): OptionsResolver 54 | { 55 | $resolver = new OptionsResolver(); 56 | $resolver->setRequired(['domain', 'source', 'element']); 57 | $resolver->setDefaults(['variables' => []]); 58 | 59 | return $resolver; 60 | } 61 | 62 | private function getSourcePath(string $path): string 63 | { 64 | return Path::join(__DIR__.'/../../skeleton', $path); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Generator/GeneratorInterface.php: -------------------------------------------------------------------------------- 1 | $options 19 | */ 20 | public function generate(array $options): string; 21 | } 22 | -------------------------------------------------------------------------------- /src/Generator/LanguageFileGenerator.php: -------------------------------------------------------------------------------- 1 | getOptionsResolver()->resolve($options); 31 | $target = Path::join($this->projectDir, 'translations', \sprintf('%s.%s.yaml', $options['domain'], $options['language'])); 32 | 33 | if ($this->fileManager->fileExists($target)) { 34 | $translations = Yaml::parse($this->fileManager->getFileContents($target)); 35 | } else { 36 | $translations = []; 37 | } 38 | 39 | $translations = array_merge_recursive($translations, $options['variables']); 40 | 41 | $this->fileManager->dumpFile($target, Yaml::dump($translations, inline: 10)); 42 | 43 | return Path::makeRelative($target, $this->projectDir); 44 | } 45 | 46 | private function getOptionsResolver(): OptionsResolver 47 | { 48 | $resolver = new OptionsResolver(); 49 | $resolver->setRequired(['domain', 'language', 'variables']); 50 | $resolver->setAllowedTypes('variables', ['array']); 51 | 52 | return $resolver; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Generator/TemplateGenerator.php: -------------------------------------------------------------------------------- 1 | getOptionsResolver()->resolve($options); 28 | 29 | $this->generator->generateFile( 30 | $options['target'], 31 | $this->getSourcePath($options['source']), 32 | $options['variables'], 33 | ); 34 | 35 | return $options['target']; 36 | } 37 | 38 | private function getOptionsResolver(): OptionsResolver 39 | { 40 | $resolver = new OptionsResolver(); 41 | $resolver->setRequired(['target', 'source']); 42 | $resolver->setDefaults(['variables' => []]); 43 | 44 | return $resolver; 45 | } 46 | 47 | private function getSourcePath(string $path): string 48 | { 49 | return Path::join(__DIR__.'/../../skeleton', $path); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Maker/AbstractFragmentMaker.php: -------------------------------------------------------------------------------- 1 | askForCategory($input, $io, $command); 51 | $this->askForDcaPalette($input, $io, $command); 52 | $this->askForTranslation($input, $io, $command); 53 | 54 | if ($input->getArgument('add-translation')) { 55 | $this->askForSourceName($input, $io, $command); 56 | $this->askForSourceDescription($input, $io, $command); 57 | 58 | $i = 0; 59 | 60 | while (true) { 61 | $this->askForAdditionalTranslation($input, $io, $command, $i); 62 | 63 | if (!$input->getArgument('add-translation-'.$i)) { 64 | break; 65 | } 66 | 67 | $this->askForLanguage($input, $io, $command, $i); 68 | $this->askForTargetName($input, $io, $command, $i); 69 | $this->askForTargetDescription($input, $io, $command, $i); 70 | 71 | ++$i; 72 | } 73 | } 74 | } 75 | 76 | public function configureDependencies(DependencyBuilder $dependencies): void 77 | { 78 | } 79 | 80 | abstract protected function getGlobalsRegistryKey(): string; 81 | 82 | abstract protected function getTemplatePrefix(): string; 83 | 84 | protected function getTemplateName(string $className): string 85 | { 86 | return Path::join( 87 | $this->projectDir, 88 | 'contao/templates', 89 | $this->getTemplatePrefix(), 90 | \sprintf('%s.html.twig', Container::underscore($className)), 91 | ); 92 | } 93 | 94 | /** 95 | * @return array 96 | */ 97 | protected function getCategories(): array 98 | { 99 | $this->framework->initialize(); 100 | 101 | return array_keys((array) $GLOBALS[$this->getGlobalsRegistryKey()]); 102 | } 103 | 104 | protected function getClassNameWithoutSuffix(string $className): string 105 | { 106 | if (str_ends_with($className, 'Controller')) { 107 | $className = substr($className, 0, -10); 108 | } 109 | 110 | return $className; 111 | } 112 | 113 | private function askForCategory(InputInterface $input, ConsoleStyle $io, Command $command): void 114 | { 115 | $command->addArgument('category', InputArgument::REQUIRED); 116 | 117 | $categories = $this->getCategories(); 118 | 119 | $io->writeln(' Suggested categories:'); 120 | $io->listing($categories); 121 | 122 | $attributeClass = match (static::class) { 123 | MakeContentElement::class => AsContentElement::class, 124 | MakeFrontendModule::class => AsFrontendModule::class, 125 | default => null, 126 | }; 127 | 128 | $default = null; 129 | 130 | if ($attributeClass) { 131 | $reflection = new \ReflectionClass($attributeClass); 132 | $params = $reflection->getConstructor()->getParameters(); 133 | 134 | foreach ($params as $param) { 135 | if ('category' === $param->getName()) { 136 | $default = $param->getDefaultValue(); 137 | 138 | break; 139 | } 140 | } 141 | } 142 | 143 | $question = new Question('Choose a category', $default); 144 | $question->setAutocompleterValues($categories); 145 | 146 | $category = (string) $io->askQuestion($question); 147 | 148 | $input->setArgument('category', $category === (string) $default ? null : $category); 149 | } 150 | 151 | private function askForDcaPalette(InputInterface $input, ConsoleStyle $io, Command $command): void 152 | { 153 | $command->addArgument('add-palette', InputArgument::REQUIRED); 154 | 155 | $question = new ConfirmationQuestion('Do you want to add a palette?'); 156 | 157 | $input->setArgument('add-palette', $io->askQuestion($question)); 158 | } 159 | 160 | private function askForTranslation(InputInterface $input, ConsoleStyle $io, Command $command): void 161 | { 162 | $command->addArgument('add-translation', InputArgument::REQUIRED); 163 | 164 | $question = new ConfirmationQuestion('Do you want to add a translation?'); 165 | 166 | $input->setArgument('add-translation', $io->askQuestion($question)); 167 | } 168 | 169 | private function askForSourceName(InputInterface $input, ConsoleStyle $io, Command $command): void 170 | { 171 | $command->addArgument('source-name', InputArgument::OPTIONAL); 172 | 173 | $question = new Question('Enter the English name'); 174 | $question->setValidator(Validator::notBlank(...)); 175 | 176 | $input->setArgument('source-name', $io->askQuestion($question)); 177 | } 178 | 179 | private function askForSourceDescription(InputInterface $input, ConsoleStyle $io, Command $command): void 180 | { 181 | $command->addArgument('source-description', InputArgument::OPTIONAL); 182 | 183 | $question = new Question('Enter the English description'); 184 | $question->setValidator(Validator::notBlank(...)); 185 | 186 | $input->setArgument('source-description', $io->askQuestion($question)); 187 | } 188 | 189 | private function askForAdditionalTranslation(InputInterface $input, ConsoleStyle $io, Command $command, int $count): void 190 | { 191 | $command->addArgument('add-translation-'.$count, InputArgument::OPTIONAL); 192 | 193 | $question = new ConfirmationQuestion('Do you want to add another translation?', false); 194 | 195 | $input->setArgument('add-translation-'.$count, $io->askQuestion($question)); 196 | } 197 | 198 | private function askForLanguage(InputInterface $input, ConsoleStyle $io, Command $command, int $count): void 199 | { 200 | $command->addArgument('language-'.$count, InputArgument::OPTIONAL); 201 | 202 | $question = new Question('Which language do you want to add? (e.g. de)'); 203 | $question->setValidator(Validator::notBlank(...)); 204 | 205 | $input->setArgument('language-'.$count, $io->askQuestion($question)); 206 | } 207 | 208 | private function askForTargetName(InputInterface $input, ConsoleStyle $io, Command $command, int $count): void 209 | { 210 | $command->addArgument('target-name-'.$count, InputArgument::OPTIONAL); 211 | 212 | $question = new Question('Enter the translated name'); 213 | $question->setValidator(Validator::notBlank(...)); 214 | 215 | $input->setArgument('target-name-'.$count, $io->askQuestion($question)); 216 | } 217 | 218 | private function askForTargetDescription(InputInterface $input, ConsoleStyle $io, Command $command, int $count): void 219 | { 220 | $command->addArgument('target-description-'.$count, InputArgument::OPTIONAL); 221 | 222 | $question = new Question('Enter the translated description'); 223 | $question->setValidator(Validator::notBlank(...)); 224 | 225 | $input->setArgument('target-description-'.$count, $io->askQuestion($question)); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/Maker/MakeContentElement.php: -------------------------------------------------------------------------------- 1 | addArgument('element-class', InputArgument::REQUIRED, \sprintf('Enter a class name for the element controller (e.g. %sController)', Str::asClassName(Str::getRandomTerm()))) 41 | ; 42 | } 43 | 44 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 45 | { 46 | $category = $input->getArgument('category'); 47 | $addPalette = $input->getArgument('add-palette'); 48 | $addTranslation = $input->getArgument('add-translation'); 49 | $name = $input->getArgument('element-class'); 50 | 51 | $className = Str::asClassName($name); 52 | $classNameWithoutSuffix = $this->getClassNameWithoutSuffix($className); 53 | $elementName = Container::underscore($classNameWithoutSuffix); 54 | $elementDetails = $generator->createClassNameDetails($name, 'Controller\ContentElement\\'); 55 | 56 | $this->classGenerator->generate([ 57 | 'source' => 'content-element/ContentElement.tpl.php', 58 | 'fqcn' => $elementDetails->getFullName(), 59 | 'variables' => [ 60 | 'className' => $elementDetails->getShortName(), 61 | 'elementName' => $elementName, 62 | 'category' => $category, 63 | ], 64 | ]); 65 | 66 | $this->templateGenerator->generate([ 67 | 'source' => 'content-element/content_element.tpl.html.twig', 68 | 'target' => $this->getTemplateName($classNameWithoutSuffix), 69 | ]); 70 | 71 | $twigRoot = Path::join($this->projectDir, 'contao/templates/.twig-root'); 72 | 73 | if (!$this->fileManager->fileExists($twigRoot)) { 74 | $this->fileManager->dumpFile($twigRoot, ''); 75 | } 76 | 77 | if ($addPalette) { 78 | $this->dcaGenerator->generate([ 79 | 'source' => 'content-element/tl_content.tpl.php', 80 | 'domain' => 'tl_content', 81 | 'element' => $elementName, 82 | ]); 83 | } 84 | 85 | if ($addTranslation) { 86 | $this->languageFileGenerator->generate([ 87 | 'domain' => 'contao_default', 88 | 'language' => 'en', 89 | 'variables' => [ 90 | 'CTE' => [ 91 | $elementName => [ 92 | $input->getArgument('source-name'), 93 | $input->getArgument('source-description'), 94 | ], 95 | ], 96 | ], 97 | ]); 98 | 99 | $i = 0; 100 | 101 | while (true) { 102 | $hasNext = $input->hasArgument('add-translation-'.$i); 103 | 104 | if (!$hasNext || false === $input->getArgument('add-translation-'.$i)) { 105 | break; 106 | } 107 | 108 | $this->languageFileGenerator->generate([ 109 | 'domain' => 'contao_default', 110 | 'language' => $input->getArgument('language-'.$i), 111 | 'variables' => [ 112 | 'CTE' => [ 113 | $elementName => [ 114 | $input->getArgument('target-name-'.$i), 115 | $input->getArgument('target-description-'.$i), 116 | ], 117 | ], 118 | ], 119 | ]); 120 | 121 | ++$i; 122 | } 123 | } 124 | 125 | $generator->writeChanges(); 126 | 127 | $this->writeSuccessMessage($io); 128 | } 129 | 130 | protected function getGlobalsRegistryKey(): string 131 | { 132 | return 'TL_CTE'; 133 | } 134 | 135 | protected function getTemplatePrefix(): string 136 | { 137 | return 'content_element'; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Maker/MakeDcaCallback.php: -------------------------------------------------------------------------------- 1 | addArgument('callback-class', InputArgument::REQUIRED, \sprintf('Enter a class name for the callback (e.g. %sListener)', Str::asClassName(Str::getRandomTerm()))) 60 | ; 61 | } 62 | 63 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 64 | { 65 | $this->askForTable($input, $io, $command); 66 | $this->askForTarget($input, $io, $command); 67 | } 68 | 69 | public function configureDependencies(DependencyBuilder $dependencies): void 70 | { 71 | } 72 | 73 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 74 | { 75 | $table = $input->getArgument('table'); 76 | $target = $input->getArgument('target'); 77 | $name = $input->getArgument('callback-class'); 78 | 79 | $targets = $this->getTargets(); 80 | 81 | if (!\array_key_exists($target, $targets)) { 82 | $io->error('Invalid DCA callback: '.$target); 83 | 84 | return; 85 | } 86 | 87 | $definition = $targets[$target]; 88 | $elementDetails = $generator->createClassNameDetails($name, 'EventListener\DataContainer\\'); 89 | 90 | if (str_contains((string) $target, '{')) { 91 | $chunks = explode('.', (string) $target); 92 | 93 | foreach ($chunks as $chunk) { 94 | if ('{' === $chunk[0]) { 95 | $target = str_replace($chunk, $input->getArgument($chunk), $target); 96 | } 97 | } 98 | } 99 | 100 | $this->classGenerator->generate([ 101 | 'source' => 'dca-callback/Callback.tpl.php', 102 | 'fqcn' => $elementDetails->getFullName(), 103 | 'variables' => [ 104 | 'uses' => $this->importExtractor->extract($definition), 105 | 'table' => $table, 106 | 'target' => $target, 107 | 'className' => $elementDetails->getShortName(), 108 | 'signature' => $this->signatureGenerator->generate($definition, '__invoke'), 109 | 'body' => $definition->getBody(), 110 | ], 111 | ]); 112 | 113 | $generator->writeChanges(); 114 | 115 | $this->writeSuccessMessage($io); 116 | } 117 | 118 | private function askForTable(InputInterface $input, ConsoleStyle $io, Command $command): void 119 | { 120 | $command->addArgument('table', InputArgument::REQUIRED); 121 | 122 | $tables = $this->getTables(); 123 | 124 | $io->writeln(' Suggested tables:'); 125 | $io->listing($tables); 126 | 127 | $question = new Question('Enter a table for the callback'); 128 | $question->setAutocompleterValues($tables); 129 | $question->setValidator(Validator::notBlank(...)); 130 | 131 | $input->setArgument('table', $io->askQuestion($question)); 132 | } 133 | 134 | private function askForTarget(InputInterface $input, ConsoleStyle $io, Command $command): void 135 | { 136 | $command->addArgument('target', InputArgument::REQUIRED); 137 | 138 | $targets = $this->getTargets(); 139 | 140 | $io->writeln(' Suggested targets:'); 141 | $io->listing(array_keys($targets)); 142 | 143 | $question = new Question('Enter a target for the callback'); 144 | $question->setAutocompleterValues(array_keys($targets)); 145 | $question->setValidator(Validator::notBlank(...)); 146 | 147 | $target = $io->askQuestion($question); 148 | 149 | if (str_contains((string) $target, '{')) { 150 | $chunks = explode('.', (string) $target); 151 | 152 | foreach ($chunks as $chunk) { 153 | if ('{' !== $chunk[0]) { 154 | continue; 155 | } 156 | 157 | $command->addArgument($chunk, InputArgument::OPTIONAL); 158 | 159 | $question = new Question(\sprintf('Please enter a value for "%s"', $chunk)); 160 | $question->setValidator(Validator::notBlank(...)); 161 | 162 | $input->setArgument($chunk, $io->askQuestion($question)); 163 | } 164 | } 165 | 166 | $input->setArgument('target', $target); 167 | } 168 | 169 | /** 170 | * @return array 171 | */ 172 | private function getTables(): array 173 | { 174 | $this->framework->initialize(); 175 | 176 | $files = $this->resourceFinder->findIn('dca')->depth(0)->files()->name('*.php'); 177 | 178 | $tables = array_map( 179 | static fn (SplFileInfo $input) => str_replace('.php', '', $input->getRelativePathname()), 180 | iterator_to_array($files->getIterator()), 181 | ); 182 | 183 | $tables = array_values($tables); 184 | 185 | return array_unique($tables); 186 | } 187 | 188 | /** 189 | * @return array 190 | */ 191 | private function getTargets(): array 192 | { 193 | $yaml = Yaml::parseFile(__DIR__.'/../../config/callbacks.yaml'); 194 | $targets = []; 195 | 196 | foreach ($yaml['callbacks'] as $key => $config) { 197 | $targets[$key] = new MethodDefinition($config['return_type'], $config['arguments'], $config['body'] ?? null); 198 | } 199 | 200 | return $targets; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Maker/MakeEventListener.php: -------------------------------------------------------------------------------- 1 | addArgument('event-class', InputArgument::OPTIONAL, \sprintf('Enter a class name for the listener (e.g. %sListener)', Str::asClassName(Str::getRandomTerm()))) 54 | ; 55 | } 56 | 57 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 58 | { 59 | $command->addArgument('event', InputArgument::OPTIONAL); 60 | 61 | $events = $this->getAvailableEvents(); 62 | 63 | $io->writeln(' Available events:'); 64 | $io->listing(array_keys($events)); 65 | 66 | $question = new Question('Choose the event to listen for'); 67 | $question->setAutocompleterValues(array_keys($events)); 68 | 69 | $input->setArgument('event', $io->askQuestion($question)); 70 | } 71 | 72 | public function configureDependencies(DependencyBuilder $dependencies): void 73 | { 74 | } 75 | 76 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 77 | { 78 | $event = $input->getArgument('event'); 79 | $name = $input->getArgument('event-class'); 80 | $events = $this->getAvailableEvents(); 81 | 82 | if (!\array_key_exists($event, $events)) { 83 | $io->error('Invalid event name: '.$event); 84 | 85 | return; 86 | } 87 | 88 | $definition = $events[$event]; 89 | $elementDetails = $generator->createClassNameDetails($name, 'EventListener\\'); 90 | 91 | $this->classGenerator->generate([ 92 | 'source' => 'event-listener/EventListener.tpl.php', 93 | 'fqcn' => $elementDetails->getFullName(), 94 | 'variables' => [ 95 | 'uses' => $this->importExtractor->extract($definition), 96 | 'event' => $event, 97 | 'className' => $elementDetails->getShortName(), 98 | 'signature' => $this->signatureGenerator->generate($definition, '__invoke'), 99 | 'body' => $definition->getBody(), 100 | ], 101 | ]); 102 | 103 | $generator->writeChanges(); 104 | 105 | $this->writeSuccessMessage($io); 106 | } 107 | 108 | /** 109 | * @return array 110 | */ 111 | private function getAvailableEvents(): array 112 | { 113 | $yaml = Yaml::parseFile(__DIR__.'/../../config/events.yaml'); 114 | $events = []; 115 | 116 | foreach ($yaml['events'] as $key => $config) { 117 | $events[$key] = new MethodDefinition($config['return_type'] ?? null, $config['arguments'] ?? [], $config['body'] ?? null); 118 | } 119 | 120 | return $events; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Maker/MakeFrontendModule.php: -------------------------------------------------------------------------------- 1 | addArgument('module-class', InputArgument::REQUIRED, \sprintf('Enter a class name for the module controller (e.g. %sController)', Str::asClassName(Str::getRandomTerm()))) 42 | ; 43 | } 44 | 45 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 46 | { 47 | $category = $input->getArgument('category'); 48 | $addPalette = $input->getArgument('add-palette'); 49 | $addTranslation = $input->getArgument('add-translation'); 50 | $name = $input->getArgument('module-class'); 51 | 52 | $className = Str::asClassName($name); 53 | $classNameWithoutSuffix = $this->getClassNameWithoutSuffix($className); 54 | $elementName = Container::underscore($classNameWithoutSuffix); 55 | $elementDetails = $generator->createClassNameDetails($name, 'Controller\\FrontendModule\\'); 56 | 57 | $this->classGenerator->generate([ 58 | 'source' => 'frontend-module/FrontendModule.tpl.php', 59 | 'fqcn' => $elementDetails->getFullName(), 60 | 'variables' => [ 61 | 'className' => $elementDetails->getShortName(), 62 | 'elementName' => $elementName, 63 | 'category' => $category, 64 | ], 65 | ]); 66 | 67 | $this->templateGenerator->generate([ 68 | 'source' => 'frontend-module/frontend_module.tpl.html.twig', 69 | 'target' => $this->getTemplateName($classNameWithoutSuffix), 70 | ]); 71 | 72 | $twigRoot = Path::join($this->projectDir, 'contao/templates/.twig-root'); 73 | 74 | if (!$this->fileManager->fileExists($twigRoot)) { 75 | $this->fileManager->dumpFile($twigRoot, ''); 76 | } 77 | 78 | if ($addPalette) { 79 | $this->dcaGenerator->generate([ 80 | 'source' => 'frontend-module/tl_module.tpl.php', 81 | 'domain' => 'tl_module', 82 | 'element' => $elementName, 83 | ]); 84 | } 85 | 86 | if ($addTranslation) { 87 | $this->languageFileGenerator->generate([ 88 | 'domain' => 'contao_modules', 89 | 'language' => 'en', 90 | 'variables' => [ 91 | 'FMD' => [ 92 | $elementName => [ 93 | $input->getArgument('source-name'), 94 | $input->getArgument('source-description'), 95 | ], 96 | ], 97 | ], 98 | ]); 99 | 100 | $i = 0; 101 | 102 | while (true) { 103 | $hasNext = $input->hasArgument('add-translation-'.$i); 104 | 105 | if (!$hasNext || false === $input->getArgument('add-translation-'.$i)) { 106 | break; 107 | } 108 | 109 | $this->languageFileGenerator->generate([ 110 | 'domain' => 'contao_modules', 111 | 'language' => $input->getArgument('language-'.$i), 112 | 'variables' => [ 113 | 'FMD' => [ 114 | $elementName => [ 115 | $input->getArgument('target-name-'.$i), 116 | $input->getArgument('target-description-'.$i), 117 | ], 118 | ], 119 | ], 120 | ]); 121 | 122 | ++$i; 123 | } 124 | } 125 | 126 | $generator->writeChanges(); 127 | 128 | $this->writeSuccessMessage($io); 129 | } 130 | 131 | public function configureDependencies(DependencyBuilder $dependencies): void 132 | { 133 | } 134 | 135 | protected function getGlobalsRegistryKey(): string 136 | { 137 | return 'FE_MOD'; 138 | } 139 | 140 | protected function getTemplatePrefix(): string 141 | { 142 | return 'frontend_module'; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Maker/MakeHook.php: -------------------------------------------------------------------------------- 1 | addArgument('hook-class', InputArgument::REQUIRED, \sprintf('Enter a class name for the listener (e.g. %sListener)', Str::asClassName(Str::getRandomTerm()))) 55 | ; 56 | } 57 | 58 | public function interact(InputInterface $input, ConsoleStyle $io, Command $command): void 59 | { 60 | $command->addArgument('hook', InputArgument::REQUIRED); 61 | 62 | $hooks = $this->getAvailableHooks(); 63 | 64 | $io->writeln(' Available hooks:'); 65 | $io->listing(array_keys($hooks)); 66 | 67 | $question = new Question('Choose the hook to listen for'); 68 | $question->setAutocompleterValues(array_keys($hooks)); 69 | $question->setValidator(Validator::notBlank(...)); 70 | 71 | $input->setArgument('hook', $io->askQuestion($question)); 72 | } 73 | 74 | public function configureDependencies(DependencyBuilder $dependencies): void 75 | { 76 | } 77 | 78 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator): void 79 | { 80 | $hook = $input->getArgument('hook'); 81 | $name = $input->getArgument('hook-class'); 82 | $hooks = $this->getAvailableHooks(); 83 | 84 | if (!\array_key_exists($hook, $hooks)) { 85 | $io->error('Invalid hook name: '.$hook); 86 | 87 | return; 88 | } 89 | 90 | $definition = $hooks[$hook]; 91 | $elementDetails = $generator->createClassNameDetails($name, 'EventListener\\'); 92 | 93 | $this->classGenerator->generate([ 94 | 'source' => 'hook/Hook.tpl.php', 95 | 'fqcn' => $elementDetails->getFullName(), 96 | 'variables' => [ 97 | 'uses' => $this->importExtractor->extract($definition), 98 | 'hook' => $hook, 99 | 'className' => $elementDetails->getShortName(), 100 | 'signature' => $this->signatureGenerator->generate($definition, '__invoke'), 101 | 'body' => $definition->getBody(), 102 | ], 103 | ]); 104 | 105 | $generator->writeChanges(); 106 | 107 | $this->writeSuccessMessage($io); 108 | } 109 | 110 | /** 111 | * @return array 112 | */ 113 | private function getAvailableHooks(): array 114 | { 115 | $yaml = Yaml::parseFile(__DIR__.'/../../config/hooks.yaml'); 116 | $hooks = []; 117 | 118 | foreach ($yaml['hooks'] as $key => $config) { 119 | $hooks[$key] = new MethodDefinition($config['return_type'] ?? null, $config['arguments'] ?? [], $config['body'] ?? null); 120 | } 121 | 122 | return $hooks; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Reflection/ImportExtractor.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function extract(MethodDefinition $method): array 21 | { 22 | $objectTypeHints = []; 23 | 24 | foreach ($method->getParameters() as $parameter) { 25 | if (null === $parameter) { 26 | continue; 27 | } 28 | 29 | $type = \is_array($parameter) ? $parameter[0] : $parameter; 30 | 31 | if (!class_exists((string) $type)) { 32 | continue; 33 | } 34 | 35 | $objectTypeHints[] = $type; 36 | } 37 | 38 | $returnType = $method->getReturnType(); 39 | 40 | // If a return type is set, check if the class exists and add it to our imports 41 | if (null !== $returnType && class_exists($returnType)) { 42 | $objectTypeHints[] = $returnType; 43 | } 44 | 45 | $objectTypeHints = array_unique($objectTypeHints); 46 | sort($objectTypeHints); 47 | 48 | return $objectTypeHints; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Reflection/MethodDefinition.php: -------------------------------------------------------------------------------- 1 | $parameters 19 | */ 20 | public function __construct( 21 | private readonly string|null $returnType, 22 | private readonly array $parameters, 23 | private readonly string|null $body = null, 24 | ) { 25 | } 26 | 27 | public function getReturnType(): string|null 28 | { 29 | return $this->returnType; 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function getParameters(): array 36 | { 37 | return $this->parameters; 38 | } 39 | 40 | public function getBody(): string 41 | { 42 | if (null !== $this->body) { 43 | return $this->body; 44 | } 45 | 46 | return match ($this->returnType) { 47 | 'string' => "return '';", 48 | '?string' => 'return null;', 49 | 'array' => 'return [];', 50 | 'bool' => 'return true;', 51 | default => '// Do something', 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Reflection/SignatureGenerator.php: -------------------------------------------------------------------------------- 1 | getParameters() as $name => $type) { 24 | $defaultValue = null; 25 | 26 | if (\is_array($type)) { 27 | [$type, $defaultValue] = $type; 28 | } 29 | 30 | $paramName = str_replace('&', '', $name); 31 | [$paramType] = \is_array($type) ? $type : [$type, null]; 32 | 33 | if (null !== $paramType && class_exists($paramType)) { 34 | $paramType = Str::getShortClassName($paramType); 35 | } 36 | 37 | $paramReference = str_starts_with($name, '&'); 38 | $parameterTemplate = \sprintf('%s %s$%s', $paramType, $paramReference ? '&' : '', $paramName); 39 | 40 | if (null !== $defaultValue) { 41 | $parameterTemplate = \sprintf('%s = %s', $parameterTemplate, $defaultValue); 42 | } 43 | 44 | $parameterTemplate = trim($parameterTemplate); 45 | $parameterTemplates[] = $parameterTemplate; 46 | } 47 | 48 | return \sprintf( 49 | 'public function %s(%s)%s', 50 | $methodName, 51 | implode(', ', $parameterTemplates), 52 | $this->getReturnType($method), 53 | ); 54 | } 55 | 56 | private function getReturnType(MethodDefinition $method): string 57 | { 58 | $returnType = $method->getReturnType(); 59 | 60 | if (null !== $returnType && class_exists($returnType)) { 61 | $returnType = Str::getShortClassName($returnType); 62 | } 63 | 64 | return $returnType ? ': '.$returnType : ''; 65 | } 66 | } 67 | --------------------------------------------------------------------------------