diff --git a/.gitattributes b/.gitattributes index 72d6d2c..ce96f76 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,11 @@ * text eol=lf +/.github/ export-ignore +/tests/ export-ignore /.gitattributes export-ignore /.gitignore export-ignore -/.travis.yml export-ignore +/phpcs.xml.dist export-ignore /phpunit.xml.dist export-ignore -/README.md export-ignore -/tests/ export-ignore \ No newline at end of file +/psalm.xml export-ignore +/CHANGELOG.md export-ignore +/README.md export-ignore \ No newline at end of file diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..57e4a83 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,58 @@ +name: Static Analysis + +on: + push: + paths: + - '**workflows/static-analysis.yml' + - '**.php' + - '**phpcs.xml.dist' + - '**psalm.xml' + pull_request: + paths: + - '**workflows/static-analysis.yml' + - '**.php' + - '**phpcs.xml.dist' + - '**psalm.xml' + workflow_dispatch: + inputs: + jobs: + required: true + type: choice + default: 'Run all' + description: 'Choose jobs to run' + options: + - 'Run all' + - 'Run PHPCS only' + - 'Run Psalm only' + - 'Run lint only' + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + lint: + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run lint only')) }} + uses: inpsyde/reusable-workflows/.github/workflows/lint-php.yml@main + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + with: + PHP_VERSION: ${{ matrix.php }} + LINT_ARGS: '-e php --colors --show-deprecated ./inc ./src' + + coding-standards-analysis: + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run PHPCS only')) }} + uses: inpsyde/reusable-workflows/.github/workflows/coding-standards-php.yml@main + with: + PHP_VERSION: '8.3' + + static-code-analysis: + if: ${{ (github.event_name != 'workflow_dispatch') || ((github.event.inputs.jobs == 'Run all') || (github.event.inputs.jobs == 'Run Psalm only')) }} + uses: inpsyde/reusable-workflows/.github/workflows/static-analysis-php.yml@main + strategy: + matrix: + php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + with: + PHP_VERSION: ${{ matrix.php }} + PSALM_ARGS: --output-format=github --no-suggestions --no-cache --no-diff \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..3dc1b26 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,63 @@ +name: Unit Tests + +on: + push: + paths: + - '**workflows/unit-tests.yml' + - '**.php' + - '**phpunit.xml.dist' + pull_request: + paths: + - '**workflows/unit-tests.yml' + - '**.php' + - '**phpcs.xml.dist' + workflow_dispatch: + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + unit-tests-php: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-ver: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] + wp-ver: [ '5.9', '6.0', '6.1', '6.2', '6.3', '6.4', '6' ] + exclude: + - php-ver: '8.2' + wp-ver: '5.9' + - php-ver: '8.3' + wp-ver: '5.9' + - php-ver: '8.2' + wp-ver: '6.0' + - php-ver: '8.3' + wp-ver: '6.0' + - php-ver: '8.3' + wp-ver: '6.1' + - php-ver: '8.3' + wp-ver: '6.2' + - php-ver: '8.3' + wp-ver: '6.3' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-ver }} + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + coverage: none + + - name: Adjust dependencies in 'composer.json' + run: | + composer remove roots/wordpress-no-content inpsyde/php-coding-standards vimeo/psalm --dev --no-update + composer require "roots/wordpress-no-content:~${{ matrix.wp-ver }}.0" --dev --no-update + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Run unit tests + run: ./vendor/bin/phpunit --no-coverage diff --git a/.gitignore b/.gitignore index 0f4548e..3f3edc9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /coverage/ composer.lock vendor/ -/phpunit.xml \ No newline at end of file +/phpunit.xml +/.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 729a184..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: php - -php: - - 7.0 - - 7.1 - - 7.2 - - nightly - -sudo: false - -matrix: - allow_failures: - - php: nightly - -before_install: - - composer self-update - -install: - - composer install - -script: - - vendor/bin/phpunit \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bc8d63..d6cee8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,29 @@ # Object Hooks Remover +## Next + +- Modernize codebase to support recent PHP versions and up-to-date Syde coding standards. +- Move logic from `utils.php` file to an internal `Functions` class (all-static, only for encapsulation and autoload). +- Modernize QA: + - Move out from Travis to GitHub actions. + - Rewrite tests, update PHPUnit version, tests now include the real WordPress functions instead of stubs. + - Added static analysis. +- Introduced `remove_all_object_hooks()`. +- Introduced `remove_static_method_hook()` to replace the now deprecated `remove_class_hook()` (which is converted to an alias). +- In `remove_closure_hook()` is now possible to use `"mixed"` as target parameter type when the closure param declare no type. +- License change from MIT to GPL due to usage of WordPress functions. +- README refresh. + +--- + ## v0.1.1 (2017-12-19) ### Fixed - Fix PHP 7 compatibility when removing hook with closures declaring param types. +--- + ## v0.1.0 (2017-12-03) First release. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 995b9b6..d159169 100644 --- a/LICENSE +++ b/LICENSE @@ -1,19 +1,339 @@ -Copyright (c) Syde GmbH - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md index 46e7bbb..5d7dc64 100644 --- a/README.md +++ b/README.md @@ -2,30 +2,16 @@ > Package to remove WordPress hook callbacks that uses object methods or closures. ----- +[![Static Analysis](https://github.com/inpsyde/objects-hooks-remover/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/inpsyde/objects-hooks-remover/actions/workflows/static-analysis.yml) +[![Unit Tests](https://github.com/inpsyde/objects-hooks-remover/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/inpsyde/objects-hooks-remover/actions/workflows/unit-tests.yml) -[![Build Status](https://travis-ci.org/inpsyde/objects-hooks-remover.svg?branch=master)](https://travis-ci.org/inpsyde/objects-hooks-remover) +--- ----- - -## Minimum Requirements and Dependencies - -_Object Hooks Remover_ is a [Composer](https://getcomposer.org) package, installable via the package name `inpsyde/object-hooks-remover`. - -It has no userland dependencies, it just requires **PHP 7+**. - -When installed for development, via Composer, _Object Hooks Remover_ also requires: - -- `phpunit/phpunit` (BSD-3-Clause) - ----- - -## Intro (or "What is this?") +## What is this? WordPress [plugin API](https://developer.wordpress.org/plugins/hooks/) has a partly incomplete implementation. -[`add_action`](https://developer.wordpress.org/reference/functions/add_action/) and [`add_filter`](https://developer.wordpress.org/reference/functions/add_filter/) -accepts as "callback" any kind PHP callable: +[`add_action`](https://developer.wordpress.org/reference/functions/add_action/) and [`add_filter`](https://developer.wordpress.org/reference/functions/add_filter/) accepts as "callback" any kind PHP callable: - named functions - static object methods @@ -33,297 +19,239 @@ accepts as "callback" any kind PHP callable: - anonymous functions - invokable objects -the functions to remove hooks, [`remove_action`](https://developer.wordpress.org/reference/functions/remove_action/) and [`remove_filter`](https://developer.wordpress.org/reference/functions/remove_filter/), -only works with named functions and static object methods (2 of the 5 types of callbacks). +The functions to remove hooks, [`remove_action`](https://developer.wordpress.org/reference/functions/remove_action/) and [`remove_filter`](https://developer.wordpress.org/reference/functions/remove_filter/), works without issues only with named functions and static object methods (2 of the 5 types of callbacks). -Well, ok, this is not completely true. `remove_action` and `remove_filter` can also be used to remove hooks with object -methods or closures when the _exact instance_ used to add the hook is available, but many and many times that's not the case. +For the remaining cases that involve object instances `remove_action` and `remove_filter` can only be used when having access to the original object instance that was used to add hooks, but many and many times that's not available. -This package provides 5 functions that can be used to remove hooks which uses object methods or closures even without having -access to the instances of the objects used. +This package provides six functions that can be used to remove hooks which uses object methods or closures even without having The package functions are: -- `Inpsyde\remove_object_hook` -- `Inpsyde\remove_closure_hook` -- `Inpsyde\remove_class_hook` -- `Inpsyde\remove_instance_hook` -- `Inpsyde\remove_invokable_hook` +- `Inpsyde\remove_object_hook()` +- `Inpsyde\remove_closure_hook()` +- `Inpsyde\remove_static_method_hook()` +- `Inpsyde\remove_instance_hook()` +- `Inpsyde\remove_invokable_hook()` +- `Inpsyde\remove_all_object_hooks()` -You might notice that there's no difference between action and filters because, expecially in removing, there's absolutely -no difference between the two. In fact, this is the code that WordPress core for `remove_action`: +You might notice that there's no difference between action and filters because, especially in removing, there's absolutely no difference between the two. -```php -function remove_action( $tag, $function_to_remove, $priority = 10 ) { - return remove_filter( $tag, $function_to_remove, $priority ); -} -``` +The return value of all the functions is the number of callbacks removed. ----- +--- - -## `Inpsyde\remove_object_hook` - -The signature of this function is the following: +## `Inpsyde\remove_object_hook()` ```php function remove_object_hook( string $hook, - string $class_name, - string $method_name = null, - int $priority = null, - bool $remove_static_callbacks = false + class-string $targetClassName, + ?string $methodName = null, + ?int $targetPriority = null, + bool $removeStaticCallbacks = false ): int ``` -This function is used to remove hook callbacks that use object methods. By default only targets dynamic methods, but can -be used for static methods as well. - -The first mandatory param is the "tag" we want to remove the callback from. - -The second mandatory param is the object class name. - -The third optional param is the method name. If not provided (or `null`), it means "all the methods". +This function is used to remove hook callbacks that use object methods. By default, only targets dynamic methods, but can be used for static methods as well passing `true` to the last parameter. -The fourth optional param is priority. Unlike WordPress `remove_action`/`remove_filter` not providing a priority means -"all the priorities". - -The fifth optional param is a boolean that defaults to false. When true the function will remove both static and dynamic -methods. - -The return value of this and all the other function of the package is the number of callbacks removed. - -**Example**: +### Usage Example ```php // Somewhere... -class Foo { - - public function __construct() { - add_action( 'init', [ $this, 'init' ], 99 ); +class Foo +{ + public function __construct() + { + add_action('init', [$this, 'init'], 99); + add_action('template_redirect', [__CLASS__, 'templateRedirect']); + } + + public function init(): void + { } - public function init() { - // some code here... + public static function templateRedirect(): void + { } } new Foo(); -// Somewhere **else**... -remove_object_hook( 'init', Foo::class, 'init' ); +// Somewhere else... +Inpsyde\remove_object_hook('init', Foo::class, 'init'); +Inpsyde\remove_object_hook('init', Foo::class, 'init', removeStaticCallbacks: true); ``` ----- - -## `Inpsyde\remove_closure_hook` +## `Inpsyde\remove_closure_hook()` This function targets hook callbacks added using anonymous functions (aka closures). Closures are the most tricky callbacks to remove, because it is hard to distinguish them. -In facts, in PHP, all closures are instances of the same class, `Closure`, and not having a method name there's very -little left to distinguish one closure from another. +In facts, in PHP, all closures are instances of the same class, `Closure`, and not having a method name there's very little left to distinguish one closure from another. This function uses two ways to distinguish closures: -- the object the closure is bound to, -- the closure signature. - -### About closures bound object - -Closures can be bound to an object, that means that inside the closure block `$this` refers to the bound object. - -When a closure is created inside a class dynamic method, the object binding is automatic: inside the closure, `$this` -refers to the instance where the closure is created. - -When a closure is created inside a static method or outside of any class, inside the closure, `$this` is not defined at -all (i.e. the closure is not bound). - -It worth nothing that: - -- Closure can be "bound" to any object after they are created (see docs for [`Closure::bind`](http://php.net/manual/en/closure.bind.php) and [`Closure::bindTo`](http://php.net/manual/en/closure.bindto.php)). - So even closures created outside any class or inside class static methods might have a bound object. -- [Closures can be created as "static"](http://php.net/manual/en/functions.anonymous.php#functions.anonymous-functions.static). - Static closures are never bound, so don't have access to any `$this`, and can't be bound to any object after creation. - Any attempt to bind a static closure will fail and result in a warning. - - -### Function Signature +- the object the closure is bound to +- the closure parameters' name and type `Inpsyde\remove_closure_hook` signature is: ```php function remove_closure_hook( string $hook, - $target_this = null, - array $target_args = null, - int $priority = null + ?object $targetThis = null, + ?array $targetArgs = null, + ?int $targetPriority = null ): int ``` -The **second optional param**, `$target_this`, can be used to identify the `$this` of the closure that need to be removed. +The **second optional param**, `$targetThis`, can be used to identify the `$this` of the closure to remove. It can be: -- `null`, which means "all of them", i.e. the function will not take into account the object bound to closure to see - if the closure should be removed or not. -- `false`, the function will only remove static closures or closure with no bound object. -- a string containing a class name, the function will only remove closures having a bound object of the given class. -- an object instance, the function will only remove closures bound to given object. +- `null`, which means "all of them", i.e. the function will not take into account the object bound to closure to see if the closure should be removed or not +- `false`, the function will only remove static closures or closure with no bound object +- a string containing a class name, the function will only remove closures having a bound object of the given class +- an object instance, the function will only remove closures bound to given object -The **third optional param**, `$target_args` is an array that can be used to distinguish closures by their parameters. +The **third optional param**, `$targetArgs` is an array that can be used to distinguish closures by their parameters. For example, a closure like this: ```php -$closure = function (string $foo, int $bar, $baz ) { /*... */ }; +$closure = function (string $foo, int $bar, $baz) { /*... */ }; ``` can be targeted just by parameter _names_, passing an array like: ```php -[ '$foo', '$bar', '$baz' ] +['$foo', '$bar', '$baz'] ``` or by parameter _names_ and _types_, passing an array like: ```php -[ '$foo' => 'string', '$bar' => 'int', '$baz' => null ] +['$foo' => 'string', '$bar' => 'int', '$baz' => null] ``` The two styles can't be mixed, if the type declaration is used for one param must be used for all of them. -In case any of the parameters has no type declaration, `null` has to be used as shown above. -When the param type is an object, the fully qualified name must be used. +In case any of the parameters has no type declaration, `null` or `"mixed"` must be used. It is also possible to pass `null` as third argument (or don't pass anything, which is the same because the param defaults to `null`), and in that case closures to be removed will be only distinguished by the bound `$this`. -In the case both second and third arguments are `null`, which is the default, all closures added to given hook are removed -(only optionally filtered by priority). +In the case both the second and the third arguments are `null`, which is the default, all closures added to given hook are removed (only optionally filtered by priority). -By the means of bound `$this`, signature, and priority, it is possible to *very effectively* distinguish closures to remove. In facts, the only possibility that two closures can't be distinguished one from the other is that they both are added to the same hook, at the same priority, from the same class and they have the same signature... - -### Usage example +### Usage Example ```php // Somewhere in a plugin... -class Foo { - +class Foo +{ public function __construct() { - add_filter( 'the_title', function( $title ) { /* ... */ } ); - add_filter( 'the_content', function( string $content ) { /* ... */ } ); + add_filter('the_title', function($title) { /* ... */ }); + add_filter('the_content', function(string $content) { /* ... */ }); } - } new Foo(); - -// Somewhere *else*... -remove_closure_hook( 'the_title', Foo::class, [ '$title' ] ); -remove_closure_hook( 'the_content', Foo::class, [ '$content' => 'string' ], 10 ); +// Somewhere else... +Inpsyde\remove_closure_hook('the_title', Foo::class, ['$title']); +Inpsyde\remove_closure_hook('the_content', Foo::class, ['$content' => 'string'], 10); ``` ----- +## `Inpsyde\remove_static_method_hook()` -## `Inpsyde\remove_class_hook` - -Similar to `remove_object_hook` this function targets *only* static methods. +Similar to `remove_object_hook()` this function targets *only* static methods. The signature is: ```php -function remove_class_hook( +function remove_static_method_hook( string $hook, - string $class_name, - string $method_name = null, - int $priority = null + class-string $targetClassName, + ?string $targetMethodName = null, + ?int $targetPriority = null ): int ``` -Example: +### Usage Example ```php // Somewhere... class Foo { - public static function instance() { - add_action( 'init', [ __CLASS__, 'init' ], 99 ); + public static function instance() + { + add_action('init', [__CLASS__, 'init'], 99); } - public static function init() { - // some code here... + public static function init() + { } } Foo::instance(); - -// Somewhere **else**... -remove_class_hook( 'init', Foo::class, 'init' ); +// Somewhere else... +Inpsyde\remove_static_method_hook('init', Foo::class, 'init'); ``` -Even if static class methods could be removed via `remove_action` / `remove_filter`, this function can be still -useful because can remove callbacks from any priority and even without specifying a method name. For example: +Even if static class methods could be removed via `remove_action` / `remove_filter`, this function can be still useful because can remove callbacks from any priority and even without specifying a method name. + +For example, we can use the following to remove _all_ the static methods of the `Foo::class` attached to the `init` hook: ```php -remove_class_hook( 'init', Foo::class ); +remove_static_method_hook('init', Foo::class); ``` -can be used to remove all the static methods of `Foo` class that are added to `init` hook. - ----- - -## `Inpsyde\remove_instance_hook` +## `Inpsyde\remove_instance_hook()` This function can be used to remove hook callbacks added with a specific object instance. -When having access to the exact instance used to add some hooks, it would be possible to remove those hooks via core -functions `remove_action` / `remove_filter`, but this function can still be useful because in a single call can remove all -the hooks that use the instance, no matter the method or the priority used. +When having access to the exact instance used to add some hooks, it would be possible to remove those hooks via core functions `remove_action` / `remove_filter`, but this function can still be useful because in a single call can remove all the hooks that use the instance, no matter the method or the priority used. `remove_instance_hook` signature is: ```php remove_instance_hook( string $hook, - $object_instance, - int $priority = null -) : int; + object $targetObject, + ?int $targetPriority = null +): int; ``` -**Example**: +### Usage Example ```php // Somewhere... -class Foo { - - public function __construct() { - add_filter( 'the_title', [ $this, 'the_title_early', 1 ] ); - add_filter( 'the_title', [ $this, 'the_title_late', 9999 ] ); - add_filter( 'the_content', [ $this, 'the_content' ] ); +class Foo +{ + public function __construct() + { + add_filter('the_title', [$this, 'the_title_early', 1]); + add_filter('the_title', [$this, 'the_title_late', 9999]); + add_filter('the_content', [$this, 'the_content']); } - } global $foo; $foo = new Foo(); -// Somewhere **else**... +// Somewhere else... global $foo; -remove_instance_hook( 'the_title', $foo ); // remove 2 callbacks -remove_instance_hook( 'the_content', $foo ); +Inpsyde\remove_instance_hook('the_title', $foo); // remove 2 callbacks +Inpsyde\remove_instance_hook('the_content', $foo); ``` ----- - -## `Inpsyde\remove_invokable_hook` +## `Inpsyde\remove_invokable_hook()` This function targets hooks that were added with [invokable objects](http://php.net/manual/en/language.oop5.magic.php#object.invoke). @@ -332,40 +260,100 @@ The signature: ```php function remove_invokable_hook( string $hook, - string $class_name, - int $priority = null + class-string $targetClassName, + ?int $targetPriority = null ) : int; ``` -**Example**: +### Usage Example ```php // Somewhere... -class Foo { - - public function __construct() { - add_filter( 'template_redirect', $this ); +class Foo +{ + public function __construct() + { + add_filter('template_redirect', $this); } - public function __invoke() { - /* some code here */ + public function __invoke() + { } - } new Foo(); -// Somewhere **else**... -remove_invokable_hook( 'template_redirect', Foo::class ); +// Somewhere else... +Inpsyde\remove_invokable_hook('template_redirect', Foo::class); +``` + + +## `Inpsyde\remove_all_object_hooks()` + +```php +function remove_all_object_hooks( + class-string|object $targetObject, + ?bool $removeStaticCallbacks = null +): int +``` + +This function is used to remove all hook callbacks that use the given object or class name. + +When passing an object instance, it removes all the hook callbacks using that exact instance. + +When passing a class name, it removes all the hook callbacks using that class (regardless the instance). + +Static methods are removed when: +- an object instance is passed and `$removeStaticCallbacks` param _is_ `true` +- a class name is passed and `$removeStaticCallbacks` param _is not_ `false` + +### Usage Example + +```php +// Somewhere... +class Foo +{ + public function __construct() + { + add_action('init', [$this, 'init'], 99); + add_action('template_redirect', [__CLASS__, 'templateRedirect']); + } + + public function init(): void + { + } + + public static function templateRedirect(): void + { + } +} + +global $foo; +$foo = new Foo(); + +// Somewhere else... +global $foo; +Inpsyde\remove_all_object_hooks($foo); // remove "init" hook +Inpsyde\remove_all_object_hooks(Foo::class); // would remove both hooks, but only one left +Inpsyde\remove_all_object_hooks($foo, true); // would remove both hooks, but none left +Inpsyde\remove_all_object_hooks(Foo::class, false); // would remove the "init" hook, but none left ``` -Note that this function is no more than a shortcut for using `Inpsyde\remove_object_hook` passing `__invoke` as method -name param. +--- + +## Minimum Requirements + +_Object Hooks Remover_ is a [Composer](https://getcomposer.org) package, installable via the package name `inpsyde/object-hooks-remover`. + +It has no dependencies, requires **PHP 7.4+**. + +It is tested and guaranteed to with WP 5.9+, but _should_ work, at least, with WP 5.3+ (which is the first version officially supporting PHP 7.4). + ----- +--- -## License and Copyright +## License -This repository is a free software, and is released under the terms of the MIT license. See [LICENSE](./LICENSE) for complete license. +This repository is a free software, and is released under the terms of the GNU General Public License version 2 or (at your option) any later version. See [LICENSE](./LICENSE) for complete license. diff --git a/composer.json b/composer.json index 67e828c..5be50f5 100644 --- a/composer.json +++ b/composer.json @@ -2,44 +2,64 @@ "name": "inpsyde/object-hooks-remover", "description": "Package to remove WordPress hook callbacks that uses object methods or closures.", "type": "library", - "license": "MIT", + "license": "GPL-2.0-or-later", "authors": [ { - "name": "Inpsyde GmbH", - "email": "hello@inpsyde.com", - "homepage": "http://inpsyde.com", + "name": "Syde GmbH", + "email": "hello@syde.com", + "homepage": "https://syde.com", "role": "Company" }, { "name": "Giuseppe Mazzapica", - "email": "g.mazzapica@inpsyde.com", + "email": "g.mazzapica@syde.com", "role": "Developer" } ], - "minimum-stability": "stable", + "minimum-stability": "dev", "require": { - "php": ">=7" + "php": ">=7.4 < 8.4" }, "require-dev": { - "phpunit/phpunit": "6.3.*" + "roots/wordpress-no-content": ">=6.5.3", + "phpunit/phpunit": "^9.6.19", + "inpsyde/php-coding-standards": "^2", + "vimeo/psalm": "^5.24.0" }, "autoload": { + "psr-4": { + "Inpsyde\\ObjectHooksRemover\\": "src/" + }, "files": [ - "inc/utils.php", "inc/object-hooks-remover.php" ] }, "autoload-dev": { "psr-4": { - "Inpsyde\\Tests\\": "tests/src/" + "Inpsyde\\ObjectHooksRemover\\Tests\\": ["tests/src/", "tests/cases/"] } }, "config": { - "optimize-autoloader": true + "optimize-autoloader": true, + "allow-plugins": { + "composer/*": true, + "inpsyde/*": true, + "dealerdirect/phpcodesniffer-composer-installer": true + } }, "extra": { "branch-alias": { - "dev-master": "0.1.x-dev" + "dev-master": "1.x-dev" } + }, + "scripts": { + "cs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", + "psalm": "@php ./vendor/vimeo/psalm/psalm --no-suggestions --report-show-info=false --find-unused-psalm-suppress --no-diff --no-cache --no-file-cache --output-format=compact", + "tests": "@php ./vendor/phpunit/phpunit/phpunit --no-coverage", + "qa": [ + "@cs", + "@psalm", + "@tests" + ] } -} \ No newline at end of file +} diff --git a/inc/object-hooks-remover.php b/inc/object-hooks-remover.php index 1e6f7bd..25fa64c 100644 --- a/inc/object-hooks-remover.php +++ b/inc/object-hooks-remover.php @@ -1,193 +1,172 @@ -rewind(); - - if ( ! $callbacks->valid() ) { - return 0; - } - - $removed = 0; - - foreach ( $callbacks as list( $idx, $priority, $object, $class, $method ) ) { - - $object_match = $remove_static_callbacks || is_object( $object ); - - if ( - ( $object_match && ObjectHooksRemover\match_object_class( $class, $class_name ) ) - && ( ! $method_name || $method === $method_name ) - ) { - remove_filter( $hook, $idx, $priority ); - $removed ++; - } - } - - return $removed; + return ObjectHooksRemover\Functions::removeObjectHook( + $hook, + $targetClassName, + $targetMethodName, + $targetPriority, + $removeStaticCallbacks + ); } /** - * Similar to `remove_object_hook()` remove **only** static methods callbacks. - * - * @param string $hook Action or filter name to remove. - * @param string $class_name Class or interface name of the hook callbacks to remove. - * @param string|null $method_name Method name of the hook callbacks to remove. - * @param int $priority Priority to target, null to target all of them. + * Similar to `remove_object_hook()` remove only static methods callbacks. * + * @param string $hook Action or filter name to remove. + * @param string $targetClassName Class or interface name of the hook callbacks to remove. + * @param string|null $targetMethodName Method name of the hook callbacks to remove. + * @param int|null $targetPriority Priority to target, null to target all of them. * @return int Number of removed hooks. */ -function remove_class_hook( string $hook, string $class_name, string $method_name = null, int $priority = null ): int { - - $callbacks = ObjectHooksRemover\object_callbacks_for_hook( $hook, $priority ); - $callbacks->rewind(); - - if ( ! $callbacks->valid() ) { - return 0; - } - - $removed = 0; - - foreach ( $callbacks as list( $idx, $priority, $object, $class, $method ) ) { - - if ( - is_null( $object ) - && ObjectHooksRemover\match_object_class( $class, $class_name ) - && ( ! $method_name || $method === $method_name ) - ) { - remove_filter( $hook, $idx, $priority ); - $removed ++; - } - } +function remove_static_method_hook( + string $hook, + string $targetClassName, + ?string $targetMethodName = null, + ?int $targetPriority = null +): int { - return $removed; + return ObjectHooksRemover\Functions::removeStaticMethodHook( + $hook, + $targetClassName, + $targetMethodName, + $targetPriority + ); } /** - * Remove callbacks added to given hook using invokable objects of given class name or extending/implementing given - * class/interface name. - * If no priority is given, hooks added to any priority are removed. + * Remove callbacks added to given hook using invokable objects of given class name or + * extending/implementing given class/interface name. * - * @param string $hook Action or filter name to remove. - * @param string $class_name Class name of the invokable object to remove callbacks for. - * @param int|null $priority Priority to target, null to target all of them. + * If no priority is given, hooks added to any priority are removed. * + * @param string $hook Action or filter name to remove. + * @param string $targetClassName Class name of the invokable object to remove callbacks for. + * @param int|null $targetPriority Priority to target, null to target all of them. * @return int Number of removed hooks. */ -function remove_invokable_hook( string $hook, string $class_name, int $priority = null ): int { +function remove_invokable_hook( + string $hook, + string $targetClassName, + ?int $targetPriority = null +): int { - return remove_object_hook( $hook, $class_name, '__invoke', $priority, FALSE ); + return remove_object_hook($hook, $targetClassName, '__invoke', $targetPriority); } /** * Remove callbacks added to given hook using object of given instance (no matter method name). - * If no priority is given, hooks added to any priority are removed. * - * @param string $hook Action or filter name to remove. - * @param object $object_instance Object instance of the hook callbacks to remove. - * @param int|null $priority Priority to target, null to target all of them. + * If no priority is given, hooks added to any priority are removed. * + * @param string $hook Action or filter name to remove. + * @param object $targetObject Object instance of the hook callbacks to remove. + * @param int|null $targetPriority Priority to target, null to target all of them. * @return int Number of removed hooks. */ -function remove_instance_hook( string $hook, $object_instance, int $priority = null ): int { - - $callbacks = ObjectHooksRemover\object_callbacks_for_hook( $hook, $priority ); - $callbacks->rewind(); - - if ( ! $callbacks->valid() ) { - return 0; - } - - $removed = 0; - foreach ( $callbacks as list( $idx, $priority, $object ) ) { - if ( $object === $object_instance ) { - remove_filter( $hook, $idx, $priority ); - $removed ++; - } - } - - return $removed; +function remove_instance_hook(string $hook, object $targetObject, ?int $targetPriority = null): int +{ + return ObjectHooksRemover\Functions::removeInstanceHook($hook, $targetObject, $targetPriority); } /** * Remove closure callbacks added to given hook. - * Via the second param `$closure_this` callbacks to remove can be limited by filtering them via the value of their - * `$this` context. Target `$this`context can be given as object (strict matching applied) or as class name. - * If no priority is given, all priorities are taken into consideration. * - * @param string $hook Action or filter name to remove. - * @param string|object|null|false $target_this Used to filter closures based on `$this` context. - * @param array|null $target_args Used to filter closures based on declared arguments. - * @param int|null $priority Priority to target, null to target all of them. + * Via the second param `$targetThis` callbacks to remove can be limited by filtering them via + * the value of their `$this` context. + * Target `$this`context can be given as object (strict matching applied) or as class name. + * If no priority is given, all priorities are taken into consideration. * + * @param string $hook Action or filter name to remove. + * @param string|object|null|false $targetThis Used to filter closures based on `$this` context. + * @param array|null $targetArgs Used to filter closures based on declared arguments. + * @param int|null $targetPriority Priority to target, null to target all of them. * @return int Number of removed hooks. - * - * @see ObjectHooksRemover\match_closure() for how to use `$target_this` and/or `$target_args` to filter closures. */ function remove_closure_hook( - string $hook, - $target_this = null, - array $target_args = null, - int $priority = null + string $hook, + $targetThis = null, + ?array $targetArgs = null, + ?int $targetPriority = null ): int { - $callbacks = ObjectHooksRemover\object_callbacks_for_hook( $hook, $priority ); - $callbacks->rewind(); - - if ( ! $callbacks->valid() ) { - return 0; - } - - if ( $target_this === null && $target_args === null ) { - return remove_invokable_hook( $hook, \Closure::class, $priority ); - } + return ObjectHooksRemover\Functions::removeClosureHook( + $hook, + $targetThis, + $targetArgs, + $targetPriority + ); +} - $removed = 0; - foreach ( $callbacks as list( $idx, $priority, $object, $class ) ) { +/** + * Remove all hook callbacks that use given object or class. + * + * @param class-string|object $object + * @param bool|null $removeStaticCallbacks + * @return int + */ +function remove_all_object_hooks($object, ?bool $removeStaticCallbacks = null): int +{ + return ObjectHooksRemover\Functions::removeAllObjectHooks($object, $removeStaticCallbacks); +} - if ( $class === \Closure::class && ObjectHooksRemover\match_closure( $object, $target_this, $target_args ) ) { - remove_filter( $hook, $idx, $priority ); - $removed ++; - } - } +/** + * @param string $hook Action or filter name to remove. + * @param string $targetClassName Class or interface name of the hook callbacks to remove. + * @param string|null $targetMethodName Method name of the hook callbacks to remove. + * @param int|null $targetPriority Priority to target, null to target all of them. + * @return int Number of removed hooks. + * + * @deprecated Use remove_static_method_hook() instead + * @codeCoverageIgnore + */ +function remove_class_hook( + string $hook, + string $targetClassName, + ?string $targetMethodName = null, + ?int $targetPriority = null +): int { - return $removed; -} \ No newline at end of file + return remove_static_method_hook( + $hook, + $targetClassName, + $targetMethodName, + $targetPriority + ); +} diff --git a/inc/utils.php b/inc/utils.php deleted file mode 100644 index 0526045..0000000 --- a/inc/utils.php +++ /dev/null @@ -1,320 +0,0 @@ - 'bool', - 'boolean' => 'bool', - 'int' => 'int', - 'integer' => 'int', - 'float' => 'float', - 'double' => 'float', - 'string' => 'string', - 'array' => 'array', -]; - -/** - * Return information about callbacks added to given hook which have an object as callback. - * If no priority is given, all priorities are taken into consideration. - * Return an iterator where each element is a SplFixedArray with a size of 5. - * - * @see parse_callback_data() for info on the 5 values. - * - * @param string $hook - * @param int|null $priority - * - * @return \Iterator - */ -function object_callbacks_for_hook( string $hook, int $priority = null ): \Iterator { - - global $wp_filter; - $callbacks_group = $wp_filter[ $hook ] ?? []; - $all_callbacks = []; - - // This is not for old WP compat, but in case this is called *very* early, before WP_Hook is even loaded. - if ( $callbacks_group instanceof \WP_Hook ) { - $all_callbacks = $callbacks_group->callbacks; - } elseif ( is_array( $callbacks_group ) ) { - $all_callbacks = $callbacks_group; - } - - if ( ! $all_callbacks || ! is_array( $all_callbacks ) ) { - return new \ArrayIterator(); - } - - $target_callbacks = is_int( $priority ) ? [ $priority => ( $all_callbacks[ $priority ] ?? [] ) ] : $all_callbacks; - if ( ! $target_callbacks || ! is_array( $target_callbacks ) ) { - return new \ArrayIterator(); - } - - $all = new \AppendIterator(); - foreach ( $target_callbacks as $callbacks_priority => $callbacks ) { - $by_priority = array_filter( - array_map( - function ( $callback ) use ( $callbacks_priority ) { - return parse_callback_data( $callback, $callbacks_priority ); - }, - $callbacks - ), - 'count' - ); - $by_priority and $all->append( new \ArrayIterator( $by_priority ) ); - } - - return $all; -} - -/** - * Given hook callback data in the format used by WordPress hooks, and the related callback id return an SplFixedArray - * with normalized callback data. - * - * If the callback do not contain an object (it is a plain named function) the returned SplFixedArray is empty, - * otherwise it will have following indexes / values: - * [0] => (string) Callback id (will contain spl object hash for non-static callbacks) - * [1] => (int) Priority - * [2] => (object|null) Callback object or null in case of static callbacks - * [3] => (string) Callback object class name - * [4] => (string) Callback object method, always "__invoke" for both closures or invokable objects. - * - * @param array $callback - * @param int $priority - * - * @return \SplFixedArray - */ -function parse_callback_data( $callback, int $priority ): \SplFixedArray { - - if ( ! is_array( $callback ) ) { - return new \SplFixedArray(); - } - - $function = $callback[ 'function' ] ?? null; - $is_invokable = $function && is_object( $function ); - - /* - * WordPress does not check that callbacks added to hooks are actually callbacks. - * We skip invalid callbacks and named function callbacks. - */ - if ( ! is_callable( $function, FALSE ) || ! ( is_array( $function ) || $is_invokable ) ) { - return new \SplFixedArray(); - } - - /* - * `$tag` and `$priority` params are required by `_wp_filter_build_unique_id` but only used for callbacks containing - * objects when `spl_object_hash` function doesn't exist, which is only possible with PHP <= 5.2 is some edge cases. - * Because we only support PHP 7+ we can happily ignore them. - */ - $callback_id = _wp_filter_build_unique_id( '', $function, 0 ); - - // Static method. - if ( ! $is_invokable && is_string( $function[ 0 ] ) ) { - return \SplFixedArray::fromArray( - [ - $callback_id, - $priority, - null, - $function[ 0 ], - $function[ 1 ] - ] - ); - } - - // Dynamic method or closure/invokable object. - return \SplFixedArray::fromArray( - [ - $callback_id, - $priority, - $is_invokable ? $function : $function[ 0 ], - $is_invokable ? get_class( $function ) : get_class( $function[ 0 ] ), - $is_invokable ? '__invoke' : $function[ 1 ] - ] - ); -} - -/** - * Check if given object match given class name. - * Check can be either exact or done against inheritance and implementation tree. - * Use "@anonymous" to check if an object is an instance of an anonymous class. - * - * @param object|string $object - * @param string $class - * @param bool $exact - * - * @return bool - */ -function match_object_class( $object, string $class, bool $exact = FALSE ): bool { - - if ( ! is_object( $object ) && ! is_string( $object ) ) { - return FALSE; - } - - $object_class = is_string( $object ) ? $object : get_class( $object ); - - if ( $class === '@anonymous' && strpos( $object_class, 'class@anonymous' ) === 0 ) { - return TRUE; - } - - if ( $exact ) { - return $class === $object_class; - } - - return is_a( $object_class, $class, TRUE ); -} - -/** - * Matches a closure against a given `$this` context and given set of declared arguments. - * - * When both `$target_this` and `$target_args` are provided (!== null), both need to match for the function return - * true. - * - * @param \Closure $closure Closure to match. - * @param string|object|boolean|null $target_this Closure $this object or class/instance name. - * It can take the shape of: - * - `null`, default value, check for closure `$this` is skipped. - * - `false`, will only match static closures. - * - Object instance, will match only if closure `$this` matches - * the given instance. - * - String, valid class/instance name that needs to match closure - * `$this` object class/instance name. - * @param array|null $target_args Closure arguments names. - * It can take the shape of: - * - `null` (default value), check for closure params is skipped. - * - Numeric array of closure param **names**. For example: - * `[ '$foo', '$bar', '$baz' ]`. - * - Associative array where array keys are closure param **names** - * and array values are closure param **types**. For example: - * `['$foo' => 'int', '$bar' => '\Foo\ClassName', '$baz' => null]`. - * In this form, `null` as array item value is used to match params - * without type declaration. - * @return bool True when there's a match. - */ -function match_closure( \Closure $closure, $target_this = null, array $target_args = null ): bool { - - if ( $target_this === null && $target_args === null ) { - return TRUE; - } - - // Ensure that `$target_this` is either `null`, `false`, an object, or a valid class/instance name. - $is_object_target = $target_this && is_object( $target_this ); - if ( - ! $is_object_target - && ! is_null( $target_this ) - && $target_this !== FALSE - && ! ( is_string( $target_this ) && ( class_exists( $target_this ) || interface_exists( $target_this ) ) ) - ) { - return FALSE; - } - - $reflection = new \ReflectionFunction( $closure ); - $closure_this = $reflection->getClosureThis(); - $matched = TRUE; - - /* - * If `$target_this` was provided as false, only matches static closures. - */ - if ( $target_this === FALSE ) { - return $closure_this === null; - } - - /* - * If `$target_this` was provided, it must either match closure `$this` instance or pass is_a check against it, - * depending if, in order, it was provided as an object or a string. - */ - if ( $target_this ) { - $matched = - $is_object_target && $closure_this === $target_this - || ! $is_object_target && match_object_class( $closure_this, $target_this ); - } - - // If `$this` check did not passed, then return false; if it matched and no `$target_args`, then return true. - if ( ! $matched || $target_args === null ) { - return $matched; - } - - // If number of arguments did not matches return false. - $closure_args_num = $reflection->getNumberOfParameters(); - if ( $closure_args_num !== count( $target_args ) ) { - return FALSE; - } - - // If number of arguments matched, and it was 0, then just return true: nothing to check. - if ( ! $closure_args_num ) { - return TRUE; - } - - // Let's check argument one by one... - - $args = $reflection->getParameters(); - - // When `$target_args` is an array of strings without keys, we are going to ignore args type and just look at names. - $ignore_type = - ! array_filter( array_keys( $target_args ), 'is_string' ) - && array_filter( $target_args, 'is_string' ) === $target_args; - - $ignore_type and $target_args = array_fill_keys( $target_args, null ); - - $index = 0; - foreach ( $target_args as $name => $type ) { - - // Sanity check on `$name` requirement. - if ( ! is_string( $name ) || ( $name[ 0 ] ?? '' ) !== '$' ) { - return FALSE; - } - - /** @var \ReflectionParameter $param */ - $param = $args[ $index ]; - $index ++; - - // If param name does not match, return false; - if ( '$' . $param->name !== $name ) { - return FALSE; - } - - // If param type is ignored and name already matched, check passed for this param. - if ( $ignore_type ) { - continue; - } - - // Sanity check on `$type` requirement. - is_null( $type ) and $type = 'null'; - if ( ! $type || ! is_string( $type ) ) { - return FALSE; - } - - $param_type = $param->getType(); - - // If declaration parameter has no type, given argument must be "null". - if ( ! $param_type && strtolower( $type ) !== 'null' ) { - return FALSE; - } - - // If declaration parameter has no type and given argument is "null", check passed for this param. - if ( ! $param_type ) { - continue; - } - - $param_type_name = (string) $param_type; - - // Match the type or return false. - $match = in_array( $param_type_name, SCALARS, TRUE ) - ? $param_type_name === ( SCALARS[ strtolower( $type ) ] ?? null ) - : $param_type_name === ltrim( $type, '\\' ); - - if ( ! $match ) { - return FALSE; - } - } - - return TRUE; -} \ No newline at end of file diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..8d20811 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,34 @@ + + + ./inc/ + ./src/ + ./tests/ + + + + + + + + + + + + + + ./tests/ + + + ./tests/ + + + ./tests/ + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 642e4b1..28b2358 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,31 +1,25 @@ - - - tests/src - - - - - inc - - tests - vendor - - - - - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" + bootstrap="tests/bootstrap.php" + colors="true"> + + + + inc + src + + + tests + vendor + + + + + + tests + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..1307485 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/src/Functions.php b/src/Functions.php new file mode 100644 index 0000000..741a15c --- /dev/null +++ b/src/Functions.php @@ -0,0 +1,589 @@ + + * + * phpcs:disable Generic.Metrics.CyclomaticComplexity + */ + private static function objectCallbacksForHook(string $hook, ?int $targetPriority = null): array + { + // phpcs:enable Generic.Metrics.CyclomaticComplexity + if ($hook === '') { + return []; + } + + global $wp_filter; + $callbacksGroup = is_array($wp_filter) ? ($wp_filter[$hook] ?? []) : []; + $allCallbacks = []; + + // This is not for old WP compat, but in case this is called *very* early, before WP_Hook is + // even loaded. + if ($callbacksGroup instanceof \WP_Hook) { + $allCallbacks = $callbacksGroup->callbacks; + } elseif (is_array($callbacksGroup)) { + $allCallbacks = $callbacksGroup; + } + + /** @psalm-suppress DocblockTypeContradiction */ + if (($allCallbacks === []) || !is_array($allCallbacks)) { + return []; + } + $targetCallbacks = ($targetPriority === null) + ? $allCallbacks + : [$targetPriority => ($allCallbacks[$targetPriority] ?? [])]; + + /** @psalm-suppress TypeDoesNotContainType */ + if (($targetCallbacks === []) || !is_array($targetCallbacks)) { + return []; + } + + $all = []; + foreach ($targetCallbacks as $priority => $callbacks) { + if (is_array($callbacks)) { + $all = static::collectObjectCallbacksByPriority($callbacks, (int) $priority, $all); + } + } + + return $all; + } + + /** + * @param array $callbacks + * @param int $priority + * @param list $all + * @return list + */ + private static function collectObjectCallbacksByPriority( + array $callbacks, + int $priority, + array $all = [] + ): array { + + foreach ($callbacks as $callback) { + [$id, $targetThis, $class, $method] = static::parseCallbackData($callback); + if (($class !== '') && ($id !== '') && ($method !== '')) { + $all[] = [$id, $priority, $targetThis, $class, $method]; + } + } + + return $all; + } + + /** + * @param mixed $callbackData + * @return list{string, object|null, class-string|"", string} + */ + private static function parseCallbackData($callbackData): array + { + if (!is_array($callbackData)) { + return ['', null, '', '']; + } + + $callback = $callbackData['function'] ?? null; + + // 2nd param `true` means syntax-only check + if (!is_callable($callback, true)) { + return ['', null, '', '']; + } + + if (is_string($callback)) { + if (substr_count($callback, '::') !== 1) { + // We are not interested in plain string function names + return ['', null, '', '']; + } + + // Static method passed as string + $callback = explode('::', $callback); + } + + $isInvokable = is_object($callback); + + /** @var list{string|object, string}|object $callback */ + + /** + * `is_callable($cb, true)` returns `true` for empty class or empty method. + * Moreover, we catch here things like "Foo::", "::foo", or "::" passed as callback. + * @psalm-suppress PossiblyInvalidArrayAccess + */ + if (!$isInvokable && (($callback[0] === '') || (($callback[1] === '')))) { + return ['', null, '', '']; + } + + /* + * 1st and 3rd params, `$tag` and `$priority, are required by `_wp_filter_build_unique_id()` + * for historical reasons, when WP supported PHP versions where `spl_object_hash()` could + * be unavailable. In modern WP, those params are there for backward compat, but not used. + */ + $callbackId = _wp_filter_build_unique_id('', $callback, 0); + + $isInvokable and $callback = [$callback, '__invoke']; + + /** @var list{class-string|object, non-empty-string} $callback */ + + return is_object($callback[0]) + ? [$callbackId, $callback[0], get_class($callback[0]), $callback[1]] + : [$callbackId, null, $callback[0], $callback[1]]; + } + + /** + * @param mixed $thing + * @return bool + * + * @psalm-assert-if-true class-string|"object" $thing + */ + private static function isClassLikeString($thing): bool + { + if (($thing === '') || !is_string($thing)) { + return false; + } + + if (($thing === 'object') || class_exists($thing) || interface_exists($thing)) { + return true; + } + + if (PHP_VERSION_ID < 801000) { + return false; + } + + /** + * @psalm-suppress UndefinedFunction + * @var true + * phpcs:disable PHPCompatibility.FunctionUse.NewFunctions.enum_existsFound + */ + return enum_exists($thing); + } + + /** + * @param mixed $targetObject + * @param string $targetClass + * @param bool $exactMatch + * @return bool + */ + private static function matchObjectClass( + $targetObject, + string $targetClass, + bool $exactMatch = false + ): bool { + + $isStringTarget = static::isClassLikeString($targetObject); + + if ( + (!$isStringTarget && !is_object($targetObject)) + || (($targetClass !== '@anonymous') && !static::isClassLikeString($targetClass)) + ) { + return false; + } + + /** + * @var class-string|"object"|"stdClass" $objectClass + * @var class-string|"object"|"@anonymous" $targetClass + */ + + $objectClass = $isStringTarget ? $targetObject : get_class($targetObject); + + if ($targetClass === 'object') { + return !$exactMatch || ($objectClass === 'stdClass'); + } + + if ($targetClass === '@anonymous') { + return strpos($objectClass, 'class@anonymous') === 0; + } + + return $exactMatch + ? ($targetClass === $objectClass) + : is_a($objectClass, $targetClass, true); + } + + /** + * @param \Closure $closure + * @param mixed $targetThis + * @param array|null $targetArgs + * @return bool True when there's a match. + */ + private static function matchClosure( + \Closure $closure, + $targetThis = null, + ?array $targetArgs = null + ): bool { + + if (($targetThis === null) && ($targetArgs === null)) { + return true; + } + + // Ensures `$targetThis` is either `null`, `false`, an object, or a class/interface name + $isObjectTarget = is_object($targetThis); + $isClassTarget = !$isObjectTarget && static::isClassLikeString($targetThis); + if ( + !$isObjectTarget + && !$isClassTarget + && ($targetThis !== null) + && ($targetThis !== false) + ) { + return false; + } + + $reflection = new \ReflectionFunction($closure); + $closureThis = $reflection->getClosureThis(); + + if (($targetThis === false) && ($closureThis !== null)) { + return false; + } elseif ($isObjectTarget && ($closureThis !== $targetThis)) { + // If `$targetThis` was provided as object it must be same instance of closure's $this + return false; + } elseif ($isClassTarget && !static::matchObjectClass($closureThis, $targetThis)) { + // If `$targetThis` was provided as class name in must match closure's $this + return false; + } + + // If `$targetThis` check above passed and there's no `$targetArgs`, it matched all + if ($targetArgs === null) { + return true; + } + + $closureParams = $reflection->getParameters(); + + if (($closureParams === []) || ($targetArgs === [])) { + return $closureParams === $targetArgs; + } + + return static::matchClosureParams($closureParams, $targetArgs); + } + + /** + * @param non-empty-array $params + * @param array $targetArgs + * @return bool + */ + private static function matchClosureParams(array $params, array $targetArgs): bool + { + [$ignoreType, $targetArgs] = static::normalizeTargetArgsList($targetArgs); + + if ($targetArgs === []) { + return false; + } + + if (count($params) !== count($targetArgs)) { + return false; + } + + $index = 0; + foreach ($targetArgs as $targetArgName => $targetArgType) { + $param = $params[$index]; + + if ("\${$param->name}" !== $targetArgName) { + return false; + } + + if (!$ignoreType && !static::matchParamType($param, $targetArgType)) { + return false; + } + + $index++; + } + + return true; + } + + /** + * @param array $array + * @return list{bool, array} + * + * phpcs:disable Inpsyde.CodeQuality.NestingLevel + */ + private static function normalizeTargetArgsList(array $array): array + { + // phpcs:enable Inpsyde.CodeQuality.NestingLevel + $mode = 'names'; + $i = -1; + $normalized = []; + foreach ($array as $key => $value) { + $i++; + $name = $value; + if ($key !== $i) { + if (($i > 0) && ($mode === 'names')) { + return [false, []]; + } + $mode = 'types'; + $name = $key; + } + + if (!is_string($key) && ($mode === 'types')) { + return [false, []]; + } + + if ( + ($name === '') + || ($name === '$') + || !is_string($name) + || (strpos($name, '$') !== 0) + ) { + return [false, []]; + } + + if ($mode === 'types') { + ($value === null) and $value = 'mixed'; + if (($value === '') || !is_string($value)) { + return [false, []]; + } + } + + /** + * @var non-empty-string $name + * @var non-empty-string $value + */ + $normalized[$name] = ($mode === 'names') ? 'mixed' : $value; + } + + return [$mode === 'names', $normalized]; + } + + /** + * @param \ReflectionParameter $param + * @param non-empty-string $targetArgType + * @return bool + */ + private static function matchParamType(\ReflectionParameter $param, string $targetArgType): bool + { + $paramType = $param->getType(); + + if ($paramType === null) { + // If declaration parameter has no type, given argument must be "mixed". + return $targetArgType === 'mixed'; + } + + $targetArgType = ltrim($targetArgType, '\\'); + + if (PHP_MAJOR_VERSION === 7) { + /** @psalm-suppress UndefinedMethod */ + $paramTypeName = $paramType->getName(); + $paramType->allowsNull() and $paramTypeName = "?{$paramTypeName}"; + + return $paramTypeName === $targetArgType; + } + + return (string) $paramType === $targetArgType; + } +} diff --git a/tests/boot.php b/tests/boot.php deleted file mode 100644 index bb4b565..0000000 --- a/tests/boot.php +++ /dev/null @@ -1,18 +0,0 @@ -getParameters(); + + static::assertSame( + $expected, + $this->execPrivateFunction('matchClosureParams', $params, $input) + ); + } + + /** + * @return \Generator + */ + public function provideMatchClosureTypedParams(): \Generator + { + yield from [ + [['$foo', '$bar'], true], + [['$foo'], false], + [['$bar'], false], + [['$foo', '$bar', '$z'], false], + [['$foo', 'y'], false], + [['x', '$bar'], false], + [['string', 'int'], false], + [['string', 'int'], false], + [['$foo' => '?string', '$bar' => 'int'], true], + [['$foo' => 'string', '$bar' => 'int'], false], + [['$foo' => '?string', '$bar' => '?int'], false], + [['$foo' => '?string', '$bar'], false], + ]; + } + + /** + * @test + * @dataProvider provideMatchClosureUntypedParams + */ + public function testMatchClosureUntypedParams(array $input, bool $expected): void + { + // phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration + $func = static function ($foo, $bar): void { + // phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration + }; + $params = (new \ReflectionFunction($func))->getParameters(); + + static::assertSame( + $expected, + $this->execPrivateFunction('matchClosureParams', $params, $input) + ); + } + + /** + * @return \Generator + */ + public function provideMatchClosureUntypedParams(): \Generator + { + yield from [ + [['$foo', '$bar'], true], + [['$foo'], false], + [['$bar'], false], + [['$foo', '$bar', '$z'], false], + [['$foo', 'y'], false], + [['x', '$bar'], false], + [['string', 'int'], false], + [['string', 'int'], false], + [['$foo' => null, '$bar' => null], true], + [['$foo' => 'mixed', '$bar' => null], true], + [['$foo' => 'mixed', '$bar' => 'mixed'], true], + [['$foo' => null, '$bar' => 'mixed'], true], + [['$foo' => 'mixed', '$bar'], false], + [['$foo', '$bar' => 'mixed'], false], + ]; + } + + /** + * @test + * @dataProvider provideMatchClosurePartiallyTypedParams + */ + public function testMatchClosurePartiallyTypedParams(array $input, bool $expected): void + { + // phpcs:disable Inpsyde.CodeQuality.ArgumentTypeDeclaration + $func = static function ($foo, ?int $bar): void { + // phpcs:enable Inpsyde.CodeQuality.ArgumentTypeDeclaration + }; + $params = (new \ReflectionFunction($func))->getParameters(); + + static::assertSame( + $expected, + $this->execPrivateFunction('matchClosureParams', $params, $input) + ); + } + + /** + * @return \Generator + */ + public function provideMatchClosurePartiallyTypedParams(): \Generator + { + yield from [ + [['$foo', '$bar'], true], + [['$foo'], false], + [['$bar'], false], + [['$foo', '$bar', '$z'], false], + [['', '?int'], false], + [['mixed', '?int'], false], + [['null', '?int'], false], + [['$foo' => null, '$bar' => '?int'], true], + [['$foo' => null, '$bar' => 'int'], false], + [['$foo', '$bar' => '?int'], false], + [['$foo' => 'mixed', '$bar' => '?int'], true], + [['$foo' => 'string', '$bar' => '?int'], false], + [['$foo' => '', '$bar' => '?int'], false], + ]; + } +} diff --git a/tests/cases/MatchClosureTest.php b/tests/cases/MatchClosureTest.php new file mode 100644 index 0000000..8ab6b5b --- /dev/null +++ b/tests/cases/MatchClosureTest.php @@ -0,0 +1,137 @@ +execPrivateFunction('matchClosure', $func)); + static::assertTrue($this->execPrivateFunction('matchClosure', $func, null, null)); + } + + /** + * @test + */ + public function testMatchClosureWithInvalidTargetThis(): void + { + $func = function (string $foo): bool { + }; + + static::assertFalse($this->execPrivateFunction('matchClosure', $func, true)); + static::assertFalse($this->execPrivateFunction('matchClosure', $func, '')); + static::assertFalse($this->execPrivateFunction('matchClosure', $func, 'Meh')); + } + + /** + * @test + */ + public function testMatchClosureWithFalseTargetThisMatchesStatic(): void + { + $func1 = function (string $foo): bool { + }; + $func2 = static function (string $foo): bool { + }; + + static::assertFalse($this->execPrivateFunction('matchClosure', $func1, false)); + static::assertTrue($this->execPrivateFunction('matchClosure', $func2, false)); + } + + /** + * @test + */ + public function testMatchClosureWithObjectTargetThisMatchesObject(): void + { + $func = function (string $foo): bool { + }; + + static::assertTrue($this->execPrivateFunction('matchClosure', $func, $this)); + static::assertFalse($this->execPrivateFunction('matchClosure', $func, clone $this)); + } + + /** + * @test + */ + public function testMatchClosureWithClassTargetThisMatchesObject(): void + { + $func = function (string $foo): bool { + }; + + static::assertTrue($this->execPrivateFunction('matchClosure', $func, __CLASS__)); + static::assertTrue($this->execPrivateFunction('matchClosure', $func, parent::class)); + } + + /** + * @test + */ + public function testMatchClosureWithNoTargetArgsMatchesNoParams(): void + { + $func1 = function (): bool { + }; + $func2 = function (string $foo): bool { + }; + + static::assertTrue($this->execPrivateFunction('matchClosure', $func1, null, [])); + static::assertFalse($this->execPrivateFunction('matchClosure', $func2, null, [])); + static::assertFalse($this->execPrivateFunction('matchClosure', $func1, null, ['$foo'])); + static::assertTrue($this->execPrivateFunction('matchClosure', $func2, null, ['$foo'])); + } + + /** + * @test + * @dataProvider provideMatchClosure + * + * @param bool $expected + * @param mixed $targetThis + * @param array $targetArgs + * @return void + */ + public function testMatchClosure(bool $expected, $targetThis, array $targetArgs): void + { + if ($targetThis === '__THIS__') { + $targetThis = $this; + } + + $func = function (?string $foo, int $bar): bool { + }; + + static::assertSame( + $expected, + $this->execPrivateFunction('matchClosure', $func, $targetThis, $targetArgs) + ); + } + + /** + * @return \Generator + */ + public static function provideMatchClosure(): \Generator + { + yield from [ + [true, null, ['$foo', '$bar']], + [true, null, ['$foo' => '?string', '$bar' => 'int']], + [true, '__THIS__', ['$foo', '$bar']], + [true, '__THIS__', ['$foo' => '?string', '$bar' => 'int']], + [true, __CLASS__, ['$foo', '$bar']], + [true, __CLASS__, ['$foo' => '?string', '$bar' => 'int']], + [false, '', ['$foo', '$bar']], + [false, '', ['$foo' => '?string', '$bar' => 'int']], + [false, null, ['$foo', '$bar', '$z']], + [false, null, ['$foo' => 'string', '$bar' => 'int']], + [false, '__THIS__', ['$foo']], + [false, '__THIS__', ['$foo' => '?string', '$bar' => '?int']], + [false, __CLASS__, ['$foo', 'y']], + [false, __CLASS__, ['$foo' => '?string', '$bar']], + ]; + } +} diff --git a/tests/cases/MatchObjectClassTest.php b/tests/cases/MatchObjectClassTest.php new file mode 100644 index 0000000..70031e2 --- /dev/null +++ b/tests/cases/MatchObjectClassTest.php @@ -0,0 +1,95 @@ +execPrivateFunction('matchObjectClass', 'Foo', 'Foo')); + } + + /** + * @test + */ + public function testMatchObjectClassAnonymous(): void + { + $obj = new class () { + }; + + static::assertTrue($this->execPrivateFunction('matchObjectClass', $obj, 'object')); + static::assertTrue($this->execPrivateFunction('matchObjectClass', $obj, '@anonymous')); + static::assertFalse($this->execPrivateFunction('matchObjectClass', $obj, 'object', true)); + } + + /** + * @test + */ + public function testMatchObjectClassStdClass(): void + { + $obj = (object) ['foo']; + + static::assertTrue($this->execPrivateFunction('matchObjectClass', $obj, 'object', false)); + static::assertFalse($this->execPrivateFunction('matchObjectClass', $obj, '@anonymous')); + static::assertTrue($this->execPrivateFunction('matchObjectClass', $obj, 'object', true)); + } + + /** + * @test + * @dataProvider provideMatchObject + * + * @param bool $expected + * @param mixed $targetObject + * @param string $targetClass + * @param bool $exact + * @return void + */ + public function testMatchObject( + bool $expected, + $targetObject, + string $targetClass, + bool $exact + ): void { + + if ($targetObject === '__THIS__') { + $targetObject = $this; + } + + static::assertSame( + $expected, + $this->execPrivateFunction('matchObjectClass', $targetObject, $targetClass, $exact) + ); + } + + /** + * @return \Generator + */ + public static function provideMatchObject(): \Generator + { + yield from [ + [true, '__THIS__', 'object', false], + [true, '__THIS__', __CLASS__, false], + [true, '__THIS__', parent::class, false], + [true, '__THIS__', Assert::class, false], + [true, __CLASS__, 'object', false], + [true, __CLASS__, __CLASS__, false], + [true, __CLASS__, parent::class, false], + [true, __CLASS__, Assert::class, false], + [false, '__THIS__', 'object', true], + [true, '__THIS__', __CLASS__, true], + [false, '__THIS__', parent::class, true], + [false, '__THIS__', Assert::class, true], + [false, __CLASS__, 'object', true], + [true, __CLASS__, __CLASS__, true], + [false, __CLASS__, parent::class, true], + [false, __CLASS__, Assert::class, true], + ]; + } +} diff --git a/tests/cases/NormalizeTargetArgsListTest.php b/tests/cases/NormalizeTargetArgsListTest.php new file mode 100644 index 0000000..9e439b0 --- /dev/null +++ b/tests/cases/NormalizeTargetArgsListTest.php @@ -0,0 +1,56 @@ +execPrivateFunction('normalizeTargetArgsList', $input); + + static::assertSame($expected, $actual); + } + + /** + * @return \Generator + */ + public static function provideNormalizeTargetArgsList(): \Generator + { + yield from [ + [ + [], + [true, []], + ], + [ + ['$foo', '$bar', '$baz'], + [true, ['$foo' => 'mixed', '$bar' => 'mixed', '$baz' => 'mixed']], + ], + [ + ['$foo' => 'string', '$bar' => 'string|null', '$baz' => 'int'], + [false, ['$foo' => 'string', '$bar' => 'string|null', '$baz' => 'int']], + ], + [ + ['$foo', 'bar', '$baz'], + [false, []], + ], + [ + ['$foo' => 'string', 'string|null', '$baz' => 'int'], + [false, []], + ], + [ + ['$foo', '$bar' => 'null', '$baz'], + [false, []], + ], + [ + ['foo'], + [false, []], + ], + ]; + } +} diff --git a/tests/cases/ObjectCallbacksForHookTest.php b/tests/cases/ObjectCallbacksForHookTest.php new file mode 100644 index 0000000..c171e7d --- /dev/null +++ b/tests/cases/ObjectCallbacksForHookTest.php @@ -0,0 +1,86 @@ +execPrivateFunction('objectCallbacksForHook', '')); + static::assertSame([], $this->execPrivateFunction('objectCallbacksForHook', '', 22)); + } + + /** + * @test + */ + public function testObjectCallbacksForHook(): void + { + $function = static function (): void { + }; + $fnId = _wp_filter_build_unique_id('', $function, 0); + + add_action('foo', __METHOD__, 11); + add_action('foo', 'strtolower', 22); + add_action('bar', $function, 33); + add_action('bar', ['', ''], 44); + add_action('bar', [__CLASS__, ''], 55); + add_action('bar', [__CLASS__, __FUNCTION__], 66); + add_action('bar', '_wp_filter_build_unique_id', 77); + + static::assertSame( + [ + [__METHOD__, 11, null, __CLASS__, __FUNCTION__], + ], + $this->execPrivateFunction('objectCallbacksForHook', 'foo') + ); + + static::assertSame( + [ + [__METHOD__, 11, null, __CLASS__, __FUNCTION__], + ], + $this->execPrivateFunction('objectCallbacksForHook', 'foo', 11) + ); + + static::assertSame( + [], + $this->execPrivateFunction('objectCallbacksForHook', 'foo', 10) + ); + + static::assertSame( + [ + [$fnId, 33, $function, \Closure::class, '__invoke'], + [__METHOD__, 66, null, __CLASS__, __FUNCTION__], + ], + $this->execPrivateFunction('objectCallbacksForHook', 'bar') + ); + + static::assertSame( + [ + [$fnId, 33, $function, \Closure::class, '__invoke'], + ], + $this->execPrivateFunction('objectCallbacksForHook', 'bar', 33) + ); + + static::assertSame( + [ + [__METHOD__, 66, null, __CLASS__, __FUNCTION__], + ], + $this->execPrivateFunction('objectCallbacksForHook', 'bar', 66) + ); + + static::assertSame( + [], + $this->execPrivateFunction('objectCallbacksForHook', 'bar', 10) + ); + } +} diff --git a/tests/cases/ParseCallbackDataTest.php b/tests/cases/ParseCallbackDataTest.php new file mode 100644 index 0000000..88db6be --- /dev/null +++ b/tests/cases/ParseCallbackDataTest.php @@ -0,0 +1,74 @@ +execPrivateFunction('parseCallbackData', $input) + ); + } + + /** + * @return \Generator + */ + public static function provideParseCallbackData(): \Generator + { + $class = __CLASS__; + $method = __METHOD__; + $function = __FUNCTION__; + + $invokable = new class () + { + public function __invoke() + { + } + }; + $invokableClass = get_class($invokable); + $invokableId = _wp_filter_build_unique_id('', $invokable, random_int(1, 10000)); + + $closure = static function (): void { + }; + $closureId = _wp_filter_build_unique_id('', $closure, random_int(1, 10000)); + + $object = new class () + { + public function example(): void + { + } + }; + $objectId = _wp_filter_build_unique_id('', [$object, 'example'], random_int(1, 10000)); + $objectClass = get_class($object); + + yield from [ + ['', ['', null, '', '']], + [[], ['', null, '', '']], + [['foo' => 'function'], ['', null, '', '']], + [['function' => []], ['', null, '', '']], + [['function' => [$class]], ['', null, '', '']], + [['function' => [$class, $function, 'meh']], ['', null, '', '']], + [['function' => 'foo'], ['', null, '', '']], + [['function' => '::'], ['', null, '', '']], + [['function' => '::' . $function], ['', null, '', '']], + [['function' => $class . '::'], ['', null, '', '']], + [['function' => $method], [$method, null, $class, __FUNCTION__]], + [['function' => $invokable], [$invokableId, $invokable, $invokableClass, '__invoke']], + [['function' => $closure], [$closureId, $closure, \Closure::class, '__invoke']], + [['function' => [$class, $function]], [$method, null, $class, $function]], + [['function' => [$object, 'example']], [$objectId, $object, $objectClass, 'example']], + ]; + } +} diff --git a/tests/cases/RemoveAllObjectHooksTest.php b/tests/cases/RemoveAllObjectHooksTest.php new file mode 100644 index 0000000..46b029e --- /dev/null +++ b/tests/cases/RemoveAllObjectHooksTest.php @@ -0,0 +1,162 @@ +addHooks(); + $foo2 = clone $foo1; + $bar = new \Bar(); + + static::assertSame(0, remove_all_object_hooks($foo2)); + static::assertSame(0, remove_all_object_hooks($bar)); + static::assertSame(2, remove_all_object_hooks($foo1)); + } + + /** + * @return void + */ + public function testByClass(): void + { + eval( + <<<'PHP' + class Bar + { + } + class Foo extends Bar + { + public function __construct() + { + add_action('first', [$this, 'first'], 11); + add_action('second', [$this, 'second'], 22); + add_action('first', [__CLASS__, 'third'], 33); + } + } + PHP + ); + + new \Foo(); + + static::assertSame(0, remove_all_object_hooks(\Bar::class)); + static::assertSame(3, remove_all_object_hooks(\Foo::class)); + } + + /** + * @return void + */ + public function testByInstanceStatic(): void + { + eval( + <<<'PHP' + class Bar + { + } + class Foo extends Bar + { + public function addHooks() + { + add_action('first', [$this, 'first'], 11); + add_action('second', [$this, 'second'], 22); + add_action('first', [__CLASS__, 'third'], 33); + } + } + PHP + ); + + $foo1 = new \Foo(); + $foo1->addHooks(); + $foo2 = clone $foo1; + $bar = new \Bar(); + + static::assertSame(0, remove_all_object_hooks($bar, true)); + static::assertSame(1, remove_all_object_hooks($foo2, true)); + } + + /** + * @return void + */ + public function testByInstanceAlsoStatic(): void + { + eval( + <<<'PHP' + class Bar + { + } + class Foo extends Bar + { + public function addHooks() + { + add_action('first', [$this, 'first'], 11); + add_action('second', [$this, 'second'], 22); + add_action('first', [__CLASS__, 'third'], 33); + } + } + PHP + ); + + $foo1 = new \Foo(); + $foo1->addHooks(); + $bar = new \Bar(); + + static::assertSame(0, remove_all_object_hooks($bar, true)); + static::assertSame(3, remove_all_object_hooks($foo1, true)); + } + + /** + * @return void + */ + public function testByClassNotStatic(): void + { + eval( + <<<'PHP' + class Bar + { + } + class Foo extends Bar + { + public function __construct() + { + add_action('first', [$this, 'first'], 11); + add_action('second', [$this, 'second'], 22); + add_action('first', [__CLASS__, 'third'], 33); + } + } + PHP + ); + + new \Foo(); + + static::assertSame(0, remove_all_object_hooks(\Bar::class, false)); + static::assertSame(2, remove_all_object_hooks(\Foo::class, false)); + } +} diff --git a/tests/cases/RemoveClosureHookTest.php b/tests/cases/RemoveClosureHookTest.php new file mode 100644 index 0000000..66699c9 --- /dev/null +++ b/tests/cases/RemoveClosureHookTest.php @@ -0,0 +1,152 @@ + true); + add_filter('bar', function (string $bar) {}); + add_filter('foo', static function (string $baz) {}); + } + } + PHP + ); + + new \Foo(); + + static::assertSame(2, remove_closure_hook('foo')); + static::assertSame(1, remove_closure_hook('bar')); + } + + /** + * @return void + */ + public function testRemoveClosureHookByClass(): void + { + eval( + <<<'PHP' + class Foo + { + public function __construct() { + add_filter('foo', fn ($foo) => true); + add_filter('bar', function (string $bar) {}); + add_filter('foo', static function (string $baz) {}); + } + } + PHP + ); + + new \Foo(); + + static::assertSame(0, remove_closure_hook('foo', __CLASS__)); + static::assertSame(1, remove_closure_hook('foo', \Foo::class)); + static::assertSame(1, remove_closure_hook('foo', false)); + static::assertSame(0, remove_closure_hook('bar', false)); + static::assertSame(1, remove_closure_hook('bar', \Foo::class)); + } + + /** + * @return void + */ + public function testRemoveClosureHookByParamsUntyped(): void + { + eval( + <<<'PHP' + class Foo + { + public function __construct() { + add_filter('foo', fn ($foo) => true); + add_filter('bar', function (string $bar) {}); + add_filter('foo', static function (string $baz) {}); + } + } + PHP + ); + + new \Foo(); + + static::assertSame(0, remove_closure_hook('foo', null, ['$foo' => ''])); + static::assertSame(1, remove_closure_hook('foo', null, ['$foo'])); + static::assertSame(1, remove_closure_hook('foo', null, ['$baz'])); + static::assertSame(1, remove_closure_hook('bar', null, ['$bar'])); + } + + /** + * @return void + */ + public function testRemoveClosureHookByParamsTyped(): void + { + eval( + <<<'PHP' + class Foo + { + public function __construct() { + add_filter('foo', fn ($foo) => true); + add_filter('bar', function (string $bar) {}); + add_filter('foo', static function (string $baz) {}); + } + } + PHP + ); + + new \Foo(); + + static::assertSame(1, remove_closure_hook('foo', null, ['$foo' => null])); + static::assertSame(1, remove_closure_hook('foo', null, ['$baz' => 'string'])); + static::assertSame(1, remove_closure_hook('bar', null, ['$bar' => 'string'])); + } + + /** + * @return void + */ + public function testRemoveClosureHookByClassAndParamsTyped(): void + { + eval( + <<<'PHP' + class Foo + { + public function __construct() { + add_filter('foo', fn ($foo) => true); + add_filter('bar', function (string $bar) {}); + add_filter('foo', static function (string $baz) {}); + } + } + PHP + ); + + $foo = new \Foo(); + + static::assertSame(0, remove_closure_hook('foo', \Foo::class, ['$foo' => ''])); + static::assertSame(0, remove_closure_hook('foo', __CLASS__, ['$foo' => null])); + static::assertSame(0, remove_closure_hook('foo', \Foo::class, ['$foo' => null], 11)); + static::assertSame(1, remove_closure_hook('foo', \Foo::class, ['$foo' => null])); + + static::assertSame(0, remove_closure_hook('foo', false, ['$baz' => 'int'])); + static::assertSame(0, remove_closure_hook('foo', \Foo::class, ['$baz' => 'string'])); + static::assertSame(0, remove_closure_hook('foo', false, ['$baz' => 'string'], 1)); + static::assertSame(1, remove_closure_hook('foo', false, ['$baz' => 'string'], 10)); + + static::assertSame(0, remove_closure_hook('bar', \Foo::class, ['$baz' => 'string'])); + static::assertSame(0, remove_closure_hook('bar', __CLASS__, ['$bar' => 'string'])); + static::assertSame(0, remove_closure_hook('bar', \Foo::class, ['$bar' => 'string'], 20)); + static::assertSame(1, remove_closure_hook('bar', $foo, ['$bar' => 'string'], 10)); + } +} diff --git a/tests/cases/RemoveInstanceHookTest.php b/tests/cases/RemoveInstanceHookTest.php new file mode 100644 index 0000000..f0fa92b --- /dev/null +++ b/tests/cases/RemoveInstanceHookTest.php @@ -0,0 +1,65 @@ + null, - '$bar' => 'array', - '$baz' => \ArrayObject::class, - ]; - - $right_args_2 = [ - '$foo' => 'null', - '$bar' => 'array', - '$baz' => \ArrayObject::class, - ]; - - $wrong_args_1 = [ - '$foo' => null, - '$bar' => 'array', - '$baz' => null, - ]; - - $wrong_args_2 = [ - '$foo' => null, - '$bar' => 'array', - ]; - - $wrong_args_3 = [ - '$foo', - '$bar', - '$baz' => \ArrayObject::class, - ]; - - $wrong_args_4 = [ - 'foo' => '', - 'bar' => 'array', - 'baz' => \ArrayObject::class, - ]; - - $wrong_args_5 = [ - '$foo' => null, - '$bar' => 'array', - 'baz' => \ArrayObject::class, - ]; - - static::assertTrue( match_closure( $cb, null, $right_args_1 ) ); - static::assertTrue( match_closure( $cb, $this, $right_args_2 ) ); - static::assertFalse( match_closure( $cb, null, $wrong_args_1 ) ); - static::assertFalse( match_closure( $cb, null, $wrong_args_2 ) ); - static::assertFalse( match_closure( $cb, null, $wrong_args_3 ) ); - static::assertFalse( match_closure( $cb, null, $wrong_args_4 ) ); - static::assertFalse( match_closure( $cb, null, $wrong_args_5 ) ); - } - - public function test_matching_args_by_type_with_ambiguous_scalars() { - - $cb = function ( int $foo, float $bar, bool $baz ) { - - }; - - $right_args_1 = [ - '$foo' => 'int', - '$bar' => 'float', - '$baz' => 'bool', - ]; - - $right_args_2 = [ - '$foo' => 'integer', - '$bar' => 'double', - '$baz' => 'boolean', - ]; - - $wrong_args = [ - '$foo' => 'number', - '$bar' => 'double', - '$baz' => 'boolean', - ]; - - static::assertTrue( match_closure( $cb, null, $right_args_1 ) ); - static::assertTrue( match_closure( $cb, null, $right_args_2 ) ); - static::assertFalse( match_closure( $cb, null, $wrong_args ) ); - } -} \ No newline at end of file diff --git a/tests/src/MatchObjectClassTest.php b/tests/src/MatchObjectClassTest.php deleted file mode 100644 index 6ff3da4..0000000 --- a/tests/src/MatchObjectClassTest.php +++ /dev/null @@ -1,99 +0,0 @@ -rewind(); - - $first = $parsed->current(); - $parsed->next(); - $second = $parsed->current(); - $parsed->next(); - $third = $parsed->current(); - - static::assertCount( 3, $parsed ); - - static::assertSame( $this, $first[2] ); - static::assertSame( 'a_public_method', $first[4] ); - - static::assertSame( null, $second[2] ); - static::assertSame( static::case_class(), $second[3] ); - - static::assertSame( 10, $third[1] ); - static::assertInstanceOf( \Closure::class, $third[2] ); - } - - public function test_object_callbacks_for_hook_one_priority_one_hook() { - - $parsed = object_callbacks_for_hook( 'shutdown' ); - $parsed->rewind(); - - $first = $parsed->current(); - - static::assertCount( 1, $parsed ); - - static::assertSame( 55, $first[1] ); - static::assertInstanceOf( \Closure::class, $first[2] ); - } - - public function test_object_callbacks_for_hook_many_priorities_many_hook() { - - $parsed = object_callbacks_for_hook( 'the_title' ); - $parsed->rewind(); - - static::assertCount( 6, $parsed ); - - $first = $parsed->current(); - $parsed->next(); - $second = $parsed->current(); - $parsed->next(); - $third = $parsed->current(); - $parsed->next(); - $fourth = $parsed->current(); - $parsed->next(); - $fifth = $parsed->current(); - $parsed->next(); - $sixth = $parsed->current(); - - static::assertSame( 123, $first[1] ); - static::assertSame( 'assertTrue', $first[4] ); - - static::assertSame( 123, $second[1] ); - static::assertSame( '__invoke', $second[4] ); - - static::assertSame( 124, $third[1] ); - static::assertSame( 'another_public_method', $third[4] ); - - static::assertSame( 124, $fourth[1] ); - static::assertSame( '__invoke', $fourth[4] ); - - static::assertSame( 666, $fifth[1] ); - static::assertSame( 'a_public_method', $fifth[4] ); - - static::assertSame( 666, $sixth[1] ); - static::assertSame( 'assertFalse', $sixth[4] ); - } - - public function test_object_callbacks_for_hook_given_priority_many_hook() { - - $parsed = object_callbacks_for_hook( 'the_title', 124 ); - $parsed->rewind(); - - static::assertCount( 2, $parsed ); - - $first = $parsed->current(); - $parsed->next(); - $second = $parsed->current(); - - static::assertSame( 124, $first[1] ); - static::assertSame( $this, $first[2] ); - static::assertSame( __CLASS__, $first[3] ); - static::assertSame( 'another_public_method', $first[4] ); - - static::assertSame( 124, $second[1] ); - static::assertSame( '__invoke', $second[4] ); - } - - public function test_object_callbacks_no_added_hook() { - - $parsed = object_callbacks_for_hook( 'foo_bar' ); - - static::assertCount( 0, $parsed ); - } -} \ No newline at end of file diff --git a/tests/src/ParseCallbackDataTest.php b/tests/src/ParseCallbackDataTest.php deleted file mode 100644 index 6fc4cd8..0000000 --- a/tests/src/ParseCallbackDataTest.php +++ /dev/null @@ -1,73 +0,0 @@ - '' ], 10 ); - $string_func = parse_callback_data( [ 'function' => 'strtolower' ], 10 ); - $array_func = parse_callback_data( [ 'function' => [ __CLASS__, __METHOD__ ] ], 10 ); - $object_func = parse_callback_data( [ 'function' => $closure ], 10 ); - - static::assertCount( 0, $empty_array ); - static::assertCount( 0, $empty_func ); - static::assertCount( 0, $string_func ); - static::assertCount( 5, $array_func ); - static::assertCount( 5, $object_func ); - } - - public function test_closure_data() { - - $closure = function () { - }; - - list( $idx, $priority, $object, $class, $method ) = parse_callback_data( [ 'function' => $closure ], 222 ); - - static::assertSame( spl_object_hash( $closure ), $idx ); - static::assertSame( 222, $priority ); - static::assertSame( $closure, $object ); - static::assertSame( \Closure::class, $class ); - static::assertSame( '__invoke', $method ); - - } - - public function test_dyn_method_data() { - - $data = parse_callback_data( [ 'function' => [ $this, __METHOD__ ] ], - 1 ); - list( $idx, $priority, $object, $class, $method ) = $data; - - static::assertSame( spl_object_hash( $this ) . __METHOD__, $idx ); - static::assertSame( - 1, $priority ); - static::assertSame( $this, $object ); - static::assertSame( __CLASS__, $class ); - static::assertSame( __METHOD__, $method ); - } - - public function test_static_method_data() { - - $data = parse_callback_data( [ 'function' => [ \SplFixedArray::class, 'fromArray' ] ], 123 ); - list( $idx, $priority, $object, $class, $method ) = $data; - - static::assertSame( \SplFixedArray::class . '::fromArray', $idx ); - static::assertSame( 123, $priority ); - static::assertSame( null, $object ); - static::assertSame( \SplFixedArray::class, $class ); - static::assertSame( 'fromArray', $method ); - } -} \ No newline at end of file diff --git a/tests/src/RemoveClassHookTest.php b/tests/src/RemoveClassHookTest.php deleted file mode 100644 index 86e5b71..0000000 --- a/tests/src/RemoveClassHookTest.php +++ /dev/null @@ -1,69 +0,0 @@ -lastRemovedFilterHookPriority() ); - } - - public function test_specific_static_method_specific_priority() { - - $removed = remove_class_hook( 'the_title', self::case_class(), 'assertFalse', 666 ); - - static::assertSame( 1, $removed ); - static::assertSame( 666, $this->lastRemovedFilterHookPriority() ); - } - - public function test_specific_static_method_specific_priority_not_found() { - - $removed = remove_class_hook( 'the_title', self::case_class(), 'assertTrue', 666 ); - - static::assertSame( 0, $removed ); - } - - public function test_specific_all_methods_specific_priority() { - - $removed = remove_class_hook( 'the_title', self::case_class(), null, 123 ); - - static::assertSame( 1, $removed ); - static::assertSame( 123, $this->lastRemovedFilterHookPriority() ); - } - -} \ No newline at end of file diff --git a/tests/src/RemoveClosureHookTest.php b/tests/src/RemoveClosureHookTest.php deleted file mode 100644 index 919605d..0000000 --- a/tests/src/RemoveClosureHookTest.php +++ /dev/null @@ -1,100 +0,0 @@ -removedFilterHookPriorities() ); - } - - public function test_remove_only_static_closures() { - - $removed_the_title = remove_closure_hook( 'the_title', FALSE ); - - $removed_template_redirect = remove_closure_hook( 'template_redirect', FALSE ); - - static::assertSame( 0, $removed_the_title ); - static::assertSame( 1, $removed_template_redirect ); - static::assertSame( 42, $this->lastRemovedFilterHookPriority() ); - } - - public function test_remove_closures_by_target_this_class() { - - $removed = remove_closure_hook( 'the_title', static::case_class() ); - - static::assertSame( 1, $removed ); - static::assertSame( 124, $this->lastRemovedFilterHookPriority() ); - } - - public function test_remove_closures_by_target_this_object() { - - $removed = remove_closure_hook( 'the_title', static::closure_bind() ); - - static::assertSame( 1, $removed ); - static::assertSame( 123, $this->lastRemovedFilterHookPriority() ); - } - - public function test_remove_closures_by_target_this_object_needs_instance_match() { - - $bind = static::closure_bind(); - $clone = clone $bind; - - $removed = remove_closure_hook( 'the_title', $clone ); - - static::assertSame( 0, $removed ); - } - - public function test_remove_closures_by_target_arg_names() { - - $removed_foo_bar = remove_closure_hook( 'the_title', null, [ '$foo', '$bar' ] ); - $removed_foo = remove_closure_hook( 'the_title', null, [ '$foo' ] ); - - static::assertSame( 0, $removed_foo_bar ); - static::assertSame( 1, $removed_foo ); - static::assertSame( 124, $this->lastRemovedFilterHookPriority() ); - } - - public function test_remove_closures_by_target_args_names_and_type() { - - $removed_no = remove_closure_hook( 'template_redirect', null, [ '$foo' => 'array', '$bar' => 'string' ] ); - $removed_ok = remove_closure_hook( 'template_redirect', null, [ '$foo' => 'string', '$bar' => 'array' ] ); - - static::assertSame( 0, $removed_no ); - static::assertSame( 1, $removed_ok ); - } - - public function test_remove_closures_by_target_this_and_args_and_priority() { - - $removed_no_1 = remove_closure_hook( 'the_title', self::closure_bind(), [ '$arg' => 'string' ], 123 ); - $removed_no_2 = remove_closure_hook( 'the_title', $this, [ '$arg' => 'int' ], 123 ); - $removed_no_3 = remove_closure_hook( 'the_title', self::closure_bind(), [ '$arg' => 'int' ], 124 ); - $removed_ok = remove_closure_hook( 'the_title', self::closure_bind(), [ '$arg' => 'int' ], 123 ); - - static::assertSame( 0, $removed_no_1 ); - static::assertSame( 0, $removed_no_2 ); - static::assertSame( 0, $removed_no_3 ); - static::assertSame( 1, $removed_ok ); - } -} \ No newline at end of file diff --git a/tests/src/RemoveInstanceHookTest.php b/tests/src/RemoveInstanceHookTest.php deleted file mode 100644 index 833b4f0..0000000 --- a/tests/src/RemoveInstanceHookTest.php +++ /dev/null @@ -1,33 +0,0 @@ -removedFilterHookPriorities() ); - } - - public function test_remove_by_priority() { - - self::assertSame( 0, remove_instance_hook( 'the_title', $this, 123 ) ); - self::assertSame( 1, remove_instance_hook( 'the_title', $this, 666 ) ); - } -} \ No newline at end of file diff --git a/tests/src/RemoveInvokableHookTest.php b/tests/src/RemoveInvokableHookTest.php deleted file mode 100644 index d603ad4..0000000 --- a/tests/src/RemoveInvokableHookTest.php +++ /dev/null @@ -1,41 +0,0 @@ -lastRemovedFilterHookPriority() ); - } - - public function test_remove_closures_as_invokable() { - - static::assertSame( 1, remove_invokable_hook( 'template_redirect', \Closure::class ) ); - static::assertSame( 42, $this->lastRemovedFilterHookPriority() ); - } - - public function test_remove_invokable_by_class_and_priority() { - - static::assertSame( 0, remove_invokable_hook( 'template_redirect', '@anonymous', 42 ) ); - static::assertSame( 1, remove_invokable_hook( 'template_redirect', '@anonymous', 4242 ) ); - static::assertSame( 1, remove_invokable_hook( 'template_redirect', \Closure::class, 42 ) ); - static::assertSame( 0, remove_invokable_hook( 'template_redirect', \Closure::class, 4242 ) ); - } -} \ No newline at end of file diff --git a/tests/src/RemoveObjectHookTest.php b/tests/src/RemoveObjectHookTest.php deleted file mode 100644 index 7d83028..0000000 --- a/tests/src/RemoveObjectHookTest.php +++ /dev/null @@ -1,136 +0,0 @@ -lastRemovedFilterHookPriority() ); - static::assertSame( 'init', $this->lastRemovedFilterHookTag() ); - } - - public function test_remove_dynamic_and_static() { - - $removed = remove_object_hook( 'init', static::case_class(), null, null, TRUE ); - - static::assertSame( 2, $removed ); - static::assertSame( [ 10, 10 ], $this->removedFilterHookPriorities() ); - static::assertSame( [ 'init', 'init' ], $this->removedFilterHookTags() ); - } - - public function test_remove_dynamic_and_static_specific_method() { - - $removed = remove_object_hook( 'init', static::case_class(), 'a_public_method', null, TRUE ); - - static::assertSame( 1, $removed ); - static::assertSame( 10, $this->lastRemovedFilterHookPriority() ); - static::assertSame( 'init', $this->lastRemovedFilterHookTag() ); - } - - public function test_remove_specific_static_method_do_nothing_if_static_not_allowed() { - - $removed = remove_object_hook( 'init', static::case_class(), 'assertTrue' ); - - static::assertSame( 0, $removed ); - } - - public function test_remove_specific_static_method_do_remove_if_static_allowed() { - - $removed = remove_object_hook( 'init', static::case_class(), 'assertTrue', null, TRUE ); - - static::assertSame( 1, $removed ); - static::assertSame( 10, $this->lastRemovedFilterHookPriority() ); - static::assertSame( 'init', $this->lastRemovedFilterHookTag() ); - } - - public function test_remove_dynamic_multi_priority() { - - $removed = remove_object_hook( 'the_title', static::case_class() ); - - static::assertSame( 2, $removed ); - static::assertSame( [ 124, 666 ], $this->removedFilterHookPriorities() ); - } - - public function test_remove_dynamic_and_static_multi_priority() { - - $removed = remove_object_hook( 'the_title', static::case_class(), null, null, TRUE ); - - static::assertSame( 4, $removed ); - static::assertSame( [ 123, 124, 666, 666 ], $this->removedFilterHookPriorities() ); - } - - public function test_remove_dynamic_and_static_specific_method_from_multi_priority_hook() { - - $removed = remove_object_hook( 'the_title', static::case_class(), 'a_public_method', null, TRUE ); - - static::assertSame( 1, $removed ); - static::assertSame( 666, $this->lastRemovedFilterHookPriority() ); - } - - public function test_remove_dynamic_and_static_specific_priority_from_multi_priority_hook() { - - $removed = remove_object_hook( 'the_title', static::case_class(), null, 123, TRUE ); - - static::assertSame( 1, $removed ); - static::assertSame( [ 123 ], $this->removedFilterHookPriorities() ); - } - - public function test_remove_dynamic_multi_priority_specific_priority_from_multi_priority_hook() { - - $removed = remove_object_hook( 'the_title', static::case_class(), null, 123 ); - - static::assertSame( 0, $removed ); - } - - public function test_remove_dynamic_specific_priority_specific_method_from_multi_priority_hook() { - - $removed = remove_object_hook( 'the_title', static::case_class(), 'assertTrue', 123 ); - - static::assertSame( 0, $removed ); - } - - public function test_remove_dynamic_and_static_specific_priority_specific_method_from_multi_priority_hook() { - - $removed = remove_object_hook( 'the_title', static::case_class(), 'assertTrue', 123, TRUE ); - - static::assertSame( 1, $removed ); - static::assertSame( [ 123 ], $this->removedFilterHookPriorities() ); - } - - public function test_remove_anonymous_by_class() { - - $removed = remove_object_hook( 'template_redirect', '@anonymous' ); - - static::assertSame( 1, $removed ); - static::assertSame( 4242, $this->lastRemovedFilterHookPriority() ); - } - - public function test_remove_anonymous_by_class_and_method() { - - $removed = remove_object_hook( 'template_redirect', '@anonymous', '__invoke' ); - - static::assertSame( 1, $removed ); - static::assertSame( 4242, $this->lastRemovedFilterHookPriority() ); - } -} \ No newline at end of file diff --git a/tests/src/TestCase.php b/tests/src/TestCase.php index a9c89ec..d9b1ca3 100644 --- a/tests/src/TestCase.php +++ b/tests/src/TestCase.php @@ -1,199 +1,35 @@ - - * @package object-hooks-remover - * @license http://opensource.org/licenses/MIT MIT - */ -abstract class TestCase extends PHPUnitTestCase { - - private static $closure_bind; - - /** - * @return string - */ - public static function case_class(): string { - return __CLASS__; - } - - /** - * @return \stdClass - */ - public static function closure_bind(): \stdClass { - - self::$closure_bind or self::$closure_bind = new \stdClass(); - - return self::$closure_bind; - } - - /** - * Used to stub WP function and make it testable. - * - * @param string $tag - * @param string $callback_id - * @param int $priority - * - * @return void - */ - public static function remove_filter( string $tag, string $callback_id, $priority = 10 ) { - - global $removed_filters; - is_array( $removed_filters ) or $removed_filters = []; - $removed_filters[] = (object) compact( 'tag', 'callback_id', 'priority' ); - } - - /** - * @return void - */ - public function a_public_method() { - } - - /** - * @return void - */ - public function another_public_method() { - } - - /** - * Set various hooks... - */ - protected function setUp() { - - parent::setUp(); - - $cl_1 = \Closure::bind( function ( int $arg ) { - - }, self::closure_bind() ); - - $cl_2 = function ( $foo ) { - - }; - - $cl_3 = static function ( string $foo, array $bar ) { - - }; - - $invokable = new class { - - public function __invoke() { - - } - }; - - global $wp_filter, $removed_filters; - $removed_filters = []; - $wp_filter = [ - 'init' => [ - 10 => [ - [ 'function' => [ $this, 'a_public_method' ] ], - [ 'function' => 'strtoupper' ], - [ 'function' => [ __CLASS__, 'assertTrue' ] ], - [ 'function' => $cl_2 ], - ] - ], - 'template_redirect' => [ - 42 => [ - [ 'function' => $cl_3 ], - ], - 4242 => [ - [ 'function' => $invokable ], - ] - ], - 'shutdown' => [ - 55 => [ - [ 'function' => $cl_1 ], - ] - ], - 'the_title' => [ - 123 => [ - [ 'function' => 'strtolower' ], - [ 'function' => [ __CLASS__, 'assertTrue' ] ], - [ 'function' => $cl_1 ], - ], - 124 => [ - [ 'function' => [ $this, 'another_public_method' ] ], - [ 'function' => 'strtoupper' ], - [ 'function' => $cl_2 ], - ], - 666 => [ - [ 'function' => [ $this, 'a_public_method' ] ], - [ 'function' => [ __CLASS__, 'assertFalse' ] ], - ] - ] - ]; - } - - /** - * Clean up hooks... - */ - protected function tearDown() { - - global $removed_filters, $wp_filter; - unset( $removed_filters, $wp_filter ); - - parent::tearDown(); - } - - /** - * @return string[] - */ - protected function removedFilterHookTags(): array { - - global $removed_filters; - - return $removed_filters ? array_column( $removed_filters, 'tag' ) : []; - } - - /** - * @return int[] - */ - protected function removedFilterHookPriorities(): array { - - global $removed_filters; - - return $removed_filters ? array_column( $removed_filters, 'priority' ) : []; - } - - /** - * @return string|null - */ - protected function lastRemovedFilterHookTag() { - - global $removed_filters; - - return $removed_filters ? reset( $removed_filters )->tag : null; - } - - /** - * @return string|null - */ - protected function lastRemovedFilterHookId() { - - global $removed_filters; - - return $removed_filters ? reset( $removed_filters )->callback_id : null; - } - - /** - * @return int|null - */ - protected function lastRemovedFilterHookPriority() { - - global $removed_filters; - - return $removed_filters ? reset( $removed_filters )->priority : null; - } - -} \ No newline at end of file +