20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
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 |
294 | Copyright (C)
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 | , 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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Joomla 3 Component Upgrade Rectors
2 |
3 | Rector rules to easily upgrade Joomla 3 components to Joomla 4 MVC
4 |
5 | Copyright (C) 2022 Nicholas K. Dionysopoulos
6 |
7 | This program is free software; you can redistribute it and/or modify
8 | it under the terms of the GNU General Public License as published by
9 | the Free Software Foundation; either version 2 of the License, or
10 | (at your option) any later version.
11 |
12 | This program is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU General Public License for more details.
16 |
17 | You should have received a copy of the GNU General Public License along
18 | with this program; if not, write to the Free Software Foundation, Inc.,
19 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 |
21 | ## What is this all about?
22 |
23 | This repository provides Rector rules to automatically refactor your legacy Joomla 3 component into Joomla 4+ MVC.
24 |
25 | It does not do everything. It will definitely _not_ result in a _fully working_ Joomla 4 component. The goal of this tool is to automate the boring, repeated and soul–crushing work. It sets you off to a great start into refactoring a legacy Joomla 3 component into a new Joomla 4+ MVC modern component. I wish I had that tool when I refactored by hand 20 extensions between March 2020 and October 2021.
26 |
27 | If you don't know much about the Joomla 4+ MVC and trying to divine how it works by reading its source code isn't your jam you may want to take a look at the [Joomla Extensions Development](https://github.com/nikosdion/joomla_extensions_development) book I'm writing. Like most of my work it's available free of charge, under an open source license, with full source code available, on a platform that fosters open collaboration.
28 |
29 | ## Sponsors welcome
30 |
31 | Do you have a Joomla extensions development business? Are you a web agency using tons of custom components? Maybe you can sponsor this work! It will save you tons of time — in the order of dozens of hours per component.
32 |
33 | Sponsorships will help me spend more time working on this tool, the Joomla extension developer's documentation and core Joomla code.
34 |
35 | If you're interested hit me up at [the Contact Me page](https://www.dionysopoulos.me/contact-me.html?view=item)! You'll get my gratitude and your logo on this page.
36 |
37 | ## Requirements
38 |
39 | * Rector 0.14
40 | * PHP 7.2 or later; 8.1 or later with XDebug _turned off_ recommended for best performance
41 | * Composer 2.x
42 |
43 | Your component project must have the structure described below.
44 |
45 | * Your component's backend code must be in a folder named `administrator`, `admin`, `backend` or `administrator/components/com_yourcomponent` (where `com_yourcomponent` is the name of your component).
46 |
47 | * Your component's frontend code must be in a folder named `site`, `frontend`, or `components/com_yourcomponent` (where `com_yourcomponent` is the name of your component).
48 |
49 | * Your component's media files must be in a folder named `media`, or `media/com_yourcomponent` (where `com_yourcomponent` is the name of your component).
50 |
51 | ## What can this tool do for me?
52 |
53 | **What it already does**
54 | * Namespace all of your MVC (Model, Controller, View and Table) classes and place them into the appropriate directories.
55 | * Refactor and namespace helper classes (e.h. ExampleHelper, ExampleHelperSomething, etc).
56 | * Refactor and namespace HTML helper classes (e.g. JHtmlExample) into HTML services.
57 | * Refactor and namespace custom form field classes (e.g. JFormFieldExample, JFormFieldModal_Example, etc).
58 | * Refactor and namespace custom form rule classes (e.g. JFormRuleExample).
59 | * Change static type hints in PHP code and docblocks.
60 |
61 | **What I would like to add**
62 | * ⚙️ Refactor static getInstance calls to the base model and table classes.
63 | * ⚙️ Refactor getModel and getView calls in controllers.
64 | * 📁 Update the XML manifest with the namespace prefix.
65 | * 📁 Rename language files so that they do NOT have a language prefix.
66 | * 📁 Update the XML manifest with the new language file prefixes.
67 | * 📁 Move view templates into the new folder structure.
68 | * 📁 Move backend and frontend XML forms to the appropriate folders.
69 | * 📁 Replace `addfieldpath` with `addfieldprefix` in XML forms.
70 | * ❓ Create a basic `services/provider.php` file. This is NOT a complete file, you still have to customise it!
71 |
72 | **What it CAN NOT and WILL NOT do**
73 | * Remove your old entry point file, possibly converting it to a custom Dispatcher. This is impossible. It requires understanding what your component does and make informed decisions on refactoring.
74 | * Refactor your frontend SEF URL Router. It's best to read my book to figure out how to proceed manually.
75 | * Create a custom component extension class to register Html, Category, Router, Tags etc. services. This requires knowing how your component works.
76 | * Refactor static getInstance calls to _descendants of_ the base model and table classes. It's not impossible, I just don't have the time to figure it out (yet?).
77 |
78 | In short, this tool tries to do the 30% of the migration work which would have taken you 70% of the time. Instead of spending _days, or weeks,_ or repetitive, boring, error–prone, soul–crushing grind you spend less than half an hour to read this README, set up Rector and another minute or so to automate all that mind–boggling drudgery. You can instead spend these few days to read my book, learn how Joomla 4+ MVC works and convert your component faster than you thought is possible!
79 |
80 | ## How to use
81 |
82 | Checkout your component's repository.
83 |
84 | Update your `composer.json` file with the following:
85 |
86 | ```json
87 | {
88 | "minimum-stability": "dev",
89 | "prefer-stable": true,
90 | "repositories": [
91 | {
92 | "name": "nikosdion/joomla_typehints",
93 | "type": "vcs",
94 | "url": "https://github.com/nikosdion/joomlatypehints"
95 | },
96 | {
97 | "name": "nikosdion/joomla_com_upgrader",
98 | "type": "vcs",
99 | "url": "https://github.com/nikosdion/joomla_com_upgrader"
100 | }
101 | ],
102 | "require-dev": {
103 | "rector/rector": "^0.14.0",
104 | "nikosdion/joomla_typehints": "*",
105 | "nikosdion/joomla_com_upgrader": "*",
106 | "friendsofphp/php-cs-fixer": "^3.0"
107 | }
108 | }
109 | ```
110 |
111 | Run `composer update --dev` to install the dependencies.
112 |
113 | Create a new `rector.php` in your component project's root with the following contents:
114 |
115 | ```php
116 | disableParallel();
134 |
135 | $rectorConfig->paths([
136 | __DIR__ . '/admin',
137 | __DIR__ . '/site',
138 | __DIR__ . '/script.php',
139 | // Add any more directories or files your project may be using here
140 | ]);
141 |
142 | $rectorConfig->skip([
143 | // These are our auto-generated renamed class maps for the second pass
144 | __DIR__ . '_classmap.php',
145 | __DIR__ . '_classmap.json',
146 | ]);
147 |
148 | // Required to autowire the custom services used by our Rector rules
149 | $services = $rectorConfig
150 | ->services()
151 | ->defaults()
152 | ->autowire()
153 | ->autoconfigure();
154 |
155 | // Register our custom services and configure them
156 | $services->set(RenamedClassHandlerService::class)
157 | ->arg('$directory', __DIR__);
158 |
159 | // Basic refactorings
160 | $rectorConfig->sets([
161 | // Auto-refactor code to at least PHP 7.2 (minimum Joomla version)
162 | LevelSetList::UP_TO_PHP_72,
163 | // Replace legacy class names with the namespaced ones
164 | __DIR__ . '/vendor/nikosdion/joomla_typehints/rector/joomla_4_0.php',
165 | // Use early returns in if-blocks (code quality)
166 | SetList::EARLY_RETURN,
167 | ]);
168 |
169 | // Configure the namespace mappings
170 | $joomlaNamespaceMaps = [
171 | new JoomlaLegacyPrefixToNamespace('Helloworld', 'Acme\HelloWorld', []),
172 | new JoomlaLegacyPrefixToNamespace('HelloWorld', 'Acme\HelloWorld', []),
173 | ];
174 |
175 | // Auto-refactor the Joomla MVC classes
176 | $rectorConfig->ruleWithConfiguration(JoomlaLegacyMVCToJ4Rector::class, $joomlaNamespaceMaps);
177 | $rectorConfig->ruleWithConfiguration(JoomlaHelpersToJ4Rector::class, $joomlaNamespaceMaps);
178 | $rectorConfig->ruleWithConfiguration(JoomlaHtmlHelpersRector::class, $joomlaNamespaceMaps);
179 | $rectorConfig->ruleWithConfiguration(JoomlaFormFieldsRector::class, $joomlaNamespaceMaps);
180 | $rectorConfig->ruleWithConfiguration(JoomlaFormRulesRector::class, $joomlaNamespaceMaps);
181 | // Dual purpose. 1st pass: collect renamed classes. 2nd pass: apply the renaming to type hints.
182 | $rectorConfig->rule(JoomlaPostRefactoringClassRenameRector::class);
183 |
184 | // Replace Fully Qualified Names (FQN) of classes with `use` imports at the top of the file.
185 | $rectorConfig->importNames();
186 | // Do NOT import short class names such as `DateTime`
187 | $rectorConfig->importShortClasses(false);
188 | };
189 | ```
190 |
191 | The lines you need to change are:
192 | ```php
193 | $joomlaNamespaceMaps = [
194 | new JoomlaLegacyPrefixToNamespace('Helloworld', 'Acme\HelloWorld', []),
195 | new JoomlaLegacyPrefixToNamespace('HelloWorld', 'Acme\HelloWorld', []),
196 | ];
197 | ```
198 | where `HelloWorld` is the name of your component without the `com_` prefix and `Acme\HelloWorld` is the namespace prefix you want to use for your component. It is recommended to use the convention `CompanyName\ComponentNameWithoutCom` or `CompanyName\Component\ComponentNameWithoutCom` for your namespace prefix.
199 |
200 | **CAUTION!** Note that I added two lines here with the legacy Joomla 3 namespace being `Helloworld` in one and `HelloWorld` in another. That's because in Joomla 3 the case of the prefix of your component does not matter. `Helloworld`, `HelloWorld` and `HELLOWORLD` would work just fine. The code refactoring rules are, however, case–sensitive. As a result you need to add as many lines as you have different cases in your component.
201 |
202 | The third argument, the empty array `[]`, is a list of class names which begin with the old prefix that you do not want to namespace. I can't think of a reason why you want to do that but I can neither claim I can think of any use case. So I added that option _just in case_ you need it.
203 |
204 | Now you can run Rector to do _a hell of a lot_ of the refactoring necessary to convert your component to Joomla 4 MVC.
205 |
206 | First, we tell it to collect the classes which will be renamed but without doing any changes to the files. **THIS STEP IS MANDATORY**.
207 |
208 | ```bash
209 | php ./vendor/bin/rector --dry-run --clear-cache
210 | ```
211 |
212 | Note: The `--dry-run` parameter prints out the changes. Now is a good time to make sure they are not wrong.
213 |
214 | Then we can run it for real (**this step modifies the files in your project**):
215 |
216 | ```bash
217 | php ./vendor/bin/rector --clear-cache
218 | ```
219 |
220 | ## How this tool came to be
221 |
222 | There's been a discussion on Joomla's GitHub repository about how “hard” it is to convert a Joomla 3 component to the new MVC shipped with Joomla 4. Having had the experience of converting 20 extensions myself — and several more dozens of plugins and modules which came with three quarters of them — I realised it's not “hard” but two crucial things were missing: documentation and a tool to get you started.
223 |
224 | The lack of documentation is something I lamented when I started trying to figure out how to support Joomla 4 in my own extensions. I decided to address it with my [Joomla Extensions Development](https://github.com/nikosdion/joomla_extensions_development) book.
225 |
226 | How to get started is a pained story. Most of my own code was already namespaced (as I was using FOF for my components which since version 3, released in 2015, required namespacing the code), therefore my experience was mostly changing namespaces and converting the internals from FOF MVC to core Joomla 4 MVC. I had two components written in plain old Joomla 3 MVC and _that_ experience sucked! I totally get the people who say it's hard. It's so boring and you need to do so much work before you see any results that it feel intimidating and unapproachable.
227 |
228 | At this point I've been using Rector for years to massage my code whenever I am changing something — albeit it's mostly been renaming classes. I looked at how to write custom Rector rules and I realised I actually understood what's going on! Apparently a summer spent 24 years ago writing my own compiler following a tutorial gave me a good background to write Rector rules today. Huh!
229 |
230 | So, here we are. Custom Rector rules to start converting legacy Joomla 3 MVC components to Joomla 4, free of charge, because **community matters**. ☮️
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nikosdion/joomla_com_upgrader",
3 | "description": "Rector rules to upgrade Joomla 3 components",
4 | "prefer-stable": true,
5 | "minimum-stability": "dev",
6 | "license": "GPL-2.0-or-later",
7 | "version": "1.0.0-dev",
8 | "config": {
9 | "optimize-autoloader": true
10 | },
11 | "repositories": [
12 | {
13 | "name": "nikosdion/joomla_typehints",
14 | "type": "vcs",
15 | "url": "https://github.com/nikosdion/joomlatypehints"
16 | }
17 | ],
18 | "require": {
19 | "php": "^7.2 | ^8.0",
20 | "ext-json": "*",
21 | "webmozart/assert": "^1.11.0"
22 | },
23 | "require-dev": {
24 | "rector/rector": "^0.14.0",
25 | "nikosdion/joomla_typehints": "*",
26 | "phpunit/phpunit": "^9.5.23"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "Rector\\": "rules",
31 | "Rector\\Tests\\": "rules-tests"
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/override/Symplify/EasyTesting/StaticFixtureSplitter.php:
--------------------------------------------------------------------------------
1 | dump() for performance reasons */
32 | \file_put_contents($temporaryFilePath, $fileContent);
33 |
34 | return new SmartFileInfo($temporaryFilePath);
35 | }
36 |
37 | public static function getTemporaryPath(): string
38 | {
39 | if (self::$customTemporaryPath !== null)
40 | {
41 | return self::$customTemporaryPath;
42 | }
43 |
44 | return \sys_get_temp_dir() . '/_temp_fixture_easy_testing';
45 | }
46 |
47 | public static function splitFileInfoToInputAndExpected(SmartFileInfo $smartFileInfo): InputAndExpected
48 | {
49 | $splitLineCount = \count(Strings::matchAll($smartFileInfo->getContents(), SplitLine::SPLIT_LINE_REGEX));
50 | // if more or less, it could be a test cases for monorepo line in it
51 | if ($splitLineCount === 1)
52 | {
53 | // input → expected
54 | [$input, $expected] = Strings::split($smartFileInfo->getContents(), SplitLine::SPLIT_LINE_REGEX);
55 | $expected = self::retypeExpected($expected);
56 |
57 | return new InputAndExpected($input, $expected);
58 | }
59 |
60 | // no changes
61 | return new InputAndExpected($smartFileInfo->getContents(), $smartFileInfo->getContents());
62 | }
63 |
64 | public static function splitFileInfoToLocalInputAndExpected(SmartFileInfo $smartFileInfo, bool $autoloadTestFixture = \false): InputFileInfoAndExpected
65 | {
66 | $inputAndExpected = self::splitFileInfoToInputAndExpected($smartFileInfo);
67 | $inputFileInfo = self::createTemporaryFileInfo($smartFileInfo, 'input', $inputAndExpected->getInput());
68 | // some files needs to be autoload to enable reflection
69 | if ($autoloadTestFixture)
70 | {
71 | require_once $inputFileInfo->getRealPath();
72 | }
73 |
74 | return new InputFileInfoAndExpected($inputFileInfo, $inputAndExpected->getExpected());
75 | }
76 |
77 | public static function splitFileInfoToLocalInputAndExpectedFileInfos(SmartFileInfo $smartFileInfo, bool $autoloadTestFixture = \false, bool $preserveDirStructure = \true): InputFileInfoAndExpectedFileInfo
78 | {
79 | $inputAndExpected = self::splitFileInfoToInputAndExpected($smartFileInfo);
80 | $prefix = '';
81 |
82 | if ($preserveDirStructure)
83 | {
84 | $dir = \explode('Fixture', $smartFileInfo->getRealPath(), 2);
85 | $prefix = isset($dir[1]) ? \dirname($dir[1]) . '/' : '';
86 | $prefix = \ltrim($prefix, '/\\');
87 |
88 | $inputFileInfo = self::createTemporaryFileInfo($smartFileInfo, $prefix, $inputAndExpected->getInput(), true);
89 | }
90 | else
91 | {
92 | $inputFileInfo = self::createTemporaryFileInfo($smartFileInfo, $prefix . 'input', $inputAndExpected->getInput());
93 | }
94 |
95 |
96 | // some files needs to be autoload to enable reflection
97 | if ($autoloadTestFixture)
98 | {
99 | require_once $inputFileInfo->getRealPath();
100 | }
101 |
102 | $expectedFileInfo = self::createTemporaryFileInfo($smartFileInfo, $prefix . 'expected', $inputAndExpected->getExpected());
103 |
104 | return new InputFileInfoAndExpectedFileInfo($inputFileInfo, $expectedFileInfo);
105 | }
106 |
107 | private static function createTemporaryPathWithPrefix(SmartFileInfo $smartFileInfo, string $prefix, bool $withHash = true): string
108 | {
109 | $hash = Strings::substring(\md5($smartFileInfo->getRealPath()), -20);
110 | $fileBasename = $smartFileInfo->getBasename('.inc');
111 |
112 | if ($withHash)
113 | {
114 | return self::getTemporaryPath() . \sprintf('/%s_%s_%s', $prefix, $hash, $fileBasename);
115 | }
116 |
117 | return self::getTemporaryPath() . \sprintf('/%s%s', $prefix, $fileBasename);
118 | }
119 |
120 | /**
121 | * @param mixed $expected
122 | *
123 | * @return mixed
124 | */
125 | private static function retypeExpected($expected)
126 | {
127 | if (!\is_numeric(\trim($expected)))
128 | {
129 | return $expected;
130 | }
131 | // value re-type
132 | if (\strlen((string) (int) $expected) === \strlen(\trim($expected)))
133 | {
134 | return (int) $expected;
135 | }
136 | if (\strlen((string) (float) $expected) === \strlen(\trim($expected)))
137 | {
138 | return (float) $expected;
139 | }
140 |
141 | return $expected;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/rector-recipe.php:
--------------------------------------------------------------------------------
1 | services();
15 | // [REQUIRED]
16 | $rectorRecipeConfiguration = [
17 | // [RECTOR CORE CONTRIBUTION - REQUIRED]
18 | // package name, basically namespace part in `rules//src`, use PascalCase
19 | Option::PACKAGE => 'Naming',
20 | // name, basically short class name; use PascalCase
21 | Option::NAME => 'JoomlaLegacyToNamespacedRector',
22 | // 1+ node types to change, pick from classes here https://github.com/nikic/PHP-Parser/tree/master/lib/PhpParser/Node
23 | // the best practise is to have just 1 type here if possible, and make separated rule for other node types
24 | Option::NODE_TYPES => [FileWithoutNamespace::class, Namespace_::class],
25 | // describe what the rule does
26 | Option::DESCRIPTION => 'Convert legacy Joomla 3 MVC class names into Joomla 4 namespaced ones.',
27 | // code before change
28 | // this is used for documentation and first test fixture
29 | Option::CODE_BEFORE => <<<'CODE_SAMPLE'
30 | /** @var FooModelBar $someModel */
31 | $model = new FooModelBar;
32 | CODE_SAMPLE
33 | ,
34 | // code after change
35 | Option::CODE_AFTER => <<<'CODE_SAMPLE'
36 | /** @var \Acme\Foo\BarModel $someModel */
37 | $model = new BarModel;
38 | CODE_SAMPLE
39 | ,
40 | ];
41 | $services->set(RectorRecipeProvider::class)->arg('$rectorRecipeConfiguration', $rectorRecipeConfiguration);
42 | };
43 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaFormFieldsRector/Fixture/admin/models/fields/example.php.inc:
--------------------------------------------------------------------------------
1 |
7 | -----
8 |
7 | -----
8 |
7 | -----
8 |
7 | -----
8 | 'admin/src/Field/ExampleField.php',
27 | 'admin/models/fields/modal/example.php' => 'admin/src/Field/Modal/ExampleField.php',
28 | 'site/models/fields/example.php' => 'site/src/Field/ExampleField.php',
29 | 'site/models/fields/modal/example.php' => 'site/src/Field/Modal/ExampleField.php',
30 | ];
31 |
32 | public function provideConfigFilePath(): string
33 | {
34 | /**
35 | * Tells Rector to use a CONFIGURED instance of our rule for testing purposes.
36 | *
37 | * Don't touch it! If this needs to change udate the config/configured_rule.php, not this method.
38 | */
39 | return __DIR__ . '/config/configured_rule.php';
40 | }
41 |
42 | /**
43 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
44 | */
45 | public function provideData(): \Iterator
46 | {
47 | // Tells Rector to create test cases from the Fixture files. Don't touch it!
48 | return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
49 | }
50 |
51 | /**
52 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
53 | */
54 | public function provideDataMini(): array
55 | {
56 | return [
57 | [new SmartFileInfo(__DIR__ . '/Fixture/admin/models/fields/modal/example.php.inc')],
58 | ];
59 | }
60 |
61 | /**
62 | * @dataProvider provideDataMini()
63 | */
64 | public function testOneFileForDebug(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
65 | {
66 | $this->testRefactorNamespace($fileInfo);
67 | }
68 |
69 | /**
70 | * @dataProvider provideData()
71 | */
72 | public function testRefactorNamespace(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
73 | {
74 | $inputFileInfoAndExpectedFileInfo = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpectedFileInfos($fileInfo);
75 | $expectedFileInfo = $inputFileInfoAndExpectedFileInfo->getExpectedFileInfo();
76 |
77 | // This runs each test. Don't touch it!
78 | $this->doTestFileInfo($fileInfo);
79 |
80 | // Returns something like /var/folders/gd/9tlfz2cj0_94qc23mv39rplw0000gn/T/_temp_fixture_easy_testing/site/controller.php
81 | $relative = $this->originalTempFileInfo->getRelativeFilePathFromDirectory($this->getFixtureTempDirectory());
82 | $newRelative = self::RENAME_MAP[$relative] ?? null;
83 |
84 | if (empty($newRelative))
85 | {
86 | $this->markTestIncomplete(
87 | sprintf(
88 | 'You have not set up the expected target path for ‘%s’ in RENAME_MAP',
89 | $relative
90 | )
91 | );
92 | }
93 |
94 | $newAbsolute = realpath($this->getFixtureTempDirectory()) . '/' . $newRelative;
95 |
96 | $this->assertFilesWereAdded([
97 | new AddedFileWithContent(
98 | $newAbsolute,
99 | $expectedFileInfo->getContents()
100 | ),
101 | ]);
102 | }
103 |
104 | /**
105 | * Set up before running the tests.
106 | *
107 | * We override the RectorPrefix202208\Symplify\EasyTesting\StaticFixtureSplitter class with our own. Rector's
108 | * default test infrastructure creates files from our fixtures which have a random name. However, our rule being
109 | * tested (our SUT -- System Under Test) relies on the filename to refactor the classes; this is an unfortunate
110 | * requirement due to the not very reasonable way Joomla 3's MVC worked, especially for View classes. Since the
111 | * Rector class is final and the methods called for testing are private we cannot extend that class and use our own
112 | * custom object. No problem! We fork it and include it before its first use.
113 | *
114 | * TODO Move this to a PHPUnit bootstrap file.
115 | *
116 | * @return void
117 | * @since 1.0.0
118 | */
119 | protected function setUp(): void
120 | {
121 | require_once __DIR__ . '/../../../../../override/Symplify/EasyTesting/StaticFixtureSplitter.php';
122 |
123 | parent::setUp();
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaFormFieldsRector/config/configured_rule.php:
--------------------------------------------------------------------------------
1 | services()
22 | ->defaults()
23 | ->autowire()
24 | ->autoconfigure();
25 |
26 | $services->set(RenamedClassHandlerService::class)
27 | ->arg('$directory', realpath(__DIR__ . '/../../../../../../'));
28 |
29 | $rectorConfig->ruleWithConfiguration(
30 | JoomlaFormFieldsRector::class,
31 | [
32 | new JoomlaLegacyPrefixToNamespace('Example', '\\Acme\\Example', []),
33 | ]
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaFormRulesRector/Fixture/admin/models/rules/example.php.inc:
--------------------------------------------------------------------------------
1 |
6 | -----
7 | 'admin/src/Rule/ExampleRule.php',
27 | ];
28 |
29 | public function provideConfigFilePath(): string
30 | {
31 | /**
32 | * Tells Rector to use a CONFIGURED instance of our rule for testing purposes.
33 | *
34 | * Don't touch it! If this needs to change udate the config/configured_rule.php, not this method.
35 | */
36 | return __DIR__ . '/config/configured_rule.php';
37 | }
38 |
39 | /**
40 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
41 | */
42 | public function provideData(): \Iterator
43 | {
44 | // Tells Rector to create test cases from the Fixture files. Don't touch it!
45 | return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
46 | }
47 |
48 | /**
49 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
50 | */
51 | public function provideDataMini(): array
52 | {
53 | return [
54 | [new SmartFileInfo(__DIR__ . '/Fixture/admin/models/rules/example.php.inc')],
55 | ];
56 | }
57 |
58 | /**
59 | * @dataProvider provideDataMini()
60 | */
61 | public function testOneFileForDebug(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
62 | {
63 | $this->testRefactorNamespace($fileInfo);
64 | }
65 |
66 | /**
67 | * @dataProvider provideData()
68 | */
69 | public function testRefactorNamespace(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
70 | {
71 | $inputFileInfoAndExpectedFileInfo = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpectedFileInfos($fileInfo);
72 | $expectedFileInfo = $inputFileInfoAndExpectedFileInfo->getExpectedFileInfo();
73 |
74 | // This runs each test. Don't touch it!
75 | $this->doTestFileInfo($fileInfo);
76 |
77 | // Returns something like /var/folders/gd/9tlfz2cj0_94qc23mv39rplw0000gn/T/_temp_fixture_easy_testing/site/controller.php
78 | $relative = $this->originalTempFileInfo->getRelativeFilePathFromDirectory($this->getFixtureTempDirectory());
79 | $newRelative = self::RENAME_MAP[$relative] ?? null;
80 |
81 | if (empty($newRelative))
82 | {
83 | $this->markTestIncomplete(
84 | sprintf(
85 | 'You have not set up the expected target path for ‘%s’ in RENAME_MAP',
86 | $relative
87 | )
88 | );
89 | }
90 |
91 | $newAbsolute = realpath($this->getFixtureTempDirectory()) . '/' . $newRelative;
92 |
93 | $this->assertFilesWereAdded([
94 | new AddedFileWithContent(
95 | $newAbsolute,
96 | $expectedFileInfo->getContents()
97 | ),
98 | ]);
99 | }
100 |
101 | /**
102 | * Set up before running the tests.
103 | *
104 | * We override the RectorPrefix202208\Symplify\EasyTesting\StaticFixtureSplitter class with our own. Rector's
105 | * default test infrastructure creates files from our fixtures which have a random name. However, our rule being
106 | * tested (our SUT -- System Under Test) relies on the filename to refactor the classes; this is an unfortunate
107 | * requirement due to the not very reasonable way Joomla 3's MVC worked, especially for View classes. Since the
108 | * Rector class is final and the methods called for testing are private we cannot extend that class and use our own
109 | * custom object. No problem! We fork it and include it before its first use.
110 | *
111 | * TODO Move this to a PHPUnit bootstrap file.
112 | *
113 | * @return void
114 | * @since 1.0.0
115 | */
116 | protected function setUp(): void
117 | {
118 | require_once __DIR__ . '/../../../../../override/Symplify/EasyTesting/StaticFixtureSplitter.php';
119 |
120 | parent::setUp();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaFormRulesRector/config/configured_rule.php:
--------------------------------------------------------------------------------
1 | services()
23 | ->defaults()
24 | ->autowire()
25 | ->autoconfigure();
26 |
27 | $services->set(RenamedClassHandlerService::class)
28 | ->arg('$directory', realpath(__DIR__ . '/../../../../../../'));
29 |
30 | $rectorConfig->ruleWithConfiguration(
31 | JoomlaFormRulesRector::class,
32 | [
33 | new JoomlaLegacyPrefixToNamespace('Example', '\\Acme\\Example', []),
34 | ]
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaHelpersToJ4Rector/Fixture/admin/helpers/associations.php.inc:
--------------------------------------------------------------------------------
1 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 | 'admin/src/Helper/ExampleHelper.php',
27 | 'admin/helpers/foobar.php' => 'admin/src/Helper/FoobarHelper.php',
28 | 'admin/helpers/associations.php' => 'admin/src/Helper/AssociationsHelper.php',
29 |
30 | 'site/helpers/example.php' => 'site/src/Helper/ExampleHelper.php',
31 | ];
32 |
33 | public function provideConfigFilePath(): string
34 | {
35 | /**
36 | * Tells Rector to use a CONFIGURED instance of our rule for testing purposes.
37 | *
38 | * Don't touch it! If this needs to change udate the config/configured_rule.php, not this method.
39 | */
40 | return __DIR__ . '/config/configured_rule.php';
41 | }
42 |
43 | /**
44 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
45 | */
46 | public function provideData(): \Iterator
47 | {
48 | // Tells Rector to create test cases from the Fixture files. Don't touch it!
49 | return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
50 | }
51 |
52 | /**
53 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
54 | */
55 | public function provideDataMini(): array
56 | {
57 | return [
58 | [new SmartFileInfo(__DIR__ . '/Fixture/admin/helpers/foobar.php.inc')],
59 | ];
60 | }
61 |
62 | /**
63 | * @dataProvider provideDataMini()
64 | */
65 | public function testOneFileForDebug(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
66 | {
67 | $this->testRefactorNamespace($fileInfo);
68 | }
69 |
70 | /**
71 | * @dataProvider provideData()
72 | */
73 | public function testRefactorNamespace(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
74 | {
75 | $inputFileInfoAndExpectedFileInfo = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpectedFileInfos($fileInfo);
76 | $expectedFileInfo = $inputFileInfoAndExpectedFileInfo->getExpectedFileInfo();
77 |
78 | // This runs each test. Don't touch it!
79 | $this->doTestFileInfo($fileInfo);
80 |
81 | // Returns something like /var/folders/gd/9tlfz2cj0_94qc23mv39rplw0000gn/T/_temp_fixture_easy_testing/site/controller.php
82 | $relative = $this->originalTempFileInfo->getRelativeFilePathFromDirectory($this->getFixtureTempDirectory());
83 | $newRelative = self::RENAME_MAP[$relative] ?? null;
84 |
85 | if (empty($newRelative))
86 | {
87 | $this->markTestIncomplete(
88 | sprintf(
89 | 'You have not set up the expected target path for ‘%s’ in RENAME_MAP',
90 | $relative
91 | )
92 | );
93 | }
94 |
95 | $newAbsolute = realpath($this->getFixtureTempDirectory()) . '/' . $newRelative;
96 |
97 | $this->assertFilesWereAdded([
98 | new AddedFileWithContent(
99 | $newAbsolute,
100 | $expectedFileInfo->getContents()
101 | ),
102 | ]);
103 | }
104 |
105 | /**
106 | * Set up before running the tests.
107 | *
108 | * We override the RectorPrefix202208\Symplify\EasyTesting\StaticFixtureSplitter class with our own. Rector's
109 | * default test infrastructure creates files from our fixtures which have a random name. However, our rule being
110 | * tested (our SUT -- System Under Test) relies on the filename to refactor the classes; this is an unfortunate
111 | * requirement due to the not very reasonable way Joomla 3's MVC worked, especially for View classes. Since the
112 | * Rector class is final and the methods called for testing are private we cannot extend that class and use our own
113 | * custom object. No problem! We fork it and include it before its first use.
114 | *
115 | * TODO Move this to a PHPUnit bootstrap file.
116 | *
117 | * @return void
118 | * @since 1.0.0
119 | */
120 | protected function setUp(): void
121 | {
122 | require_once __DIR__ . '/../../../../../override/Symplify/EasyTesting/StaticFixtureSplitter.php';
123 |
124 | parent::setUp();
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaHelpersToJ4Rector/config/configured_rule.php:
--------------------------------------------------------------------------------
1 | services()
21 | ->defaults()
22 | ->autowire()
23 | ->autoconfigure();
24 |
25 | $services->set(RenamedClassHandlerService::class)
26 | ->arg('$directory', realpath(__DIR__ . '/../../../../../../'));
27 |
28 | $rectorConfig->ruleWithConfiguration(
29 | JoomlaHelpersToJ4Rector::class,
30 | [
31 | new JoomlaLegacyPrefixToNamespace('Example', '\\Acme\\Example', []),
32 | ]
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaHtmlHelpersRector/Fixture/admin/helpers/html/example.php.inc:
--------------------------------------------------------------------------------
1 |
15 | -----
16 | 'admin/src/Service/Html/Example.php',
27 | ];
28 |
29 | public function provideConfigFilePath(): string
30 | {
31 | /**
32 | * Tells Rector to use a CONFIGURED instance of our rule for testing purposes.
33 | *
34 | * Don't touch it! If this needs to change udate the config/configured_rule.php, not this method.
35 | */
36 | return __DIR__ . '/config/configured_rule.php';
37 | }
38 |
39 | /**
40 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
41 | */
42 | public function provideData(): \Iterator
43 | {
44 | // Tells Rector to create test cases from the Fixture files. Don't touch it!
45 | return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
46 | }
47 |
48 | /**
49 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
50 | */
51 | public function provideDataMini(): array
52 | {
53 | return [
54 | [new SmartFileInfo(__DIR__ . '/Fixture/admin/helpers/html/example.php.inc')],
55 | ];
56 | }
57 |
58 | /**
59 | * @dataProvider provideDataMini()
60 | */
61 | public function testOneFileForDebug(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
62 | {
63 | $this->testRefactorNamespace($fileInfo);
64 | }
65 |
66 | /**
67 | * @dataProvider provideData()
68 | */
69 | public function testRefactorNamespace(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
70 | {
71 | $inputFileInfoAndExpectedFileInfo = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpectedFileInfos($fileInfo);
72 | $expectedFileInfo = $inputFileInfoAndExpectedFileInfo->getExpectedFileInfo();
73 |
74 | // This runs each test. Don't touch it!
75 | $this->doTestFileInfo($fileInfo);
76 |
77 | // Returns something like /var/folders/gd/9tlfz2cj0_94qc23mv39rplw0000gn/T/_temp_fixture_easy_testing/site/controller.php
78 | $relative = $this->originalTempFileInfo->getRelativeFilePathFromDirectory($this->getFixtureTempDirectory());
79 | $newRelative = self::RENAME_MAP[$relative] ?? null;
80 |
81 | if (empty($newRelative))
82 | {
83 | $this->markTestIncomplete(
84 | sprintf(
85 | 'You have not set up the expected target path for ‘%s’ in RENAME_MAP',
86 | $relative
87 | )
88 | );
89 | }
90 |
91 | $newAbsolute = realpath($this->getFixtureTempDirectory()) . '/' . $newRelative;
92 |
93 | $this->assertFilesWereAdded([
94 | new AddedFileWithContent(
95 | $newAbsolute,
96 | $expectedFileInfo->getContents()
97 | ),
98 | ]);
99 | }
100 |
101 | /**
102 | * Set up before running the tests.
103 | *
104 | * We override the RectorPrefix202208\Symplify\EasyTesting\StaticFixtureSplitter class with our own. Rector's
105 | * default test infrastructure creates files from our fixtures which have a random name. However, our rule being
106 | * tested (our SUT -- System Under Test) relies on the filename to refactor the classes; this is an unfortunate
107 | * requirement due to the not very reasonable way Joomla 3's MVC worked, especially for View classes. Since the
108 | * Rector class is final and the methods called for testing are private we cannot extend that class and use our own
109 | * custom object. No problem! We fork it and include it before its first use.
110 | *
111 | * TODO Move this to a PHPUnit bootstrap file.
112 | *
113 | * @return void
114 | * @since 1.0.0
115 | */
116 | protected function setUp(): void
117 | {
118 | require_once __DIR__ . '/../../../../../override/Symplify/EasyTesting/StaticFixtureSplitter.php';
119 |
120 | parent::setUp();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaHtmlHelpersRector/config/configured_rule.php:
--------------------------------------------------------------------------------
1 | services()
21 | ->defaults()
22 | ->autowire()
23 | ->autoconfigure();
24 |
25 | $services->set(RenamedClassHandlerService::class)
26 | ->arg('$directory', realpath(__DIR__ . '/../../../../../../'));
27 |
28 | $rectorConfig->ruleWithConfiguration(
29 | JoomlaHtmlHelpersRector::class,
30 | [
31 | new JoomlaLegacyPrefixToNamespace('Example', '\\Acme\\Example', []),
32 | ]
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaLegacyMVCToJ4Rector/Fixture/admin/controller.php.inc:
--------------------------------------------------------------------------------
1 |
7 | -----
8 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
7 | -----
8 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 |
8 | -----
9 | 'admin/src/Controller/DisplayController.php',
27 | 'admin/controllers/example.php' => 'admin/src/Controller/ExampleController.php',
28 | 'admin/controllers/foobar.php' => 'admin/src/Controller/FoobarController.php',
29 | 'admin/models/example.php' => 'admin/src/Model/ExampleModel.php',
30 | 'admin/models/foobar.php' => 'admin/src/Model/FoobarModel.php',
31 | 'admin/tables/example.php' => 'admin/src/Table/ExampleTable.php',
32 | 'admin/tables/foobar.php' => 'admin/src/Table/FoobarTable.php',
33 | 'admin/views/example/view.html.php' => 'admin/src/View/Example/HtmlView.php',
34 | 'admin/views/example/view.json.php' => 'admin/src/View/Example/JsonView.php',
35 | 'admin/views/foobar/view.html.php' => 'admin/src/View/Foobar/HtmlView.php',
36 |
37 | 'site/controller.php' => 'site/src/Controller/DisplayController.php',
38 | 'site/controllers/example.php' => 'site/src/Controller/ExampleController.php',
39 | 'site/controllers/foobar.php' => 'site/src/Controller/FoobarController.php',
40 | 'site/models/example.php' => 'site/src/Model/ExampleModel.php',
41 | 'site/models/foobar.php' => 'site/src/Model/FoobarModel.php',
42 | 'site/views/example/view.html.php' => 'site/src/View/Example/HtmlView.php',
43 | 'site/views/example/view.json.php' => 'site/src/View/Example/JsonView.php',
44 | 'site/views/foobar/view.html.php' => 'site/src/View/Foobar/HtmlView.php',
45 | ];
46 |
47 | public function provideConfigFilePath(): string
48 | {
49 | /**
50 | * Tells Rector to use a CONFIGURED instance of our rule for testing purposes.
51 | *
52 | * Don't touch it! If this needs to change udate the config/configured_rule.php, not this method.
53 | */
54 | return __DIR__ . '/config/configured_rule.php';
55 | }
56 |
57 | /**
58 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
59 | */
60 | public function provideData(): \Iterator
61 | {
62 | // Tells Rector to create test cases from the Fixture files. Don't touch it!
63 | return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
64 | }
65 |
66 | /**
67 | * @return \Iterator<\Symplify\SmartFileSystem\SmartFileInfo>
68 | */
69 | public function provideDataMini(): array
70 | {
71 | return [
72 | [new SmartFileInfo(__DIR__ . '/Fixture/admin/tables/example.php.inc')]
73 | ];
74 | }
75 |
76 | /**
77 | * @dataProvider provideDataMini()
78 | */
79 | public function testOneFileForDebug(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
80 | {
81 | $this->testRefactorNamespace($fileInfo);
82 | }
83 |
84 | /**
85 | * @dataProvider provideData()
86 | */
87 | public function testRefactorNamespace(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
88 | {
89 | $inputFileInfoAndExpectedFileInfo = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpectedFileInfos($fileInfo);
90 | $expectedFileInfo = $inputFileInfoAndExpectedFileInfo->getExpectedFileInfo();
91 |
92 | // This runs each test. Don't touch it!
93 | $this->doTestFileInfo($fileInfo);
94 |
95 | // Returns something like /var/folders/gd/9tlfz2cj0_94qc23mv39rplw0000gn/T/_temp_fixture_easy_testing/site/controller.php
96 | $relative = $this->originalTempFileInfo->getRelativeFilePathFromDirectory($this->getFixtureTempDirectory());
97 | $newRelative = self::RENAME_MAP[$relative] ?? null;
98 |
99 | if (empty($newRelative))
100 | {
101 | $this->markTestIncomplete(
102 | sprintf(
103 | 'You have not set up the expected target path for ‘%s’ in RENAME_MAP',
104 | $relative
105 | )
106 | );
107 | }
108 |
109 | $newAbsolute = realpath($this->getFixtureTempDirectory()) . '/' . $newRelative;
110 |
111 | $this->assertFilesWereAdded([
112 | new AddedFileWithContent(
113 | $newAbsolute,
114 | $expectedFileInfo->getContents()
115 | ),
116 | ]);
117 | }
118 |
119 | /**
120 | * Set up before running the tests.
121 | *
122 | * We override the RectorPrefix202208\Symplify\EasyTesting\StaticFixtureSplitter class with our own. Rector's
123 | * default test infrastructure creates files from our fixtures which have a random name. However, our rule being
124 | * tested (our SUT -- System Under Test) relies on the filename to refactor the classes; this is an unfortunate
125 | * requirement due to the not very reasonable way Joomla 3's MVC worked, especially for View classes. Since the
126 | * Rector class is final and the methods called for testing are private we cannot extend that class and use our own
127 | * custom object. No problem! We fork it and include it before its first use.
128 | *
129 | * TODO Move this to a PHPUnit bootstrap file.
130 | *
131 | * @return void
132 | * @since 1.0.0
133 | */
134 | protected function setUp(): void
135 | {
136 | require_once __DIR__ . '/../../../../../override/Symplify/EasyTesting/StaticFixtureSplitter.php';
137 |
138 | parent::setUp();
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaLegacyMVCToJ4Rector/config/configured_rule.php:
--------------------------------------------------------------------------------
1 | services()
21 | ->defaults()
22 | ->autowire()
23 | ->autoconfigure();
24 |
25 | $services->set(RenamedClassHandlerService::class)
26 | ->arg('$directory', realpath(__DIR__ . '/../../../../../../'));
27 |
28 | $rectorConfig->ruleWithConfiguration(
29 | JoomlaLegacyMVCToJ4Rector::class,
30 | [
31 | new JoomlaLegacyPrefixToNamespace('Example', '\\Acme\\Example', [
32 | 'Example',
33 | 'ExampleFoobar',
34 | ]),
35 | ]
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaPostRefactoringClassRenameRector/Fixture/admin/src/Controller/Example.php.inc:
--------------------------------------------------------------------------------
1 | getModel('Foobar');
15 | }
16 | }
17 |
18 | ?>
19 | -----
20 | getModel('Foobar');
34 | }
35 | }
36 |
37 | ?>
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaPostRefactoringClassRenameRector/Fixture/site/src/Controller/Example.php.inc:
--------------------------------------------------------------------------------
1 | getModel('Foobar');
15 | }
16 | }
17 |
18 | ?>
19 | -----
20 | getModel('Foobar');
34 | }
35 | }
36 |
37 | ?>
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaPostRefactoringClassRenameRector/JoomlaPostRefactoringClassRenameRectorTest.php:
--------------------------------------------------------------------------------
1 |
36 | */
37 | public function provideData(): \Iterator
38 | {
39 | // Tells Rector to create test cases from the Fixture files. Don't touch it!
40 | return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
41 | }
42 |
43 | /**
44 | * @dataProvider provideData()
45 | */
46 | public function testRefactorNamespace(\Symplify\SmartFileSystem\SmartFileInfo $fileInfo): void
47 | {
48 | $inputFileInfoAndExpectedFileInfo = StaticFixtureSplitter::splitFileInfoToLocalInputAndExpectedFileInfos($fileInfo);
49 | $expectedFileInfo = $inputFileInfoAndExpectedFileInfo->getExpectedFileInfo();
50 |
51 | // This runs each test. Don't touch it!
52 | $this->doTestFileInfo($fileInfo);
53 | }
54 |
55 | /**
56 | * Set up before running the tests.
57 | *
58 | * We override the RectorPrefix202208\Symplify\EasyTesting\StaticFixtureSplitter class with our own. Rector's
59 | * default test infrastructure creates files from our fixtures which have a random name. However, our rule being
60 | * tested (our SUT -- System Under Test) relies on the filename to refactor the classes; this is an unfortunate
61 | * requirement due to the not very reasonable way Joomla 3's MVC worked, especially for View classes. Since the
62 | * Rector class is final and the methods called for testing are private we cannot extend that class and use our own
63 | * custom object. No problem! We fork it and include it before its first use.
64 | *
65 | * TODO Move this to a PHPUnit bootstrap file.
66 | *
67 | * @return void
68 | * @since 1.0.0
69 | */
70 | protected function setUp(): void
71 | {
72 | require_once __DIR__ . '/../../../../../override/Symplify/EasyTesting/StaticFixtureSplitter.php';
73 |
74 | parent::setUp();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaPostRefactoringClassRenameRector/_classmap.json:
--------------------------------------------------------------------------------
1 | {"site":{"ExampleController":"Acme\\Example\\Site\\Controller\\DisplayController","ExampleModelExample":"Acme\\Example\\Site\\Model\\ExampleModel","ExampleModelFoobar":"Acme\\Example\\Site\\Model\\FoobarModel","ExampleControllerExample":"Acme\\Example\\Site\\Controller\\ExampleController","ExampleControllerFoobar":"Acme\\Example\\Site\\Controller\\FoobarController","ExampleViewExample":"Acme\\Example\\Site\\View\\Example\\JsonView","ExampleViewFoobar":"Acme\\Example\\Site\\View\\Foobar\\HtmlView"},"admin":{"ExampleTableExample":"Acme\\Example\\Administrator\\Table\\ExampleTable","ExampleTableFoobar":"Acme\\Example\\Administrator\\Table\\FoobarTable","ExampleController":"Acme\\Example\\Administrator\\Controller\\DisplayController","ExampleModelExample":"Acme\\Example\\Administrator\\Model\\ExampleModel","ExampleModelFoobar":"Acme\\Example\\Administrator\\Model\\FoobarModel","ExampleControllerExample":"Acme\\Example\\Administrator\\Controller\\ExampleController","ExampleControllerFoobar":"Acme\\Example\\Administrator\\Controller\\FoobarController","ExampleViewExample":"Acme\\Example\\Administrator\\View\\Example\\JsonView","ExampleViewFoobar":"Acme\\Example\\Administrator\\View\\Foobar\\HtmlView"}}
--------------------------------------------------------------------------------
/rules-tests/Naming/Rector/FileWithoutNamespace/JoomlaPostRefactoringClassRenameRector/config/configured_rule.php:
--------------------------------------------------------------------------------
1 | services()
22 | ->defaults()
23 | ->autowire()
24 | ->autoconfigure();
25 |
26 | $services->set(RenamedClassHandlerService::class)
27 | ->arg('$directory', realpath(__DIR__ . '/../'));
28 |
29 | $rectorConfig->rule(JoomlaPostRefactoringClassRenameRector::class);
30 | };
31 |
--------------------------------------------------------------------------------
/rules/Naming/Config/JoomlaLegacyPrefixToNamespace.php:
--------------------------------------------------------------------------------
1 | namespacePrefix = $namespacePrefix;
27 | $this->newNamespace = $newNamespace;
28 | $this->excludedClasses = $excludedClasses;
29 | }
30 | public function getNamespacePrefix() : string
31 | {
32 | return $this->namespacePrefix;
33 | }
34 | /**
35 | * @return string[]
36 | */
37 | public function getExcludedClasses() : array
38 | {
39 | return $this->excludedClasses;
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function getNewNamespace(): string
46 | {
47 | return $this->newNamespace;
48 | }
49 | }
--------------------------------------------------------------------------------
/rules/Naming/Rector/FileWithoutNamespace/JoomlaConstants.php:
--------------------------------------------------------------------------------
1 | file->getFilePath();
63 | $filePath = str_replace('\\', '/', $filePath);
64 |
65 | if (strpos($filePath, '/models/fields/') === false)
66 | {
67 | return null;
68 | }
69 |
70 | return parent::refactor($node);
71 | }
72 |
73 | /**
74 | * Process a Name or Identifier node but only if necessary!
75 | *
76 | * @param Name|Identifier $node The node to possibly refactor
77 | *
78 | * @return Identifier|Name|null The refactored node; NULL if no refactoring was necessary / possible.
79 | * @since 1.0.0
80 | */
81 | protected function processNameOrIdentifier($node, bool $isNewFile = false): ?Node
82 | {
83 | // no name → skip
84 | if ($node->toString() === '')
85 | {
86 | return null;
87 | }
88 |
89 | // The class name must begin with a form of "JFormField".
90 | if (!$this->isName($node, 'JFormField*'))
91 | {
92 | return null;
93 | }
94 |
95 | foreach ($this->legacyPrefixesToNamespaces as $legacyPrefixToNamespace)
96 | {
97 | $prefix = substr($this->getName($node), 0, 5);
98 | $excludedClasses = $legacyPrefixToNamespace->getExcludedClasses();
99 |
100 | if ($excludedClasses !== [] && $this->isNames($node, $excludedClasses))
101 | {
102 | return null;
103 | }
104 |
105 | if ($node instanceof Name)
106 | {
107 | return $this->processName($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
108 | }
109 |
110 | return $this->processIdentifier($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
111 | }
112 |
113 | return null;
114 | }
115 |
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/rules/Naming/Rector/FileWithoutNamespace/JoomlaFormRulesRector.php:
--------------------------------------------------------------------------------
1 | file->getFilePath();
63 | $filePath = str_replace('\\', '/', $filePath);
64 |
65 | if (strpos($filePath, '/models/rules/') === false)
66 | {
67 | return null;
68 | }
69 |
70 | return parent::refactor($node);
71 | }
72 |
73 | /**
74 | * Process a Name or Identifier node but only if necessary!
75 | *
76 | * @param Name|Identifier $node The node to possibly refactor
77 | *
78 | * @return Identifier|Name|null The refactored node; NULL if no refactoring was necessary / possible.
79 | * @since 1.0.0
80 | */
81 | protected function processNameOrIdentifier($node, bool $isNewFile = false): ?Node
82 | {
83 | // no name → skip
84 | if ($node->toString() === '')
85 | {
86 | return null;
87 | }
88 |
89 | // The class name must begin with a form of "JFormField".
90 | if (!$this->isName($node, 'JFormRule*'))
91 | {
92 | return null;
93 | }
94 |
95 | foreach ($this->legacyPrefixesToNamespaces as $legacyPrefixToNamespace)
96 | {
97 | $prefix = substr($this->getName($node), 0, 5);
98 | $excludedClasses = $legacyPrefixToNamespace->getExcludedClasses();
99 |
100 | if ($excludedClasses !== [] && $this->isNames($node, $excludedClasses))
101 | {
102 | return null;
103 | }
104 |
105 | if ($node instanceof Name)
106 | {
107 | return $this->processName($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
108 | }
109 |
110 | return $this->processIdentifier($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
111 | }
112 |
113 | return null;
114 | }
115 |
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/rules/Naming/Rector/FileWithoutNamespace/JoomlaHelpersToJ4Rector.php:
--------------------------------------------------------------------------------
1 | toString() === '')
71 | {
72 | return null;
73 | }
74 |
75 | foreach ($this->legacyPrefixesToNamespaces as $legacyPrefixToNamespace)
76 | {
77 | $prefix = $legacyPrefixToNamespace->getNamespacePrefix();
78 | $supported = [
79 | $prefix . 'Helper*',
80 | $prefix . '*Helper',
81 | ];
82 |
83 | if (!$this->isNames($node, $supported))
84 | {
85 | continue;
86 | }
87 |
88 | $excludedClasses = $legacyPrefixToNamespace->getExcludedClasses();
89 |
90 | if ($excludedClasses !== [] && $this->isNames($node, $excludedClasses))
91 | {
92 | return null;
93 | }
94 |
95 | if ($node instanceof Name)
96 | {
97 | return $this->processName($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
98 | }
99 |
100 | return $this->processIdentifier($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
101 | }
102 |
103 | return null;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/rules/Naming/Rector/FileWithoutNamespace/JoomlaHtmlHelpersRector.php:
--------------------------------------------------------------------------------
1 | classMethodVisibilityGuard = $classMethodVisibilityGuard;
72 | $this->visibilityManipulator = $visibilityManipulator;
73 | $this->reflectionResolver = $reflectionResolver;
74 | }
75 |
76 | public function getNodeTypes(): array
77 | {
78 | return array_merge(parent::getNodeTypes(), [Class_::class, ClassMethod::class, StaticCall::class]);
79 | }
80 |
81 | /**
82 | * Get the rule definition.
83 | *
84 | * This was used to generate the initial test fixture.
85 | *
86 | * @return RuleDefinition
87 | * @throws \Symplify\RuleDocGenerator\Exception\PoorDocumentationException
88 | * @since 1.0.0
89 | */
90 | public function getRuleDefinition(): RuleDefinition
91 | {
92 | return new RuleDefinition('Convert legacy Joomla 3 HTML Helper class names into Joomla 4 namespaced ones.', [
93 | new CodeSample(
94 | <<<'CODE_SAMPLE'
95 | abstract class JHtmlExample
96 | {
97 | static function derp(): string
98 | {
99 | return "derp";
100 | }
101 | }
102 | CODE_SAMPLE
103 | , <<<'CODE_SAMPLE'
104 | namespace Acme\Example\Administrator\Service\HTML;
105 |
106 | class Example
107 | {
108 | function derp(): string
109 | {
110 | return "derp";
111 | }
112 | }
113 | CODE_SAMPLE
114 | ),
115 | ]);
116 | }
117 |
118 | public function refactor(Node $node): ?Node
119 | {
120 | // Makes sure the immediate path is /helpers/html
121 | $filePath = $this->file->getFilePath();
122 | $filePath = str_replace('\\', '/', $filePath);
123 | $pathBits = explode('/', $filePath);
124 |
125 | if (implode('/', array_slice($pathBits, -3, 2)) !== 'helpers/html')
126 | {
127 | return null;
128 | }
129 |
130 | /**
131 | * Change abstract classes to non-abstract
132 | */
133 | if ($node instanceof Class_)
134 | {
135 | return $this->refactorClass($node);
136 | }
137 |
138 | /**
139 | * Change static methods to non-static and refactor local static calls to non-static
140 | *
141 | * @see \Rector\RemovingStatic\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector
142 | */
143 | if ($node instanceof ClassMethod)
144 | {
145 | return $this->refactorClassMethod($node);
146 | }
147 |
148 | if ($node instanceof StaticCall)
149 | {
150 | return $this->refactorStaticCall($node);
151 | }
152 |
153 | /**
154 | * Add namespace and move the file
155 | */
156 | return parent::refactor($node);
157 | }
158 |
159 |
160 | /**
161 | * Process a Name or Identifier node but only if necessary!
162 | *
163 | * @param Name|Identifier $node The node to possibly refactor
164 | *
165 | * @return Identifier|Name|null The refactored node; NULL if no refactoring was necessary / possible.
166 | * @since 1.0.0
167 | */
168 | protected function processNameOrIdentifier($node, bool $isNewFile = false): ?Node
169 | {
170 | // no name → skip
171 | if ($node->toString() === '')
172 | {
173 | return null;
174 | }
175 |
176 | // The class name must begin with a form of "JHtml".
177 | $supported = [
178 | 'JHtml*',
179 | 'Jhtml*',
180 | 'JHTML*',
181 | 'jhtml*',
182 | 'jHtml*',
183 | 'jHTML*',
184 | ];
185 |
186 | if (!$this->isNames($node, $supported))
187 | {
188 | return null;
189 | }
190 |
191 | foreach ($this->legacyPrefixesToNamespaces as $legacyPrefixToNamespace)
192 | {
193 | $prefix = substr($this->getName($node), 0, 5);
194 | $excludedClasses = $legacyPrefixToNamespace->getExcludedClasses();
195 |
196 | if ($excludedClasses !== [] && $this->isNames($node, $excludedClasses))
197 | {
198 | return null;
199 | }
200 |
201 | if ($node instanceof Name)
202 | {
203 | return $this->processName($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
204 | }
205 |
206 | return $this->processIdentifier($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
207 | }
208 |
209 | return null;
210 | }
211 |
212 | /**
213 | * Convert an abstract class to non-abstract
214 | *
215 | * @param Class_ $node
216 | *
217 | * @return Class_|null
218 | */
219 | private function refactorClass(Class_ $node)
220 | {
221 | if (!$node->isAbstract())
222 | {
223 | return null;
224 | }
225 |
226 | $node->flags = $node->flags & ~Class_::MODIFIER_ABSTRACT;
227 |
228 | return $node;
229 | }
230 |
231 | /**
232 | * Convert a static class method to non-static
233 | *
234 | * @param ClassMethod $classMethod
235 | *
236 | * @return ClassMethod|null
237 | * @since 1.0.0
238 | * @see \Rector\RemovingStatic\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector
239 | */
240 | private function refactorClassMethod(ClassMethod $classMethod) : ?ClassMethod
241 | {
242 | if (!$classMethod->isStatic())
243 | {
244 | return null;
245 | }
246 |
247 | $dirty = false;
248 |
249 | if (($classMethod->flags & Class_::VISIBILITY_MODIFIER_MASK) === 0)
250 | {
251 | $this->visibilityManipulator->makePublic($classMethod);
252 |
253 | $dirty = true;
254 | }
255 |
256 | $classReflection = $this->reflectionResolver->resolveClassReflection($classMethod);
257 |
258 | if (!$classReflection instanceof ClassReflection)
259 | {
260 | return $dirty ? $classMethod : null;
261 | }
262 |
263 | if ($this->classMethodVisibilityGuard->isClassMethodVisibilityGuardedByParent($classMethod, $classReflection))
264 | {
265 | return $dirty ? $classMethod : null;
266 | }
267 |
268 | // Change static calls to non-static ones, but only if in non-static method!!!
269 | $this->visibilityManipulator->makeNonStatic($classMethod);
270 |
271 | return $classMethod;
272 | }
273 |
274 | /**
275 | * Refactor a static method call to non-static, within the same class only
276 | *
277 | * @param StaticCall $staticCall
278 | *
279 | * @return MethodCall|null
280 | * @since 1.0.0
281 | * @see \Rector\RemovingStatic\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector
282 | */
283 | private function refactorStaticCall(StaticCall $staticCall): ?MethodCall
284 | {
285 | $classLike = $this->betterNodeFinder->findParentType($staticCall, ClassLike::class);
286 |
287 | if (!$classLike instanceof ClassLike)
288 | {
289 | return null;
290 | }
291 |
292 | /** @var ClassMethod[] $classMethods */
293 | $classMethods = $this->betterNodeFinder->findInstanceOf($classLike, ClassMethod::class);
294 |
295 | foreach ($classMethods as $classMethod)
296 | {
297 | if (!$this->isClassMethodMatchingStaticCall($classMethod, $staticCall))
298 | {
299 | continue;
300 | }
301 |
302 | if ($this->isInStaticClassMethod($staticCall))
303 | {
304 | continue;
305 | }
306 |
307 | $thisVariable = new Variable('this');
308 |
309 | return new MethodCall($thisVariable, $staticCall->name, $staticCall->args);
310 | }
311 |
312 | return null;
313 | }
314 |
315 | /**
316 | * Are we inside a static class method?
317 | *
318 | * @param StaticCall $staticCall
319 | *
320 | * @return bool
321 | * @since 1.0.0
322 | * @see \Rector\RemovingStatic\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector
323 | */
324 | private function isInStaticClassMethod(StaticCall $staticCall): bool
325 | {
326 | $locationClassMethod = $this->betterNodeFinder->findParentType($staticCall, ClassMethod::class);
327 |
328 | if (!$locationClassMethod instanceof ClassMethod)
329 | {
330 | return \false;
331 | }
332 |
333 | return $locationClassMethod->isStatic();
334 | }
335 |
336 | /**
337 | * @param ClassMethod $classMethod
338 | * @param StaticCall $staticCall
339 | *
340 | * @return bool
341 | * @since 1.0.0
342 | * @see \Rector\RemovingStatic\Rector\ClassMethod\LocallyCalledStaticMethodToNonStaticRector
343 | */
344 | private function isClassMethodMatchingStaticCall(ClassMethod $classMethod, StaticCall $staticCall): bool
345 | {
346 | $classLike = $this->betterNodeFinder->findParentType($classMethod, ClassLike::class);
347 |
348 | if (!$classLike instanceof ClassLike)
349 | {
350 | return \false;
351 | }
352 |
353 | $className = (string) $this->nodeNameResolver->getName($classLike);
354 | $objectType = new ObjectType($className);
355 | $callerType = $this->nodeTypeResolver->getType($staticCall->class);
356 |
357 | return $objectType->equals($callerType);
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/rules/Naming/Rector/FileWithoutNamespace/JoomlaLegacyMVCToJ4Rector.php:
--------------------------------------------------------------------------------
1 | removedAndAddedFilesCollector = $removedAndAddedFilesCollector;
90 | $this->renamedClassHandlerService = $renamedClassHandlerService;
91 | }
92 |
93 | /**
94 | * Configuration handler. Called internally by Rector.
95 | *
96 | * @param JoomlaLegacyPrefixToNamespace[] $configuration
97 | *
98 | * @since 1.0.0
99 | */
100 | public function configure(array $configuration): void
101 | {
102 | Assert::allIsAOf($configuration, JoomlaLegacyPrefixToNamespace::class);
103 | $this->legacyPrefixesToNamespaces = $configuration;
104 | }
105 |
106 | /**
107 | * Tell Rector which AST node types we can handle with this rule.
108 | *
109 | * @return array>
110 | * @since 1.0.0
111 | */
112 | public function getNodeTypes(): array
113 | {
114 | return [
115 | FileWithoutNamespace::class, Namespace_::class,
116 | ];
117 | }
118 |
119 | /**
120 | * @return RemovedAndAddedFilesCollector
121 | */
122 | public function getRemovedAndAddedFilesCollector(): RemovedAndAddedFilesCollector
123 | {
124 | return $this->removedAndAddedFilesCollector;
125 | }
126 |
127 | /**
128 | * Get the rule definition.
129 | *
130 | * This was used to generate the initial test fixture.
131 | *
132 | * @return RuleDefinition
133 | * @throws \Symplify\RuleDocGenerator\Exception\PoorDocumentationException
134 | * @since 1.0.0
135 | */
136 | public function getRuleDefinition(): RuleDefinition
137 | {
138 | return new RuleDefinition('Convert legacy Joomla 3 MVC class names into Joomla 4 namespaced ones.', [
139 | new CodeSample(
140 | <<<'CODE_SAMPLE'
141 | /** @var FooModelBar $someModel */
142 | $model = new FooModelBar;
143 | CODE_SAMPLE
144 | , <<<'CODE_SAMPLE'
145 | /** @var \Acme\Foo\BarModel $someModel */
146 | $model = new BarModel;
147 | CODE_SAMPLE
148 | ),
149 | ]);
150 | }
151 |
152 | /**
153 | * Performs the refactoring on the supported nodes.
154 | *
155 | * @param FileWithoutNamespace|Namespace_ $node
156 | *
157 | * @since 1.0.0
158 | */
159 | public function refactor(Node $node): ?Node
160 | {
161 | $this->newNamespace = null;
162 |
163 | if ($node instanceof FileWithoutNamespace)
164 | {
165 | $changedStmts = $this->refactorStmts($node->stmts, true);
166 |
167 | if ($changedStmts === null)
168 | {
169 | return null;
170 | }
171 |
172 | $node->stmts = $changedStmts;
173 |
174 | // Add a new namespace?
175 | if ($this->newNamespace !== null)
176 | {
177 | return new Namespace_(new Name($this->newNamespace), $changedStmts);
178 | }
179 | }
180 |
181 | if ($node instanceof Namespace_)
182 | {
183 | return $this->refactorNamespace($node);
184 | }
185 |
186 | return null;
187 | }
188 |
189 | /**
190 | * Processes an Identifier node
191 | *
192 | * @param Identifier $identifier The node to process
193 | * @param string $prefix The legacy Joomla 3 prefix, e.g. Example
194 | * @param string $newNamespacePrefix The Joomla 4 common namespace prefix e.g. \Acme\Example
195 | * @param bool $isNewFile Is this a file without a namespace already defined?
196 | *
197 | * @return Identifier|null The refactored identified; null if no refactoring is necessary / possible
198 | * @throws ShouldNotHappenException A file had two classes in it yielding different namespaces. Don't do that!
199 | * @since 1.0.0
200 | */
201 | protected function processIdentifier(Identifier $identifier, string $prefix, string $newNamespacePrefix, bool $isNewFile = false): ?Identifier
202 | {
203 | $parentNode = $identifier->getAttribute(AttributeKey::PARENT_NODE);
204 |
205 | if (!$parentNode instanceof Class_)
206 | {
207 | return null;
208 | }
209 |
210 | $name = $this->getName($identifier);
211 |
212 | if ($name === null)
213 | {
214 | return null;
215 | }
216 |
217 | $newNamespace = '';
218 | $lastNewNamePart = $name;
219 | $fqn = $this->legacyClassNameToNamespaced($name, $prefix, $newNamespacePrefix, $isNewFile);
220 |
221 | if ($fqn === $name)
222 | {
223 | return $identifier;
224 | }
225 |
226 | $this->renamedClassHandlerService->addEntry($name, $fqn, $newNamespacePrefix);
227 |
228 | $bits = explode('\\', $fqn);
229 |
230 | if (count($bits) > 1)
231 | {
232 | $lastNewNamePart = array_pop($bits);
233 | $newNamespace = implode('\\', $bits);
234 | }
235 |
236 | if ($this->newNamespace !== null && $this->newNamespace !== $newNamespace)
237 | {
238 | throw new ShouldNotHappenException('There cannot be 2 different namespaces in one file');
239 | }
240 |
241 | $this->newNamespace = $newNamespace;
242 | $identifier->name = $lastNewNamePart;
243 |
244 | $this->moveFile($newNamespacePrefix, $fqn);
245 |
246 | return $identifier;
247 | }
248 |
249 | /**
250 | * Process a Name node
251 | *
252 | * @param Name $name The node to refactor
253 | * @param string $prefix The legacy Joomla 3 prefix, e.g. Example
254 | * @param string $newNamespacePrefix The Joomla 4 common namespace prefix e.g. \Acme\Example
255 | * @param bool $isNewFile Is this a file without a namespace already defined?
256 | *
257 | * @return Name The refactored Node. Original node if nothing was refactored.
258 | * @since 1.0.0
259 | */
260 | protected function processName(Name $name, string $prefix, string $newNamespace, bool $isNewFile = false): Name
261 | {
262 | // The class name
263 | $legacyClassName = $this->getName($name);
264 |
265 | $fqn = $this->legacyClassNameToNamespaced($legacyClassName, $prefix, $newNamespace, $isNewFile);
266 |
267 | if ($fqn === $legacyClassName)
268 | {
269 | return $name;
270 | }
271 |
272 | $name->parts = explode('\\', $fqn);
273 |
274 | return $name;
275 | }
276 |
277 | /**
278 | * Process a Name or Identifier node but only if necessary!
279 | *
280 | * @param Name|Identifier $node The node to possibly refactor
281 | *
282 | * @return Identifier|Name|null The refactored node; NULL if no refactoring was necessary / possible.
283 | * @since 1.0.0
284 | */
285 | protected function processNameOrIdentifier($node, bool $isNewFile = false): ?Node
286 | {
287 | // no name → skip
288 | if ($node->toString() === '')
289 | {
290 | return null;
291 | }
292 |
293 | foreach ($this->legacyPrefixesToNamespaces as $legacyPrefixToNamespace)
294 | {
295 | $prefix = $legacyPrefixToNamespace->getNamespacePrefix();
296 | $supported = [
297 | $prefix . 'Controller*',
298 | $prefix . 'Model*',
299 | $prefix . 'View*',
300 | $prefix . 'Table*',
301 | ];
302 |
303 | if (!$this->isNames($node, $supported))
304 | {
305 | continue;
306 | }
307 |
308 | $excludedClasses = $legacyPrefixToNamespace->getExcludedClasses();
309 |
310 | if ($excludedClasses !== [] && $this->isNames($node, $excludedClasses))
311 | {
312 | return null;
313 | }
314 |
315 | if ($node instanceof Name)
316 | {
317 | return $this->processName($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
318 | }
319 |
320 | return $this->processIdentifier($node, $prefix, $legacyPrefixToNamespace->getNewNamespace(), $isNewFile);
321 | }
322 |
323 | return null;
324 | }
325 |
326 | /**
327 | * Refactor a namespace node
328 | *
329 | * @param Namespace_ $namespace The node to possibly refactor
330 | *
331 | * @return Namespace_|null The refactored node; NULL if nothing is refactored
332 | * @since 1.0.0
333 | */
334 | protected function refactorNamespace(Namespace_ $namespace): ?Namespace_
335 | {
336 | $changedStmts = $this->refactorStmts($namespace->stmts);
337 |
338 | if ($changedStmts === null)
339 | {
340 | return null;
341 | }
342 |
343 | return $namespace;
344 | }
345 |
346 | /**
347 | * Refactor an array of statement nodes
348 | *
349 | * @param array $stmts The array of nodes to possibly refactor
350 | * @param bool $isNewFile Is this a file without a namespace?
351 | *
352 | * @return array|null The array of refactored statements. NULL if was nothing to refactor.
353 | * @since 1.0.0
354 | */
355 | protected function refactorStmts(array $stmts, bool $isNewFile = false): ?array
356 | {
357 | $hasChanged = \false;
358 |
359 | $this->traverseNodesWithCallable($stmts, function (Node $node) use (&$hasChanged, $isNewFile): ?Node {
360 | if (
361 | !$node instanceof Name
362 | && !$node instanceof Identifier
363 | && !$node instanceof Property
364 | && !$node instanceof FunctionLike
365 | )
366 | {
367 | return null;
368 | }
369 |
370 | if (
371 | $node instanceof Name
372 | || $node instanceof Identifier
373 | )
374 | {
375 | $changedNode = $this->processNameOrIdentifier($node, $isNewFile);
376 |
377 | if ($changedNode instanceof Node)
378 | {
379 | $hasChanged = \true;
380 |
381 | return $changedNode;
382 | }
383 | }
384 |
385 | return null;
386 | });
387 |
388 | if ($hasChanged)
389 | {
390 | return $stmts;
391 | }
392 |
393 | return null;
394 | }
395 | }
396 |
--------------------------------------------------------------------------------
/rules/Naming/Rector/FileWithoutNamespace/JoomlaNamespaceHandlingTrait.php:
--------------------------------------------------------------------------------
1 | file->getFilePath());
27 | $pathBits = explode('/', $path);
28 |
29 | while (!empty($pathBits))
30 | {
31 | $lastPart = array_pop($pathBits);
32 |
33 | if ($lastPart === null)
34 | {
35 | return null;
36 | }
37 |
38 | $isComponent = substr($lastPart, 0, 4) === 'com_';
39 |
40 | if ($isComponent || in_array($lastPart, JoomlaConstants::ACCEPTABLE_CONTAINMENT_FOLDERS))
41 | {
42 | $pathBits[] = $lastPart;
43 |
44 | break;
45 | }
46 | }
47 |
48 | if (empty($pathBits))
49 | {
50 | return null;
51 | }
52 |
53 | return implode('/', $pathBits);
54 | }
55 |
56 | /**
57 | * Figure out which application side (admin, side or api) this file corresponds to.
58 | *
59 | * @return string One of 'Administrator', 'Site', 'Api'
60 | * @since 1.0.0
61 | */
62 | protected function getApplicationSide(): string
63 | {
64 | /**
65 | * I need to find the parent folder of my file to see if it's one of admin, administrator, backend, site,
66 | * frontend, api and decide which namespace suffix to add.
67 | */
68 | // The full path to the current file, normalised as a UNIX path
69 | $fullPath = str_replace('\\', '/', $this->file->getFilePath());
70 | // Explode the path to an array
71 | $pathBits = explode('/', $fullPath);
72 | // This is the filename
73 | array_pop($pathBits);
74 | // Remove the immediate folder we are in, I can infer it from the classname, duh
75 | $temp = array_pop($pathBits);
76 | // But, wait! What if it's the legacy display controller?! In this case I need to put that last folder back!
77 | if (in_array($temp, JoomlaConstants::ACCEPTABLE_CONTAINMENT_FOLDERS) || substr($temp, 0, 4) === 'com_')
78 | {
79 | $pathBits[] = $temp;
80 | }
81 | $isTmpl = $temp === 'tmpl';
82 | // Get the parent folder
83 | $parentFolder = array_pop($pathBits);
84 |
85 | // If the parent folder starts with com_ I will get its grandparent instead
86 | if (substr($parentFolder, 0, 4) === 'com_')
87 | {
88 | $parentFolder = array_pop($pathBits);
89 | $parentFolder = array_pop($pathBits);
90 | }
91 |
92 | switch (strtolower(trim($parentFolder ?: '')))
93 | {
94 | case 'admin':
95 | case 'administrator':
96 | case 'backend':
97 | return 'Administrator';
98 |
99 | case 'site':
100 | case 'frontend':
101 | return 'Site';
102 |
103 | case 'api':
104 | return 'Api';
105 | }
106 |
107 | // I have no idea where I am. Okay, let's start going back until I find something that makes sense.
108 | $pathBits = explode('/', $fullPath);
109 |
110 | while (!empty($pathBits))
111 | {
112 | $lastFolder = array_pop($pathBits);
113 |
114 | if (!in_array($lastFolder, JoomlaConstants::ACCEPTABLE_CONTAINMENT_FOLDERS))
115 | {
116 | continue;
117 | }
118 |
119 | switch (strtolower(trim($lastFolder ?: '')))
120 | {
121 | case 'admin':
122 | case 'administrator':
123 | case 'backend':
124 | return 'Administrator';
125 |
126 | case 'site':
127 | case 'frontend':
128 | return 'Site';
129 |
130 | case 'api':
131 | return 'Api';
132 | }
133 | }
134 |
135 | return 'Site';
136 | }
137 |
138 | /**
139 | * Convert a legacy Joomla 3 class name to its Joomla 4 namespaced equivalent.
140 | *
141 | * @param string $legacyClassName The legacy class name, e.g. ExampleControllerFoobar
142 | * @param string $componentPrefix The common prefix of the legacy Joomla 3 classes, e.g. Example for
143 | * com_example
144 | * @param string $newNamespace The common namespace prefix for the Joomla 4 component
145 | * @param bool $isNewFile Is this a file without a namespace already defined?
146 | *
147 | * @return string The FQN of the namespaced Joomla 4 class e.g.
148 | * \Acme\Example\Administrator\Controller\ExampleController
149 | * @since 1.0.0
150 | */
151 | protected function legacyClassNameToNamespaced(string $legacyClassName, string $componentPrefix, string $newNamespace, bool $isNewFile = false): string
152 | {
153 | $applicationSide = $this->getApplicationSide();
154 |
155 | // Controller, Model and Table are pretty straightforward
156 | $legacySuffixes = ['Controller', 'Model', 'Table', 'Helper'];
157 |
158 | /**
159 | * Special case: JHtml prefix
160 | *
161 | * HTML helper static classes become non-static HTML helper services, renamed and moved accordingly.
162 | */
163 | if (strtoupper(substr($legacyClassName, 0, 5)) === 'JHTML')
164 | {
165 | $fqn = trim($newNamespace, '\\')
166 | . '\\' . $applicationSide
167 | . '\\Service\\Html\\'
168 | . ucfirst(strtolower(substr($legacyClassName, 5)));
169 |
170 | return $fqn;
171 | }
172 | /**
173 | * Special case: JFormRule prefix
174 | *
175 | * Form rule classes are renamed and moved accordingly.
176 | */
177 | elseif (strtoupper(substr($legacyClassName, 0, 9)) === 'JFORMRULE')
178 | {
179 | $fqn = trim($newNamespace, '\\')
180 | . '\\' . $applicationSide
181 | . '\\Rule\\'
182 | . ucfirst(strtolower(substr($legacyClassName, 9))) . 'Rule';
183 |
184 | return $fqn;
185 | }
186 | /**
187 | * Special case: JFormField prefix
188 | *
189 | * Form field type classes are renamed and moved accordingly.
190 | */
191 | elseif (strtoupper(substr($legacyClassName, 0, 10)) === 'JFORMFIELD')
192 | {
193 | $bareName = strtolower(substr($legacyClassName, 10));
194 | $bareName = str_replace('_', '\\', $bareName);
195 | $bareParts = explode('\\', $bareName);
196 | $bareParts = array_map('ucfirst', $bareParts);
197 | $bareName = implode('\\', $bareParts);
198 | $fqn = trim($newNamespace, '\\')
199 | . '\\' . $applicationSide
200 | . '\\Field\\'
201 | . $bareName . 'Field';
202 |
203 | return $fqn;
204 | }
205 |
206 | foreach ($legacySuffixes as $legacySuffix)
207 | {
208 | $fullLegacyPrefix = $componentPrefix . $legacySuffix;
209 |
210 | if ($legacyClassName === $fullLegacyPrefix)
211 | {
212 | if (!in_array($legacySuffix, ['Controller', 'Helper']))
213 | {
214 | return $legacyClassName;
215 | }
216 |
217 | // If the file already has a namespace go away. We have already refactored it.
218 | if (!$isNewFile)
219 | {
220 | return $legacyClassName;
221 | }
222 |
223 | switch ($legacySuffix)
224 | {
225 | case 'Controller':
226 | $legacyClassName = $fullLegacyPrefix . 'Display';
227 | break;
228 |
229 | case 'Helper':
230 | $legacyClassName = $fullLegacyPrefix . $componentPrefix;
231 | break;
232 | }
233 | }
234 | /**
235 | * Special handling for regular Helper classes.
236 | *
237 | * Naming pattern for com_example: ExampleSomethingHelper
238 | */
239 | elseif ($legacySuffix === 'Helper' && substr($legacyClassName, -6) === 'Helper')
240 | {
241 | // Rewrite ExampleSomethingHelper to ExampleHelperSomething for our code below to work.
242 | $plain = substr($legacyClassName, strlen($componentPrefix));
243 | $plain = substr($plain, 0, -strlen($legacySuffix));
244 | $legacyClassName = $componentPrefix . $legacySuffix . $plain;
245 | }
246 |
247 | if (strpos($legacyClassName, $fullLegacyPrefix) !== 0)
248 | {
249 | continue;
250 | }
251 |
252 | // Convert FooModelBar => BarModel
253 | $bareName = ucfirst(strtolower(substr($legacyClassName, strlen($fullLegacyPrefix)))) . $legacySuffix;
254 |
255 | $fqn = trim($newNamespace, '\\')
256 | . '\\' . $applicationSide
257 | . '\\' . $legacySuffix
258 | . '\\' . $bareName;
259 |
260 | return $fqn;
261 | }
262 |
263 | /**
264 | * Special handling for View classes
265 | */
266 | $fullLegacyPrefix = $componentPrefix . 'View';
267 |
268 | if (strpos($legacyClassName, $fullLegacyPrefix) !== 0)
269 | {
270 | return $legacyClassName;
271 | }
272 |
273 | // The full path to the current file, normalised as a UNIX path
274 | $fullPath = str_replace('\\', '/', $this->file->getFilePath());
275 | // Explode the path to an array
276 | $pathBits = explode('/', $fullPath);
277 | // This is the filename
278 | $filename = array_pop($pathBits);
279 | /**
280 | * Strip the 'view.' prefix and '.php' suffix from the filename, add 'View' to it. This changes a filename
281 | * view.html.php into the HtmlView classname.
282 | */
283 | $leafClassName = ucfirst(strtolower(str_replace(['view.', '.php'], ['', ''], $filename))) . 'View';
284 |
285 | // FooViewBar => Bar\HtmlView
286 | $bareName = ucfirst(strtolower(substr($legacyClassName, strlen($fullLegacyPrefix)))) . '\\' . $leafClassName;
287 | $fqn = trim($newNamespace, '\\')
288 | . '\\' . $applicationSide
289 | . '\\View'
290 | . '\\' . $bareName;
291 |
292 | return $fqn;
293 | }
294 |
295 | /**
296 | * Moves a (namespaced) file to its canonical PSR-4 folder
297 | *
298 | * @param string $newNamespacePrefix The common namespace prefix for the component.
299 | * @param string $fqn The FQN of the class whose file is being moved.
300 | *
301 | * @return void
302 | * @since 1.0.0
303 | */
304 | protected function moveFile(string $newNamespacePrefix, string $fqn)
305 | {
306 | // I also need to move the file
307 | $thisSideRoot = $this->divineExtensionRootFolder();
308 |
309 | if ($thisSideRoot === null)
310 | {
311 | return;
312 | }
313 |
314 | // Remove the common namespace prefix
315 | $newNamespacePrefix = trim($newNamespacePrefix, '\\');
316 | $fqn = trim($fqn, '\\');
317 |
318 | if (strpos($fqn, $newNamespacePrefix) !== 0)
319 | {
320 | // Whatever happened is massively wrong. Give up.
321 | return;
322 | }
323 |
324 | /**
325 | * Convert the namespace \Acme\Example\Administrator\Controller\ExampleController to
326 | * /path/to/component/admin/src/Controller/ExampleController.php
327 | *
328 | * Logic:
329 | * * Start with \Acme\Example\Administrator\Controller\ExampleController
330 | * * Remove the common namespace, so it becomes Administrator\Controller\ExampleController
331 | * * Remove the first part (Administrator). We're left with Controller\ExampleController.
332 | * * Replace the backslashes with directory separators e.g. Controller/ExampleController
333 | * * Make the path by combining
334 | * - The root of the component side e.g. /path/to/component/admin
335 | * - The literal 'src'
336 | * - The relative path from the previous step e.g. Controller/ExampleController
337 | * - The literal '.php'
338 | * There we get /path/to/component/admin/src/Controller/ExampleController.php
339 | */
340 | $relativeName = trim(substr($fqn, strlen($newNamespacePrefix)), '\\');
341 | $fqnParts = explode('\\', $relativeName);
342 | array_shift($fqnParts);
343 | $newPath = $thisSideRoot . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . implode(
344 | DIRECTORY_SEPARATOR,
345 | $fqnParts
346 | ) . '.php';
347 |
348 | // Make sure we actually DO need to rename the file.
349 | if ($this->file->getFilePath() === $newPath)
350 | {
351 | // Okay, this is already in the correct PSR-4 folder. Bye-bye!
352 | return;
353 | }
354 |
355 | // Move the file
356 | $this->getRemovedAndAddedFilesCollector()->addMovedFile($this->file, $newPath);
357 | }
358 | }
--------------------------------------------------------------------------------
/rules/Naming/Rector/FileWithoutNamespace/RenamedClassHandlerService.php:
--------------------------------------------------------------------------------
1 | [],
32 | 'admin' => [],
33 | ];
34 |
35 | /**
36 | * Public constructor
37 | *
38 | * @param string $directory The directory of the _classmap.json file
39 | *
40 | * @since 1.0.0
41 | */
42 | public function __construct(string $directory)
43 | {
44 | $this->directory = $directory;
45 |
46 | $this->load();
47 | }
48 |
49 | /**
50 | * Called on service destruction. Auto-saves the class map file.
51 | *
52 | * @since 1.0.0
53 | */
54 | public function __destruct()
55 | {
56 | $this->save();
57 | }
58 |
59 | /**
60 | * Adds an entry into the class map
61 | *
62 | * @param string $legacyClass The legacy class which we renamed from.
63 | * @param string $namespacedClass The FQN of the namespaced class we renamed to.
64 | * @param string $namespacePrefix The namespace prefix we used.
65 | *
66 | * @return void
67 | * @since 1.0.0
68 | */
69 | public function addEntry(string $legacyClass, string $namespacedClass, string $namespacePrefix)
70 | {
71 | $prefix = trim($namespacePrefix, '\\');
72 | $tempName = trim($namespacedClass, '\\');
73 |
74 | if (strpos($tempName, $prefix) !== 0)
75 | {
76 | return;
77 | }
78 |
79 | $tempName = trim(substr($tempName, strlen($prefix)), '\\');
80 | $parts = explode('\\', $tempName);
81 |
82 | if (!in_array($parts[0], ['Administrator', 'Site']))
83 | {
84 | return;
85 | }
86 |
87 | $side = $parts[0] === 'Site' ? 'site' : 'admin';
88 |
89 | $this->map[$side][$legacyClass] = $namespacedClass;
90 | }
91 |
92 | /**
93 | * Return an old to new classname map for a specific application side
94 | *
95 | * @param string|null $side The application side: admin or site.
96 | *
97 | * @return array
98 | * @since 1.0.0
99 | */
100 | public function getOldToNewMap(?string $side = 'admin')
101 | {
102 | return $this->map[$side] ?? [];
103 | }
104 |
105 | /**
106 | * Load the already saved class map from _classmap.json
107 | *
108 | * @return void
109 | * @since 1.0.0
110 | */
111 | private function load()
112 | {
113 | $filePath = $this->directory . '/_classmap.json';
114 |
115 | if (!is_file($filePath))
116 | {
117 | return;
118 | }
119 |
120 | $contents = file_get_contents($filePath);
121 | $this->map = @json_decode($contents, true) ?? [
122 | 'site' => [],
123 | 'admin' => [],
124 | ];
125 | }
126 |
127 | /**
128 | * Saved the class map into _classmap.json
129 | *
130 | * @return void
131 | * @since 1.0.0
132 | */
133 | private function save()
134 | {
135 | $filePath = $this->directory . '/_classmap.json';
136 | $contents = json_encode($this->map);
137 |
138 | file_put_contents($filePath, $contents);
139 | }
140 | }
--------------------------------------------------------------------------------
/rules/Naming/Rector/JoomlaPostRefactoringClassRenameRector.php:
--------------------------------------------------------------------------------
1 | renamedClassHandlerService = $renamedClassHandlerService;
54 | $this->classRenamer = $classRenamer;
55 | $this->rectorConfigProvider = $rectorConfigProvider;
56 | }
57 |
58 | /**
59 | * @return array>
60 | */
61 | public function getNodeTypes(): array
62 | {
63 | return [
64 | Name::class, Property::class, FunctionLike::class, Expression::class, ClassLike::class, Namespace_::class,
65 | FileWithoutNamespace::class, Use_::class,
66 | ];
67 | }
68 |
69 | public function getRuleDefinition(): RuleDefinition
70 | {
71 | return new RuleDefinition('Replaces defined classes by new ones.', [
72 | new CodeSample(
73 | <<<'CODE_SAMPLE'
74 | class ExampleModelFoobar extends \Joomla\CMS\MVC\Model\BaseModel
75 | {
76 | /**
77 | * @return ExampleTableFoobar
78 | */
79 | public function doSomething(): ExampleTableFoobar
80 | {
81 | return $this->getTable();
82 | }
83 | }
84 | CODE_SAMPLE
85 | , <<<'CODE_SAMPLE'
86 | namespace \Acme\Example\Administrator\Model;
87 |
88 | use \Acme\Example\Administrator\Table\FoobarTable;
89 |
90 | class FoobarModel extends \Joomla\CMS\MVC\Model\BaseModel
91 | {
92 | /**
93 | * @return FoobarTable
94 | */
95 | public function doSomething(): FoobarTable
96 | {
97 | return $this->getTable();
98 | }
99 | }
100 | CODE_SAMPLE
101 | ),
102 | ]);
103 | }
104 |
105 | /**
106 | * @param FunctionLike|Name|ClassLike|Expression|Namespace_|Property|FileWithoutNamespace|Use_ $node
107 | */
108 | public function refactor(Node $node): ?Node
109 | {
110 | $applicationSide = strtolower($this->getApplicationSide());
111 | $applicationSide = ($applicationSide === 'administrator') ? 'admin' : $applicationSide;
112 |
113 | $oldToNewClasses = $this->renamedClassHandlerService->getOldToNewMap($applicationSide);
114 |
115 | if ($oldToNewClasses === [])
116 | {
117 | return null;
118 | }
119 |
120 | if (!$node instanceof Use_)
121 | {
122 | return $this->classRenamer->renameNode($node, $oldToNewClasses);
123 | }
124 |
125 | if (!$this->rectorConfigProvider->shouldImportNames())
126 | {
127 | return null;
128 | }
129 |
130 | return $this->processCleanUpUse($node, $oldToNewClasses);
131 | }
132 |
133 | /**
134 | * @param array $oldToNewClasses
135 | */
136 | private function processCleanUpUse(Use_ $use, array $oldToNewClasses): ?Use_
137 | {
138 | foreach ($use->uses as $useUse)
139 | {
140 | if (!$useUse->alias instanceof Identifier && isset($oldToNewClasses[$useUse->name->toString()]))
141 | {
142 | $this->removeNode($use);
143 |
144 | return $use;
145 | }
146 | }
147 |
148 | return null;
149 | }
150 | }
--------------------------------------------------------------------------------