├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── quality-assurance.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── SECURITY.md ├── benchmarks ├── CompiledProviderBench.php ├── OptimizedCompiledProviderBench.php ├── OrderedProviderBench.php ├── ProviderBenchBase.php └── TukioBenchmarks.php ├── composer.json ├── default-.env ├── phpbench.json ├── phpinsights.php ├── phpstan.neon └── src ├── CallbackEventInterface.php ├── CallbackProvider.php ├── CompiledListenerProviderBase.php ├── ContainerMissingException.php ├── DebugEventDispatcher.php ├── Dispatcher.php ├── Entry ├── CompileableListenerEntry.php ├── ListenerEntry.php ├── ListenerFunctionEntry.php ├── ListenerServiceEntry.php └── ListenerStaticMethodEntry.php ├── InvalidTypeException.php ├── Listener.php ├── ListenerAfter.php ├── ListenerAttribute.php ├── ListenerBefore.php ├── ListenerPriority.php ├── ListenerProxy.php ├── Order.php ├── OrderedListenerProvider.php ├── OrderedProviderInterface.php ├── ProviderBuilder.php ├── ProviderCollector.php ├── ProviderCompiler.php ├── ServiceRegistrationClassNotExists.php ├── ServiceRegistrationTooManyMethods.php └── SubscriberInterface.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Crell] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Detailed description 4 | 5 | Provide a detailed description of the change or addition you are proposing. 6 | 7 | Make it clear if the issue is a bug, an enhancement or just a question. 8 | 9 | ## Context 10 | 11 | Why is this change important to you? How would you use it? 12 | 13 | How can it benefit other users? 14 | 15 | ## Possible implementation 16 | 17 | Not obligatory, but suggest an idea for implementing addition or change. 18 | 19 | ## Your environment 20 | 21 | Include as many relevant details about the environment you experienced the bug in and how to reproduce it. 22 | 23 | * Version used (e.g. PHP 5.6, HHVM 3): 24 | * Operating system and version (e.g. Ubuntu 16.04, Windows 7): 25 | * Link to your project: 26 | * ... 27 | * ... 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | Describe your changes in detail. 6 | 7 | ## Motivation and context 8 | 9 | Why is this change required? What problem does it solve? 10 | 11 | If it fixes an open issue, please link to the issue here (if you write `fixes #num` 12 | or `closes #num`, the issue will be automatically closed when the pull is accepted.) 13 | 14 | ## How has this been tested? 15 | 16 | Please describe in detail how you tested your changes. 17 | 18 | Include details of your testing environment, and the tests you ran to 19 | see how your change affects other areas of the code, etc. 20 | 21 | ## Screenshots (if appropriate) 22 | 23 | ## Types of changes 24 | 25 | What types of changes does your code introduce? Put an `x` in all the boxes that apply: 26 | - [ ] Bug fix (non-breaking change which fixes an issue) 27 | - [ ] New feature (non-breaking change which adds functionality) 28 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 29 | 30 | ## Checklist: 31 | 32 | Go over all the following points, and put an `x` in all the boxes that apply. 33 | 34 | Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/). 35 | 36 | - [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document. 37 | - [ ] My pull request addresses exactly one patch/feature. 38 | - [ ] I have created a branch for this patch/feature. 39 | - [ ] Each individual commit in the pull request is meaningful. 40 | - [ ] I have added tests to cover my changes. 41 | - [ ] If my change requires a change to the documentation, I have updated it accordingly. 42 | 43 | If you're unsure about any of these, don't hesitate to ask. We're here to help! 44 | -------------------------------------------------------------------------------- /.github/workflows/quality-assurance.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Quality assurance 3 | on: 4 | push: 5 | branches: ['master'] 6 | pull_request: ~ 7 | 8 | jobs: 9 | phpunit: 10 | name: PHPUnit tests on ${{ matrix.php }} ${{ matrix.composer-flags }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php: [ '8.1', '8.2', '8.3', '8.4' ] 15 | composer-flags: [ '' ] 16 | phpunit-flags: [ '--coverage-text' ] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php }} 22 | coverage: xdebug 23 | tools: composer:v2 24 | - run: composer install --no-progress ${{ matrix.composer-flags }} 25 | - run: vendor/bin/phpunit ${{ matrix.phpunit-flags }} 26 | phpstan: 27 | name: PHPStan checks on ${{ matrix.php }} 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | php: [ '8.1', '8.2', '8.3', '8.4' ] 32 | composer-flags: [ '' ] 33 | steps: 34 | - uses: actions/checkout@v2 35 | - uses: shivammathur/setup-php@v2 36 | with: 37 | php-version: ${{ matrix.php }} 38 | coverage: xdebug 39 | tools: composer:v2 40 | - run: composer install --no-progress ${{ matrix.composer-flags }} 41 | - run: vendor/bin/phpstan 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Tukio` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | ## 2.0.0 - 2024-04-14 8 | 9 | ### Added 10 | - Major internal refactoring. 11 | - There is now a `listener()` method on the Provider and Compiler classes that allows specifying multiple before/after rules at once, in addition to priority. It is *recommended* to use this method in place of the older ones. 12 | - Similarly, there is a `listenerService()` method for registering any service-based listener. 13 | - Upgraded to OrderedCollection v2, and switched to a Topological-based sort. The main advantage is the ability to support multiple before/after rules. However, this has a side effect that the order of listeners that had no relative order specified may have changed. This is not an API break as that order was never guaranteed, but may still affect some order-sensitive code that worked by accident. If that happens, and you care about the order, specify before/after orders as appropriate. 14 | - Attributes are now the recommended way to register listeners. 15 | - Attributes may be placed on the class level, and will be inherited by method-level listeners. 16 | 17 | ### Deprecated 18 | - `SubscriberInterface` is now deprecated. It will be removed in v3. 19 | - the `addListener`, `addListenerBefore`, `addListenerAfter`, `addListenerService`, `addListenerServiceBefore`, and `addListenerServiceAfter` methods have been deprecated. They will be removed in v3. Use `listener()` and `listenerService()` instead. 20 | 21 | ### Fixed 22 | - Nothing 23 | 24 | ### Removed 25 | - Nothing 26 | 27 | ### Security 28 | - Nothing 29 | 30 | 31 | ## 1.5.0 - 2023-03-25 32 | 33 | ### Added 34 | - Tukio now depends on Crell/OrderedCollection, which used to be part of this library. 35 | 36 | ### Deprecated 37 | - Nothing 38 | 39 | ### Fixed 40 | - Nothing 41 | 42 | ## 1.4.1 - 2022-06-02 43 | 44 | ### Added 45 | - Nothing 46 | 47 | ### Deprecated 48 | - Nothing 49 | 50 | ### Fixed 51 | - Moved phpstan to a dev dependency, where it should have been in the first place. 52 | 53 | ## 1.4.0 - 2022-03-30 54 | 55 | ### Added 56 | - Added PHPStan and PHPBench as direct dev dependencies. 57 | 58 | ### Deprecated 59 | - Nothing 60 | 61 | ### Fixed 62 | - The codebase is now PHPStan Level 6 compliant. There should be no functional changes. 63 | 64 | ### Removed 65 | - Removed support for PHP < 7.4. Stats show the number of such users is zero. 66 | - Increased required PHPUnit version to support PHP 8.1 67 | - Remove PHPInsights and the nasty vendor-bin workaround it required for dev. 68 | 69 | ### Security 70 | - Nothing 71 | 72 | ## NEXT - YYYY-MM-DD 73 | 74 | ### Added 75 | - Nothing 76 | 77 | ### Deprecated 78 | - Nothing 79 | 80 | ### Fixed 81 | - Nothing 82 | 83 | ### Removed 84 | - Nothing 85 | 86 | ### Security 87 | - Nothing 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | The Code Manifesto 2 | ================== 3 | 4 | We want to work in an ecosystem that empowers developers to reach their potential--one that encourages growth and effective collaboration. A space that is safe for all. 5 | 6 | A space such as this benefits everyone that participates in it. It encourages new developers to enter our field. It is through discussion and collaboration that we grow, and through growth that we improve. 7 | 8 | In the effort to create such a place, we hold to these values: 9 | 10 | 1. **Discrimination limits us.** This includes discrimination on the basis of race, gender, sexual orientation, gender identity, age, nationality, technology and any other arbitrary exclusion of a group of people. 11 | 2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort levels. Remember that, and if brought to your attention, heed it. 12 | 3. **We are our biggest assets.** None of us were born masters of our trade. Each of us has been helped along the way. Return that favor, when and where you can. 13 | 4. **We are resources for the future.** As an extension of #3, share what you know. Make yourself a resource to help those that come after you. 14 | 5. **Respect defines us.** Treat others as you wish to be treated. Make your discussions, criticisms and debates from a position of respectfulness. Ask yourself, is it true? Is it necessary? Is it constructive? Anything less is unacceptable. 15 | 6. **Reactions require grace.** Angry responses are valid, but abusive language and vindictive actions are toxic. When something happens that offends you, handle it assertively, but be respectful. Escalate reasonably, and try to allow the offender an opportunity to explain themselves, and possibly correct the issue. 16 | 7. **Opinions are just that: opinions.** Each and every one of us, due to our background and upbringing, have varying opinions. That is perfectly acceptable. Remember this: if you respect your own opinions, you should respect the opinions of others. 17 | 8. **To err is human.** You might not intend it, but mistakes do happen and contribute to build experience. Tolerate honest mistakes, and don't hesitate to apologize if you make one yourself. 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/Crell/Tukio). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PER-CS Coding Standard](https://www.php-fig.org/per/coding-style/)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0-rc-cli 2 | WORKDIR /usr/src/myapp 3 | # CMD [ "vendor/bin/phpunit", "--filter=CompiledEventDispatcherAttributeTest" ] 4 | CMD [ "vendor/bin/phpunit" ] 5 | # CMD [ "php", "test.php" ] 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | This version of the GNU Lesser General Public License incorporates the 12 | terms and conditions of version 3 of the GNU General Public License, 13 | supplemented by the additional permissions listed below. 14 | 15 | #### 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the 19 | GNU General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, other 22 | than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | #### 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | #### 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | - a) under this License, provided that you make a good faith effort 58 | to ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | - b) under the GNU GPL, with none of the additional permissions of 62 | this License applicable to that copy. 63 | 64 | #### 3. Object Code Incorporating Material from Library Header Files. 65 | 66 | The object code form of an Application may incorporate material from a 67 | header file that is part of the Library. You may convey such object 68 | code under terms of your choice, provided that, if the incorporated 69 | material is not limited to numerical parameters, data structure 70 | layouts and accessors, or small macros, inline functions and templates 71 | (ten or fewer lines in length), you do both of the following: 72 | 73 | - a) Give prominent notice with each copy of the object code that 74 | the Library is used in it and that the Library and its use are 75 | covered by this License. 76 | - b) Accompany the object code with a copy of the GNU GPL and this 77 | license document. 78 | 79 | #### 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, taken 82 | together, effectively do not restrict modification of the portions of 83 | the Library contained in the Combined Work and reverse engineering for 84 | debugging such modifications, if you also do each of the following: 85 | 86 | - a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | - b) Accompany the Combined Work with a copy of the GNU GPL and this 90 | license document. 91 | - c) For a Combined Work that displays copyright notices during 92 | execution, include the copyright notice for the Library among 93 | these notices, as well as a reference directing the user to the 94 | copies of the GNU GPL and this license document. 95 | - d) Do one of the following: 96 | - 0) Convey the Minimal Corresponding Source under the terms of 97 | this License, and the Corresponding Application Code in a form 98 | suitable for, and under terms that permit, the user to 99 | recombine or relink the Application with a modified version of 100 | the Linked Version to produce a modified Combined Work, in the 101 | manner specified by section 6 of the GNU GPL for conveying 102 | Corresponding Source. 103 | - 1) Use a suitable shared library mechanism for linking with 104 | the Library. A suitable mechanism is one that (a) uses at run 105 | time a copy of the Library already present on the user's 106 | computer system, and (b) will operate properly with a modified 107 | version of the Library that is interface-compatible with the 108 | Linked Version. 109 | - e) Provide Installation Information, but only if you would 110 | otherwise be required to provide such information under section 6 111 | of the GNU GPL, and only to the extent that such information is 112 | necessary to install and execute a modified version of the 113 | Combined Work produced by recombining or relinking the Application 114 | with a modified version of the Linked Version. (If you use option 115 | 4d0, the Installation Information must accompany the Minimal 116 | Corresponding Source and Corresponding Application Code. If you 117 | use option 4d1, you must provide the Installation Information in 118 | the manner specified by section 6 of the GNU GPL for conveying 119 | Corresponding Source.) 120 | 121 | #### 5. Combined Libraries. 122 | 123 | You may place library facilities that are a work based on the Library 124 | side by side in a single library together with other library 125 | facilities that are not Applications and are not covered by this 126 | License, and convey such a combined library under terms of your 127 | choice, if you do both of the following: 128 | 129 | - a) Accompany the combined library with a copy of the same work 130 | based on the Library, uncombined with any other library 131 | facilities, conveyed under the terms of this License. 132 | - b) Give prominent notice with the combined library that part of it 133 | is a work based on the Library, and explaining where to find the 134 | accompanying uncombined form of the same work. 135 | 136 | #### 6. Revised Versions of the GNU Lesser General Public License. 137 | 138 | The Free Software Foundation may publish revised and/or new versions 139 | of the GNU Lesser General Public License from time to time. Such new 140 | versions will be similar in spirit to the present version, but may 141 | differ in detail to address new problems or concerns. 142 | 143 | Each version is given a distinguishing version number. If the Library 144 | as you received it specifies that a certain numbered version of the 145 | GNU Lesser General Public License "or any later version" applies to 146 | it, you have the option of following the terms and conditions either 147 | of that published version or of any later version published by the 148 | Free Software Foundation. If the Library as you received it does not 149 | specify a version number of the GNU Lesser General Public License, you 150 | may choose any version of the GNU Lesser General Public License ever 151 | published by the Free Software Foundation. 152 | 153 | If the Library as you received it specifies that a proxy can decide 154 | whether future versions of the GNU Lesser General Public License shall 155 | apply, that proxy's public statement of acceptance of any version is 156 | permanent authorization for you to choose that version for the 157 | Library. 158 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | compose_command = docker-compose run -u $(id -u ${USER}):$(id -g ${USER}) --rm php81 2 | 3 | build: 4 | docker-compose build 5 | 6 | shell: build 7 | $(compose_command) bash 8 | 9 | destroy: 10 | docker-compose down -v 11 | 12 | composer: build 13 | $(compose_command) composer install 14 | 15 | test: build 16 | $(compose_command) vendor/bin/phpunit 17 | 18 | phpstan: build 19 | $(compose_command) vendor/bin/phpstan 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tukio 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Total Downloads][ico-downloads]][link-downloads] 6 | 7 | 8 | Tukio is a complete and robust implementation of the [PSR-14](http://www.php-fig.org/psr/psr-14/) Event Dispatcher specification. It supports normal and debug Event Dispatchers, both runtime and compiled Providers, complex ordering of Listeners, and attribute-based registration on PHP 8. 9 | 10 | "Tukio" is the Swahili word for "Event". 11 | 12 | ## Install 13 | 14 | Via Composer 15 | 16 | ``` bash 17 | $ composer require crell/tukio 18 | ``` 19 | 20 | ## PSR-14 Usage 21 | 22 | PSR-14 consists of two key components: A Dispatcher and a Provider. A Dispatcher is what a client library will compose and then pass Events objects to. A Provider is what a framework will give to a Dispatcher to match the Event to Listeners. Tukio includes multiple implementations of both. 23 | 24 | The general structure of using any PSR-14-compliant implementation is: 25 | 26 | ```php 27 | $provider = new SomeProvider(); 28 | // Do some sort of setup on $provider so that it knows what Listeners it should match to what Events. 29 | // This could be pretty much anything depending on the implementation. 30 | 31 | $dispatcher = new SomeDispatcher($provider); 32 | 33 | // Pass $dispatcher to some client code somewhere, and then: 34 | 35 | $thingHappened = new ThingHappened($thing); 36 | 37 | $dispatcher->dispatch($thingHappened); 38 | ``` 39 | 40 | Note that `dispatch()` will return `$thingHappened` as well, so if Listeners are expected to add data to it then you can chain a method call on it if desired: 41 | 42 | ```php 43 | $reactions = $dispatcher->dispatch(new ThingHappened($thing))->getReactions(); 44 | ``` 45 | 46 | In practice most of that will be handled through a Dependency Injection Container, but there's no requirement that it do so. 47 | 48 | ## Dispatchers 49 | 50 | Tukio includes two Dispatchers: `Dispatcher` is the standard implementation, which should be used 95% of the time. It can optionally take a [PSR-3 Logger](http://www.php-fig.org/psr/psr-14/), in which case it will log a warning on any exception thrown from a Listener. 51 | 52 | The second is `DebugEventDispatcher`, which like the names says is useful for debugging. It logs every Event that is passed and then delegates to a composed dispatcher (such as `Dispatcher`) to actually dispatch the Event. 53 | 54 | For example: 55 | 56 | ```php 57 | use Crell\Tukio\Dispatcher; 58 | use Crell\Tukio\DebugEventDispatcher; 59 | 60 | $provider = new SomeProvider(); 61 | 62 | $logger = new SomeLogger(); 63 | 64 | $dispatcher = new Dispatcher($provider, $logger); 65 | 66 | $debugDispatcher = new DebugEventDispatcher($dispatcher, $logger); 67 | 68 | // Now pass $debugDispatcher around and use it like any other dispatcher. 69 | ``` 70 | 71 | ## Providers 72 | 73 | Tukio includes multiple options for building Providers, and it's encouraged that you combine them with the generic ones included in the [`fig/event-dispatcher-util`](https://github.com/php-fig/event-dispatcher-util) library provided by the FIG. 74 | 75 | Which Provider or Providers you use depends on your use case. All of them are valid for certain use cases. 76 | 77 | ### `OrderedListenerProvider` 78 | 79 | As the name implies, `OrderedListenerProvider` is all about ordering. Users can explicitly register Listeners on it that will be matched against an Event based on its type. 80 | 81 | If no order is specified, then the order that Listeners will be returned is undefined, and in practice users should expect the order to be stable, but not predictable. That makes the degenerate case super-easy: 82 | 83 | ```php 84 | use Crell\Tukio\OrderedListenerProvider; 85 | 86 | class StuffHappened {} 87 | 88 | class SpecificStuffHappened extends StuffHappened {} 89 | 90 | function handleStuff(StuffHappened $stuff) { ... } 91 | 92 | 93 | $provider = new OrderedListenerProvider(); 94 | 95 | $provider->listener(function(SpecificStuffHappened) { 96 | // ... 97 | }); 98 | 99 | $provider->listener('handleStuff'); 100 | ``` 101 | 102 | That adds two Listeners to the Provider; one anonymous function and one named function. The anonymous function will be called for any `SpecificStuffHappened` event. The named function will be called for any `StuffHappened` *or* `SpecificStuffHappened` event. And the user doesn't really care which one happens first (which is the typical case). 103 | 104 | #### Ordering listeners 105 | 106 | However, the user can also be picky about the order in which Listeners will fire. Tukio supports two ordering mechanisms: Priority order, and Topological sorting (before/after markers). Internally, Tukio will convert priority ordering into topological ordering. 107 | 108 | ```php 109 | use Crell\Tukio\OrderedListenerProvider; 110 | 111 | $provider = new OrderedListenerProvider(); 112 | 113 | $provider->listener(function(SpecificStuffHappened) { 114 | // ... 115 | }, priority: 10); 116 | 117 | $provider->listener('handleStuff', priority: 20); 118 | ``` 119 | 120 | Now, the named function Listener will get called before the anonymous function does. (Higher priority number comes first, and negative numbers are totally legal.) If two listeners have the same priority then their order relative to each other is undefined. 121 | 122 | Sometimes, though, you may not know the priority of another Listener, but it's important your Listener happen before or after it. For that we need to add a new concept: IDs. Every Listener has an ID, which can be provided when the Listener is added or will be auto-generated if not. The auto-generated value is predictable (the name of a function, the class-and-method of an object method, etc.), so in most cases it's not necessary to read the return value of `listener()` although that is slightly more robust. 123 | 124 | ```php 125 | use Crell\Tukio\OrderedListenerProvider; 126 | 127 | $provider = new OrderedListenerProvider(); 128 | 129 | // The ID will be "handleStuff", unless there is such an ID already, 130 | //in which case it would be "handleStuff-1" or similar. 131 | $id = $provider->listener('handleStuff'); 132 | 133 | // This Listener will get called before handleStuff does. If you want to specify an ID 134 | // you can, since anonymous functions just get a random string as their generated ID. 135 | $provider->listener($id, function(SpecificStuffHappened) { 136 | // ... 137 | }, before: ['my_specifics']); 138 | ``` 139 | 140 | Here, the priority of `handleStuff` is undefined; the user doesn't care when it gets called. However, the anonymous function, if it should get called at all, will always get called after `handleStuff` does. It's possible that some other Listener could also be called in between the two, but one will always happen after the other. 141 | 142 | The `listener()` method is used for all registration, and can accept a priority, a list of listener IDs the new listener must come before, and a list of listener IDs the new listener must come after. It also supports specifying a custom ID, and a custom `$type`. 143 | 144 | Because that's a not-small number of options, it is *strongly recommended* that you use named arguments for all arguments other than the listener callable itself. 145 | 146 | ```php 147 | public function listener( 148 | callable $listener, 149 | ?int $priority = null, 150 | array $before = [], 151 | array $after = [], 152 | ?string $id = null, 153 | ?string $type = null 154 | ): string; 155 | ``` 156 | 157 | The `listener()` method will always return the ID that was used for that listener. If desired the `$type` parameter allows a user to specify the Event type that the Listener is for if different than the type declaration in the function. For example, if the Listener doesn't have a type declaration or should only apply to some parent class of what it's type declaration is. (That's a rare edge case, which is why it's the last parameter.) 158 | 159 | #### Service Listeners 160 | 161 | Often, though, Listeners are themselves methods of objects that should not be instantiated until and unless needed. That's exactly what a Dependency Injection Container allows, and `OrderedListenerProvider` fully supports those, called "Service Listeners." They work almost exactly the same, except you specify a service and method name: 162 | 163 | ```php 164 | public function listenerService( 165 | string $service, 166 | ?string $method = null, 167 | ?string $type = null, 168 | ?int $priority = null, 169 | array $before = [], 170 | array $after = [], 171 | ?string $id = null 172 | ): string; 173 | ``` 174 | 175 | The `$type`, `$priority`, `$before`, `$after`, and `$id` parameters work the same way as for `listener()`. `$service` is any service name that will be retrieved from a container on-demand, and `$method` is the method on the object. 176 | 177 | If the service name is the same as that of a found class (which is typical in most modern conventions), then Tukio can attempt to derive the method and type from the class. If the service name is not the same as a defined class, it cannot do so and both `$method` and `$type` are required and will throw an exception if missing. 178 | 179 | If no `$method` is specified and the service name matches a class, Tukio will attempt to derive the method for you. If the class has only one method, that method will be automatically selected. Otherwise, if there is a `__invoke()` method, that will be automatically selected. Otherwise, the auto-detection fails and an exception is thrown. 180 | 181 | The services themselves can be from any [PSR-11](http://www.php-fig.org/psr/psr-11/)-compatible Container. 182 | 183 | ```php 184 | use Crell\Tukio\OrderedListenerProvider; 185 | 186 | class SomeService 187 | { 188 | public function methodA(ThingHappened $event): void { ... } 189 | 190 | public function methodB(SpecificThingHappened $event): void { ... } 191 | } 192 | 193 | class MyListeners 194 | { 195 | public function methodC(WhatHappened $event): void { ... } 196 | 197 | public function somethingElse(string $beep): string { ... } 198 | } 199 | 200 | class EasyListening 201 | { 202 | public function __invoke(SpecificThingHappened $event): void { ... } 203 | } 204 | 205 | $container = new SomePsr11Container(); 206 | // Configure the container somehow. 207 | $container->register('some_service', SomeService::class); 208 | $container->register(MyListeners::class, MyListeners::class); 209 | $container->register(EasyListening::class, EasyListening::class); 210 | 211 | $provider = new OrderedListenerProvider($container); 212 | 213 | // Manually register two methods on the same service. 214 | $idA = $provider->listenerService('some_service', 'methodA', ThingHappened::class); 215 | $idB = $provider->listenerService('some_service', 'methodB', SpecificThingHappened::class); 216 | 217 | // Register a specific method on a derivable service class. 218 | // The type (WhatHappened) will be derived automatically. 219 | $idC = $provider->listenerService(MyListeners::class, 'methodC', after: 'some_service-methodB'); 220 | 221 | // Auto-everything! This is the easiest option. 222 | $provider->listenerService(EasyListening::class, before: $idC); 223 | ``` 224 | 225 | In this example, we have listener methods defined in three different classes, all of which are registered with a PSR-11 container. In the first code block, we register two listeners out of a class whose service name does not match its class name. In the second, we register a method on a class whose service name does match its class name, so we can derive the event type by reflection. In the third block, we use a single-method listener class, which allows everything to be derived! 226 | 227 | Of note, the `methodB` Listener is referencing the `methodA` listener by an explict ID. The generated ID is as noted predictable, so in most cases you don't need to use the return value. The return value is the more robust and reliable option, though, as if the requested ID is already in-use a new one will be generated. 228 | 229 | #### Attribute-based registration 230 | 231 | The preferred way to configure Tukio, however, is via attributes. There are four relevant attributes: `Listener`, `ListenerPriority`, `ListenerBefore`, and `ListenerAfter`. All can be used with sequential parameters or named parameters. In most cases, named parameters will be more self-documenting. All attributes are valid only on functions and methods. 232 | 233 | * `Listener` declares a callable a listener and optionally sets the `id` and `type`: `#[Listener(id: 'a_listener', type: 'SomeClass')]. 234 | * `ListenerPriority` has a required `priority` parameter, and optional `id` and `type`: `#[ListenerPriority(5)]` or `#[ListenerPriority(priority: 3, id: "a_listener")]`. 235 | * `ListenerBefore` has a required `before` parameter, and optional `id` and `type`: `#[ListenerBefore('other_listener')]` or `#[ListenerBefore(before: 'other_listener', id: "a_listener")]`. 236 | * `ListenerAfter` has a required `after` parameter, and optional `id` and `type`: `#[ListenerAfter('other_listener')]` or `#[ListenerAfter(after: ['other_listener'], id: "a_listener")]`. 237 | 238 | The `$before` and `$after` parameters will accept either a single string, or an array of strings. 239 | 240 | As multiple attributes may be included in a single block, that allows for compact syntax like so: 241 | 242 | ```php 243 | #[Listener(id: 'a_listener'), ListenerBefore('other'), ListenerAfter('something', 'else')] 244 | function my_listener(SomeEvent $event): void { ... } 245 | 246 | // Or just use the one before/after you care about: 247 | #[ListenerAfter('something_early')] 248 | function other(SomeEvent $event): void { ... } 249 | ``` 250 | 251 | If you pass a listener with Listener attributes to `listener()` or `listenerService()`, the attribute defined configuration will be used. If you pass configuration in the method signature, however, that will override any values taken from the attributes. 252 | 253 | ### Subscribers 254 | 255 | A "Subscriber" (a name openly and unashamedly borrowed from Symfony) is a class with multiple listener methods on it. Tukio allows you to bulk-register any listener-like methods on a class, just by registering the class. 256 | 257 | ```php 258 | $provider->addSubscriber(SomeCollectionOfListeners::class, 'service_name'); 259 | ``` 260 | 261 | As before, if the service name is the same as that of the class, it may be omitted. A method will be registered if either: 262 | 263 | * it has any `Listener*` attributes on it. 264 | * the method name begins with `on`. 265 | 266 | For example: 267 | 268 | ```php 269 | class SomeCollectionOfListeners 270 | { 271 | // Registers, with a custom ID. 272 | #[Listener(id: 'a')] 273 | public function onA(CollectingEvent $event) : void 274 | { 275 | $event->add('A'); 276 | } 277 | 278 | // Registers, with a custom priority. 279 | #[ListenerPriority(priority: 5)] 280 | public function onB(CollectingEvent $event) : void 281 | { 282 | $event->add('B'); 283 | } 284 | 285 | // Registers, before listener "a" above. 286 | #[ListenerBefore(before: 'a')] 287 | public function onC(CollectingEvent $event) : void 288 | { 289 | $event->add('C'); 290 | } 291 | 292 | // Registers, after listener "a" above. 293 | #[ListenerAfter(after: 'a')] 294 | public function onD(CollectingEvent $event) : void 295 | { 296 | $event->add('D'); 297 | } 298 | 299 | // This still self-registers because of the name. 300 | public function onE(CollectingEvent $event) : void 301 | { 302 | $event->add('E'); 303 | } 304 | 305 | // Registers, with a given priority despite its non-standard name. 306 | #[ListenerPriority(priority: -5)] 307 | public function notNormalName(CollectingEvent $event) : void 308 | { 309 | $event->add('F'); 310 | } 311 | 312 | // No attribute, non-standard name, this method is not registered. 313 | public function ignoredMethodThatDoesNothing() : void 314 | { 315 | throw new \Exception('What are you doing here?'); 316 | } 317 | } 318 | ``` 319 | 320 | ### Listener classes 321 | 322 | As hinted above, one of the easiest ways to structure a listener is to make it the only method on a class, particularly if it is named `__invoke()`, and give the service the same name as the class. That way, it can be registered trivially and derive all of its configuration through attributes. Since it is extremely rare for a listener to be registered twice (use cases likely exist, but we are not aware of one), this does not cause a name collision issue. 323 | 324 | Tukio has two additional features to make it even easier. One, if the listener method is `__invoke()`, then the ID of the listener will by default be just the class name. Two, the Listener attributes may also be placed on the class, not the method, in which case the class-level settings will inherit to every method. 325 | 326 | The result is that the easiest way to define listeners is as single-method classes, like so: 327 | 328 | ```php 329 | class ListenerOne 330 | { 331 | public function __construct( 332 | private readonly DependencyA $depA, 333 | private readonly DependencyB $depB, 334 | ) {} 335 | 336 | public function __invoke(MyEvent $event): void { ... } 337 | } 338 | 339 | #[ListenerBefore(ListenerOne::class)] 340 | class ListenerTwo 341 | { 342 | public function __invoke(MyEvent $event): void { ... } 343 | } 344 | 345 | $provider->listenerService(ListenerOne::class); 346 | $provider->listenerService(ListenerTwo::class); 347 | ``` 348 | 349 | Now, the API call itself is trivially easy. Just specify the class name. `ListenerTwo::__invoke()` will be called before `ListnerOne::__invoke()`, regardless of the order in which they were registered. When `ListenerOne` is requested from your DI container, the container will fill in its dependencies automatically. 350 | 351 | This is the recommended way to write listeners for use with Tukio. 352 | 353 | ### Deprecated functionality 354 | 355 | A few registration mechanisms left over from Tukio version 1 are still present, but explicitly deprecated. They will be removed in a future version. Please migrate off of them as soon as possible. 356 | 357 | #### Dedicated registration methods 358 | 359 | The following methods still work, but are just aliases around calling `listener()` or `listenerService()`. They are less capable than just using `listener()`, as `listener()` allows for specifying a priority, before, and after all at once, including multiple before/after targets. The methods below do neither. Please migrate to `listener()` and `listenerService()`. 360 | 361 | ```php 362 | public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string; 363 | 364 | public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string; 365 | 366 | public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string; 367 | 368 | public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string; 369 | 370 | public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string; 371 | 372 | public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string; 373 | ``` 374 | 375 | #### Subscriber interface 376 | 377 | In Tukio v1, there was an optional `SubscriberInterface` to allow for customizing the registration of methods as listeners via a static method that bundled the various `addListener*()` calls up within the class. With the addition of attributes in PHP 8, however, that functionality is no longer necessary as attributes can do everything the Subscriber interface could, with less work. 378 | 379 | The `SubscriberInterface` is still supported, but deprecated. It will be removed in a future version. Please migrate to attributes. 380 | 381 | The basic case works like this: 382 | 383 | ```php 384 | use Crell\Tukio\OrderedListenerProvider; 385 | use Crell\Tukio\SubscriberInterface; 386 | 387 | class Subscriber implements SubscriberInterface 388 | { 389 | public function onThingsHappening(ThingHappened $event) : void { ... } 390 | 391 | public function onSpecialEvent(SpecificThingHappened $event) : void { ... } 392 | 393 | public function somethingElse(ThingHappened $event) : void { ... } 394 | 395 | public static function registerListeners(ListenerProxy $proxy) : void 396 | { 397 | $id = $proxy->addListener('somethingElse', 10); 398 | $proxy->addListenerAfter($id, 'onSpecialEvent'); 399 | } 400 | } 401 | 402 | $container = new SomePsr11Container(); 403 | // Configure the container so that the service 'listeners' is an instance of Subscriber above. 404 | 405 | $provider = new OrderedListenerProvider($container); 406 | 407 | $provider->addSubscriber(Subscriber::class, 'listeners'); 408 | ``` 409 | 410 | As before, `onThingsHappen()` will be registered automatically. However, `somethingElse()` will also be registered as a Listener with a priority of 10, and `onSpecialEvent()` will be registered to fire after it. 411 | 412 | ### Compiled Provider 413 | 414 | All of that registration and ordering logic is powerful, and it's surprisingly fast in practice. What's even faster, though, is not having to re-register on every request. For that, Tukio offers a compiled provider option. 415 | 416 | The compiled provider comes in three parts: `ProviderBuilder`, `ProviderCompiler`, and a generated provider class. `ProviderBuilder`, as the name implies, is an object that allows you to build up a set of Listeners that will make up a Provider. They work exactly the same as on `OrderedListenerProvider`, and in fact it exposes the same `OrderedProviderInterface`. 417 | 418 | `ProviderCompiler` then takes a builder object and writes a new PHP class to a provided stream (presumably a file on disk) that matches the definitions in the builder. That built Provider is fixed; it cannot be modified and no new Listeners can be added to it, but all the ordering and sorting has already been done, making it notably faster (to say nothing of skipping the registration process itself). 419 | 420 | Let's see it in action: 421 | 422 | ```php 423 | use Crell\Tukio\ProviderBuilder; 424 | use Crell\Tukio\ProviderCompiler; 425 | 426 | $builder = new ProviderBuilder(); 427 | 428 | $builder->listener('listenerA', priority: 100); 429 | $builder->listener('listenerB', after: 'listenerA'); 430 | $builder->listener([Listen::class, 'listen']); 431 | $builder->listenerService(MyListener::class); 432 | $builder->addSubscriber('subscriberId', Subscriber::class); 433 | 434 | $compiler = new ProviderCompiler(); 435 | 436 | // Write the generated compiler out to a file. 437 | $filename = 'MyCompiledProvider.php'; 438 | $out = fopen($filename, 'w'); 439 | 440 | // Here's the magic: 441 | $compiler->compile($builder, $out, 'MyCompiledProvider', '\\Name\\Space\\Of\\My\\App'); 442 | 443 | fclose($out); 444 | ``` 445 | 446 | `$builder` can do anything that `OrderedListenerProvider` can do, except that it only supports statically-defined Listeners. That means it does not support anonymous functions or methods of an object, but it will still handle functions, static methods, services, and subscribers just fine. In practice when using a compiled container you will most likely want to use almost entirely service listeners and subscribers, since you'll most likely be using it with a container. 447 | 448 | That gives you a file on disk named `MyCompiledProvider.php`, which contains `Name\Space\Of\My\App\MyCompiledProvider`. (Name it something logical for you.) At runtime, then, do this: 449 | 450 | ```php 451 | // Configure the container such that it has a service "listeners" 452 | // and another named "subscriber". 453 | 454 | $container = new Psr11Container(); 455 | $container->addService('D', new ListenService()); 456 | 457 | include('MyCompiledProvider.php'); 458 | 459 | $provider = new Name\Space\Of\My\App\MyCompiledProvider($container); 460 | ``` 461 | 462 | And boom! `$provider` is now a fully functional Provider you can pass to a Dispatcher. It will work just like any other, but faster. 463 | 464 | Alternatively, the compiler can output a file with an anonymous class. In this case, a class name or namespace are irrelevant. 465 | 466 | ```php 467 | // Write the generated compiler out to a file. 468 | $filename = 'MyCompiledProvider.php'; 469 | $out = fopen($filename, 'w'); 470 | 471 | $compiler->compileAnonymous($builder, $out); 472 | 473 | fclose($out); 474 | ``` 475 | 476 | Because the compiled container will be instantiated by including a file, but it needs a container instance to function, it cannot be easily just `require()`ed. Instead, use the `loadAnonymous()` method on a `ProviderCompiler` instance to load it. (It does not need to be the same instance that was used to create it.) 477 | 478 | ```php 479 | $compiler = new ProviderCompiler(); 480 | 481 | $provider = $compiler->loadAnonymous($filename, $containerInstance); 482 | ``` 483 | 484 | But what if you want to have most of your listeners pre-registered, but have some that you add conditionally at runtime? Have a look at the FIG's [`AggregateProvider`](https://github.com/php-fig/event-dispatcher-util/blob/master/src/AggregateProvider.php), and combine your compiled Provider with an instance of `OrderedListenerProvider`. 485 | 486 | ### Compiler optimization 487 | 488 | The `ProviderBuilder` has one other trick. If you specify one or more events via the `optimizeEvent($class)` method, then the compiler will pre-compute what listeners apply to it based on its type, including its parent classes and interfaces. The result is a constant-time simple array lookup for those events, also known as "virtually instantaneous." 489 | 490 | ```php 491 | use Crell\Tukio\ProviderBuilder; 492 | use Crell\Tukio\ProviderCompiler; 493 | 494 | $builder = new ProviderBuilder(); 495 | 496 | $builder->listener('listenerA', priority: 100); 497 | $builder->listener('listenerB', after: 'listenerA'); 498 | $builder->listener([Listen::class, 'listen']); 499 | $builder->listenerService(MyListener::class); 500 | $builder->addSubscriber('subscriberId', Subscriber::class); 501 | 502 | // Here's where you specify what events you know you will have. 503 | // Returning the listeners for these events will be near instant. 504 | $builder->optimizeEvent(EvenOne::class); 505 | $builder->optimizeEvent(EvenTwo::class); 506 | 507 | $compiler = new ProviderCompiler(); 508 | 509 | // Write the generated compiler out to a file. 510 | $filename = 'MyCompiledProvider.php'; 511 | $out = fopen($filename, 'w'); 512 | 513 | $compiler->compileAnonymous($builder, $out); 514 | 515 | fclose($out); 516 | ``` 517 | 518 | ### `CallbackProvider` 519 | 520 | The third option Tukio provides is a `CallbackProvider`, which takes an entirely different approach. In this case, the Provider works only on events that have a `CallbackEventInterface`. The use case is for Events that are carrying some other object, which itself has methods on it that should be called at certain times. Think lifecycle callbacks for a domain object, for example. 521 | 522 | To see it in action, we'll use an example straight out of Tukio's test suite: 523 | 524 | ```php 525 | use Crell\Tukio\CallbackEventInterface; 526 | use Crell\Tukio\CallbackProvider; 527 | 528 | class LifecycleEvent implements CallbackEventInterface 529 | { 530 | protected $entity; 531 | 532 | public function __construct(FakeEntity $entity) 533 | { 534 | $this->entity = $entity; 535 | } 536 | 537 | public function getSubject() : object 538 | { 539 | return $this->entity; 540 | } 541 | } 542 | 543 | class LoadEvent extends LifecycleEvent {} 544 | 545 | class SaveEvent extends LifecycleEvent {} 546 | 547 | class FakeEntity 548 | { 549 | 550 | public function load(LoadEvent $event) : void { ... } 551 | 552 | public function save(SaveEvent $event) : void { ... } 553 | 554 | public function stuff(StuffEvent $event) : void { ... } 555 | 556 | public function all(LifecycleEvent $event) : void { ... } 557 | } 558 | 559 | $provider = new CallbackProvider(); 560 | 561 | $entity = new FakeEntity(); 562 | 563 | $provider->addCallbackMethod(LoadEvent::class, 'load'); 564 | $provider->addCallbackMethod(SaveEvent::class, 'save'); 565 | $provider->addCallbackMethod(LifecycleEvent::class, 'all'); 566 | 567 | $event = new LoadEvent($entity); 568 | 569 | $provider->getListenersForEvent($event); 570 | ``` 571 | 572 | In this example, the provider is configured not with Listeners but with method names that correspond to Events. Those methods are methods on the "subject" object. The Provider will now return callables for `[$entity, 'load']` and `[$entity, 'all']` when called with a `LoadEvent`. That allows a domain object itself to have Listeners on it that will get called at the appropriate time. 573 | 574 | ## Change log 575 | 576 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 577 | 578 | ## Testing 579 | 580 | ``` bash 581 | $ composer test 582 | ``` 583 | 584 | ## Contributing 585 | 586 | Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details. 587 | 588 | ## Security 589 | 590 | If you discover any security related issues, please use the [GitHub security reporting form](https://github.com/Crell/Tukio/security) rather than the issue queue. 591 | 592 | ## Credits 593 | 594 | - [Larry Garfield][link-author] 595 | - [All Contributors][link-contributors] 596 | 597 | ## License 598 | 599 | The Lesser GPL version 3 or later. Please see [License File](LICENSE.md) for more information. 600 | 601 | [ico-version]: https://img.shields.io/packagist/v/Crell/Tukio.svg?style=flat-square 602 | [ico-license]: https://img.shields.io/badge/License-LGPLv3-green.svg?style=flat-square 603 | [ico-downloads]: https://img.shields.io/packagist/dt/Crell/Tukio.svg?style=flat-square 604 | 605 | [link-packagist]: https://packagist.org/packages/Crell/Tukio 606 | [link-scrutinizer]: https://scrutinizer-ci.com/g/Crell/Tukio/code-structure 607 | [link-code-quality]: https://scrutinizer-ci.com/g/Crell/Tukio 608 | [link-downloads]: https://packagist.org/packages/Crell/Tukio 609 | [link-author]: https://github.com/Crell 610 | [link-contributors]: ../../contributors 611 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Brand Promise 2 | 3 | Perfect security is not an achievable goal, but it is a goal to strive for nonetheless. To that end, we welcome responsible security reports from both users and external security researchers. 4 | 5 | # Scope 6 | 7 | If you believe you've found a security issue in software that is maintained in this repository, we encourage you to notify us. 8 | 9 | | Version | In scope | Source code | 10 | | ------- | -------- |--------------------------| 11 | | latest | ✅ | https://github.com/Crell/Tukio | 12 | 13 | Only the latest stable release of this library is supported. In general, bug and security fixes will not be backported unless there is a substantial imminent threat to users in not doing so. 14 | 15 | # How to Submit a Report 16 | 17 | To submit a vulnerability report, please contact us through [GitHub](https://github.com/Crell/Tukio/security). Your submission will be reviewed as soon as feasible, but as this is a volunteer project we cannot guarantee a response time. 18 | 19 | # Safe Harbor 20 | 21 | We support safe harbor for security researchers who: 22 | 23 | * Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our services. 24 | * Only interact with accounts you own or with explicit permission of the account holder. If you do encounter Personally Identifiable Information (PII) contact us immediately, do not proceed with access, and immediately purge any local information. 25 | * Provide us with a reasonable amount of time to resolve vulnerabilities prior to any disclosure to the public or a third-party. 26 | 27 | We will consider activities conducted consistent with this policy to constitute "authorized" conduct and will not pursue civil action or initiate a complaint to law enforcement. We will help to the extent we can if legal action is initiated by a third party against you. 28 | 29 | Please submit a report to us before engaging in conduct that may be inconsistent with or unaddressed by this policy. 30 | 31 | # Preferences 32 | 33 | * Please provide detailed reports with reproducible steps and a clearly defined impact. 34 | * Include the version number of the vulnerable package in your report. 35 | * Providing a suggested fix is welcome, but not required, and we may choose to implement our own, based on your submitted fix or not. 36 | * This is a volunteer project. We will make every effort to respond to security reports in a timely manner, but that may be a week or two on the first contact. 37 | 38 | -------------------------------------------------------------------------------- /benchmarks/CompiledProviderBench.php: -------------------------------------------------------------------------------- 1 | next(); 37 | 38 | foreach(range(1, ceil(static::$numListeners/2)) as $counter) { 39 | $builder->addListener([static::class, 'fakeListener'], $priority->current()); 40 | $builder->addListenerService('Foo', 'bar', DummyEvent::class, $priority->current()); 41 | $priority->next(); 42 | } 43 | 44 | $builder->optimizeEvents(...static::$optimizeClasses); 45 | 46 | // Write the generated compiler out to a temp file. 47 | $out = fopen(static::$filename, 'w'); 48 | $compiler->compile($builder, $out, static::$className, static::$namespace); 49 | fclose($out); 50 | } 51 | 52 | public static function removeCompiledProvider(): void 53 | { 54 | unlink(static::$filename); 55 | } 56 | 57 | public function setUp(): void 58 | { 59 | include static::$filename; 60 | 61 | $container = new MockContainer(); 62 | 63 | $compiledClassName = static::$namespace . '\\' . static::$className; 64 | $this->provider = new $compiledClassName($container); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /benchmarks/OptimizedCompiledProviderBench.php: -------------------------------------------------------------------------------- 1 | provider = new OrderedListenerProvider(); 18 | 19 | $priority = new \InfiniteIterator(new \ArrayIterator(static::$listenerPriorities)); 20 | $priority->next(); 21 | 22 | foreach(range(1, static::$numListeners) as $counter) { 23 | $this->provider->addListener([static::class, 'fakeListener'], $priority->current()); 24 | $priority->next(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /benchmarks/ProviderBenchBase.php: -------------------------------------------------------------------------------- 1 | 32 | * 33 | * Deliberately in an unsorted order to force it to need to be sorted. 34 | */ 35 | protected static $listenerPriorities = [1, 2, -2, 3, 0, -1, -3]; 36 | 37 | public function setUp(): void 38 | { 39 | throw new \Exception('You need to implement setUp().'); 40 | } 41 | 42 | /** 43 | * ParamProviders({"provideItems"}) 44 | */ 45 | public function bench_match_provider(): void 46 | { 47 | $task = new CollectingEvent(); 48 | 49 | $listeners = $this->provider->getListenersForEvent($task); 50 | 51 | // Run out the generator. 52 | is_array($listeners) || iterator_to_array($listeners); 53 | } 54 | 55 | public static function fakeListener(CollectingEvent $task): void 56 | { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /benchmarks/TukioBenchmarks.php: -------------------------------------------------------------------------------- 1 | 'default', 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | IDE 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This options allow to add hyperlinks in your terminal to quickly open 28 | | files in your favorite IDE while browsing your PhpInsights report. 29 | | 30 | | Supported: "textmate", "macvim", "emacs", "sublime", "phpstorm", 31 | | "atom", "vscode". 32 | | 33 | | If you have another IDE that is not in this list but which provide an 34 | | url-handler, you could fill this config with a pattern like this: 35 | | 36 | | myide://open?url=file://%f&line=%l 37 | | 38 | */ 39 | 40 | 'ide' => 'phpstorm', 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Configuration 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Here you may adjust all the various `Insights` that will be used by PHP 48 | | Insights. You can either add, remove or configure `Insights`. Keep in 49 | | mind, that all added `Insights` must belong to a specific `Metric`. 50 | | 51 | */ 52 | 53 | 'exclude' => [ 54 | // 'path/to/directory-or-file' 55 | 'benchmarks', 56 | ], 57 | 58 | 'add' => [ 59 | // ExampleMetric::class => [ 60 | // ExampleInsight::class, 61 | // ] 62 | ], 63 | 64 | 'remove' => [ 65 | // In hindsight I agree with these, but it would be an API break to change. Consider doing that in the next major. 66 | SlevomatCodingStandard\Sniffs\Classes\SuperfluousExceptionNamingSniff::class, 67 | SlevomatCodingStandard\Sniffs\Classes\SuperfluousInterfaceNamingSniff::class, 68 | // Public properties on internal classes are fine. 69 | ObjectCalisthenics\Sniffs\Classes\ForbiddenPublicPropertySniff::class, 70 | // It's not unusual for an implementing class to not need every optional parameter. 71 | SlevomatCodingStandard\Sniffs\Functions\UnusedParameterSniff::class, 72 | // This is broken, because sometimes you have to late static bind a constant 73 | // if the constant won't be defined until the child class, which is exactly what we 74 | // do in the compiled provider. 75 | SlevomatCodingStandard\Sniffs\Classes\DisallowLateStaticBindingForConstantsSniff::class, 76 | // There's nothing wrong with the short ternary, WTF? 77 | SlevomatCodingStandard\Sniffs\ControlStructures\DisallowShortTernaryOperatorSniff::class, 78 | // Sometimes a swallowing catch really is correct. I'm not sure how to not disable this globally. 79 | PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis\EmptyStatementSniff::class, 80 | // This is just a stupid rule. 81 | SlevomatCodingStandard\Sniffs\ControlStructures\AssignmentInConditionSniff::class, 82 | // This is a well-meaning but over-aggressive rule. 83 | SlevomatCodingStandard\Sniffs\Operators\RequireOnlyStandaloneIncrementAndDecrementOperatorsSniff::class, 84 | // I don't even know why this exists. 85 | SlevomatCodingStandard\Sniffs\Commenting\InlineDocCommentDeclarationSniff::class, 86 | // Most of the time this is good, but OrderedCollection has to support mixed. 87 | SlevomatCodingStandard\Sniffs\TypeHints\DisallowMixedTypeHintSniff::class, 88 | // Now you're just being stupid. 89 | NunoMaduro\PhpInsights\Domain\Insights\ForbiddenTraits::class, 90 | // Oh hells no. Keep that anal retentive stupidity out of my code base. 91 | NunoMaduro\PhpInsights\Domain\Insights\ForbiddenNormalClasses::class, 92 | // I just don't agree with this one. 93 | PhpCsFixer\Fixer\CastNotation\CastSpacesFixer::class, 94 | // Or this one. 95 | PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\SpaceAfterNotSniff::class, 96 | ], 97 | 98 | 'config' => [ 99 | \ObjectCalisthenics\Sniffs\NamingConventions\ElementNameMinimalLengthSniff::class => [ 100 | 'minLength' => 3, 101 | 'allowedShortNames' => ['i', 'id', 'to', 'up', 'e'], 102 | ], 103 | \ObjectCalisthenics\Sniffs\Metrics\MaxNestingLevelSniff::class => [ 104 | 'maxNestingLevel' => 3, 105 | ], 106 | \PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff::class => [ 107 | 'lineLimit' => 80, 108 | 'absoluteLineLimit' => 120, 109 | 'ignoreComments' => false, 110 | ], 111 | ], 112 | 113 | /* 114 | |-------------------------------------------------------------------------- 115 | | Requirements 116 | |-------------------------------------------------------------------------- 117 | | 118 | | Here you may define a level you want to reach per `Insights` category. 119 | | When a score is lower than the minimum level defined, then an error 120 | | code will be returned. This is optional and individually defined. 121 | | 122 | */ 123 | 124 | 'requirements' => [ 125 | // 'min-quality' => 0, 126 | // 'min-complexity' => 0, 127 | // 'min-architecture' => 0, 128 | // 'min-style' => 0, 129 | // 'disable-security-check' => false, 130 | ], 131 | 132 | ]; 133 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 8 3 | paths: 4 | - src 5 | - tests 6 | ignoreErrors: 7 | - 8 | identifier: missingType.generics 9 | - 10 | message: '#type has no value type specified in iterable type array#' 11 | path: tests/ 12 | - 13 | message: '#type has no value type specified in iterable type iterable#' 14 | path: tests/ 15 | # PHPStan is overly aggressive on readonly properties. 16 | - '#Class (.*) has an uninitialized readonly property (.*). Assign it in the constructor.#' 17 | - '#Readonly property (.*) is assigned outside of the constructor.#' 18 | -------------------------------------------------------------------------------- /src/CallbackEventInterface.php: -------------------------------------------------------------------------------- 1 | > */ 12 | protected array $callbacks = []; 13 | 14 | /** 15 | * @return iterable 16 | */ 17 | public function getListenersForEvent(object $event): iterable 18 | { 19 | if (!$event instanceof CallbackEventInterface) { 20 | return []; 21 | } 22 | 23 | $subject = $event->getSubject(); 24 | 25 | foreach ($this->callbacks as $type => $callbacks) { 26 | if ($event instanceof $type) { 27 | foreach ($callbacks as $callback) { 28 | if (method_exists($subject, $callback)) { 29 | yield $subject->$callback(...); 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | public function addCallbackMethod(string $type, string $method): self 37 | { 38 | $this->callbacks[$type][] = $method; 39 | return $this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/CompiledListenerProviderBase.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | protected array $listeners = []; 22 | 23 | /** 24 | * This nested array will be generated by the compiler in a subclass. It's listed here for reference only. 25 | * 26 | * The keys are an event class name. The value is an array of callables that may be 27 | * returned to the dispatcher as-is. 28 | * 29 | * @var array> 30 | */ 31 | protected array $optimized = []; 32 | 33 | public function __construct(protected ContainerInterface $container) {} 34 | 35 | /** 36 | * @return iterable 37 | */ 38 | public function getListenersForEvent(object $event): iterable 39 | { 40 | if (isset($this->optimized[$event::class])) { 41 | return $this->optimized[$event::class]; 42 | } 43 | 44 | $count = count($this->listeners); 45 | $ret = []; 46 | for ($i = 0; $i < $count; ++$i) { 47 | /** @var array $listener */ 48 | $listener = $this->listeners[$i]; 49 | if ($event instanceof $listener['type']) { 50 | $ret[] = $listener['callable']; 51 | } 52 | } 53 | return $ret; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ContainerMissingException.php: -------------------------------------------------------------------------------- 1 | logger->debug('Processing event of type {type}.', ['type' => get_class($event), 'event' => $event]); 31 | return $this->dispatcher->dispatch($event); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Dispatcher.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 22 | $this->logger = $logger ?? new NullLogger(); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function dispatch(object $event) 29 | { 30 | // If the event is already stopped, this method becomes a no-op. 31 | if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { 32 | return $event; 33 | } 34 | 35 | foreach ($this->provider->getListenersForEvent($event) as $listener) { 36 | // Technically this has an extraneous stopped-check after the last listener, 37 | // but that doesn't violate the spec since it's still technically checking 38 | // before each listener is called, given the check above. 39 | try { 40 | $listener($event); 41 | if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { 42 | break; 43 | } 44 | } // We do not catch Errors here, because Errors indicate the developer screwed up in 45 | // some way. Let those bubble up because they should just become fatals. 46 | catch (\Exception $e) { 47 | $this->logger->warning('Unhandled exception thrown from listener while processing event.', [ 48 | 'event' => $event, 49 | 'exception' => $e, 50 | ]); 51 | 52 | throw $e; 53 | } 54 | } 55 | return $event; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Entry/CompileableListenerEntry.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function getProperties(): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Entry/ListenerEntry.php: -------------------------------------------------------------------------------- 1 | listener = $listener; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Entry/ListenerFunctionEntry.php: -------------------------------------------------------------------------------- 1 | static::class, 25 | 'listener' => $this->listener, 26 | 'type' => $this->type, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Entry/ListenerServiceEntry.php: -------------------------------------------------------------------------------- 1 | static::class, 32 | 'serviceName' => $this->serviceName, 33 | 'method' => $this->method, 34 | 'type' => $this->type, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Entry/ListenerStaticMethodEntry.php: -------------------------------------------------------------------------------- 1 | static::class, 32 | 'class' => $this->class, 33 | 'method' => $this->method, 34 | 'type' => $this->type, 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/InvalidTypeException.php: -------------------------------------------------------------------------------- 1 | getName(), $method); 25 | } catch (ReflectionException $e) { 26 | $message .= " ((unknown class)::{$method})"; 27 | } 28 | return new self($message, 0, $previous); 29 | } 30 | 31 | public static function fromFunctionCallable(callable $function, ?Throwable $previous = null): self 32 | { 33 | $message = static::$baseMessage; 34 | if (is_string($function) || $function instanceof \Closure) { 35 | try { 36 | $reflector = new ReflectionFunction($function); 37 | $message .= sprintf(' (%s:%s)', $reflector->getFileName(), $reflector->getStartLine()); 38 | } catch (ReflectionException $e) { 39 | // No meaningful data to add 40 | } 41 | } 42 | return new self($message, 0, $previous); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Listener.php: -------------------------------------------------------------------------------- 1 | type) { 62 | $this->hasDefinition = true; 63 | } 64 | } 65 | 66 | public function fromReflection(\ReflectionMethod $subject): void 67 | { 68 | $this->paramCount = $subject->getNumberOfRequiredParameters(); 69 | if ($this->paramCount === 1) { 70 | $params = $subject->getParameters(); 71 | // getName() isn't part of the interface, but is present. PHP bug. 72 | // @phpstan-ignore-next-line 73 | $this->type ??= $params[0]->getType()?->getName(); 74 | } 75 | } 76 | 77 | /** 78 | * This will only get called when this attribute is on a class. 79 | * 80 | * @param Listener[] $methods 81 | */ 82 | public function setMethods(array $methods): void 83 | { 84 | $this->methods = $methods; 85 | } 86 | 87 | public function includeMethodsByDefault(): bool 88 | { 89 | return true; 90 | } 91 | 92 | public function methodAttribute(): string 93 | { 94 | return __CLASS__; 95 | } 96 | 97 | /** 98 | * @param array $methods 99 | */ 100 | public function setStaticMethods(array $methods): void 101 | { 102 | $this->staticMethods = $methods; 103 | } 104 | 105 | public function includeStaticMethodsByDefault(): bool 106 | { 107 | return true; 108 | } 109 | 110 | public function staticMethodAttribute(): string 111 | { 112 | return __CLASS__; 113 | } 114 | 115 | 116 | /** 117 | * This will only get called when this attribute is used on a method. 118 | * 119 | * @param Listener $class 120 | */ 121 | public function fromClassAttribute(object $class): void 122 | { 123 | $this->id ??= $class->id; 124 | $this->type ??= $class->type; 125 | $this->priority ??= $class->priority; 126 | $this->before = [...$this->before, ...$class->before]; 127 | $this->after = [...$this->after, ...$class->after]; 128 | } 129 | 130 | public function subAttributes(): array 131 | { 132 | return [ 133 | ListenerBefore::class => 'fromBefore', 134 | ListenerAfter::class => 'fromAfter', 135 | ListenerPriority::class => 'fromPriority', 136 | ]; 137 | } 138 | 139 | /** 140 | * @param array $attribs 141 | */ 142 | public function fromBefore(array $attribs): void 143 | { 144 | if ($attribs) { 145 | $this->hasDefinition ??= true; 146 | } 147 | foreach ($attribs as $attrib) { 148 | $this->id ??= $attrib->id; 149 | $this->type ??= $attrib->type; 150 | $this->before = [...$this->before, ...$attrib->before]; 151 | } 152 | } 153 | 154 | /** 155 | * @param array $attribs 156 | */ 157 | public function fromAfter(array $attribs): void 158 | { 159 | if ($attribs) { 160 | $this->hasDefinition ??= true; 161 | } 162 | foreach ($attribs as $attrib) { 163 | $this->id ??= $attrib->id; 164 | $this->type ??= $attrib->type; 165 | $this->after = [...$this->after, ...$attrib->after]; 166 | } 167 | } 168 | 169 | public function fromPriority(?ListenerPriority $attrib): void 170 | { 171 | if ($attrib) { 172 | $this->hasDefinition ??= true; 173 | } 174 | $this->id ??= $attrib?->id; 175 | $this->type ??= $attrib?->type; 176 | $this->priority = $attrib?->priority; 177 | } 178 | 179 | public function finalize(): void 180 | { 181 | $this->methods ??= []; 182 | $this->hasDefinition ??= false; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/ListenerAfter.php: -------------------------------------------------------------------------------- 1 | $after 18 | */ 19 | public function __construct( 20 | string|array $after, 21 | public ?string $id = null, 22 | public ?string $type = null, 23 | ) { 24 | $this->after = is_array($after) ? $after : [$after]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ListenerAttribute.php: -------------------------------------------------------------------------------- 1 | $before 18 | */ 19 | public function __construct( 20 | string|array $before, 21 | public ?string $id = null, 22 | public ?string $type = null, 23 | ) { 24 | $this->before = is_array($before) ? $before : [$before]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ListenerPriority.php: -------------------------------------------------------------------------------- 1 | 15 | * Methods that have already been registered on this subscriber, so we know not to double-subscribe them. 16 | */ 17 | protected array $registeredMethods = []; 18 | 19 | /** 20 | * @param OrderedProviderInterface $provider 21 | * @param string $serviceName 22 | * @param class-string $serviceClass 23 | */ 24 | public function __construct( 25 | protected OrderedProviderInterface $provider, 26 | protected string $serviceName, 27 | protected string $serviceClass 28 | ) {} 29 | 30 | /** 31 | * Adds a method on a service as a listener. 32 | * 33 | * @param string $methodName 34 | * The method name of the service that is the listener being registered. 35 | * @param ?int $priority 36 | * The numeric priority of the listener. Higher numbers will trigger before lower numbers. 37 | * @param ?string $id 38 | * The ID of this listener, so it can be referenced by other listeners. 39 | * @param ?string $type 40 | * The class or interface type of events for which this listener will be registered. 41 | * 42 | * @return string 43 | * The opaque ID of the listener. This can be used for future reference. 44 | */ 45 | public function addListener(string $methodName, ?int $priority = 0, ?string $id = null, ?string $type = null): string 46 | { 47 | $type = $type ?? $this->getServiceMethodType($methodName); 48 | $this->registeredMethods[] = $methodName; 49 | return $this->provider->addListenerService($this->serviceName, $methodName, $type, $priority, $id); 50 | } 51 | 52 | /** 53 | * Adds a service listener to trigger before another existing listener. 54 | * 55 | * Note: The new listener is only guaranteed to come before the specified existing listener. No guarantee is made 56 | * regarding when it comes relative to any other listener. 57 | * 58 | * @param string $before 59 | * The ID of an existing listener. 60 | * @param string $methodName 61 | * The method name of the service that is the listener being registered. 62 | * @param ?string $id 63 | * The ID of this listener, so it can be referenced by other listeners. 64 | * @param ?string $type 65 | * The class or interface type of events for which this listener will be registered. 66 | * 67 | * @return string 68 | * The opaque ID of the listener. This can be used for future reference. 69 | */ 70 | public function addListenerBefore(string $before, string $methodName, ?string $id = null, ?string $type = null): string 71 | { 72 | $type = $type ?? $this->getServiceMethodType($methodName); 73 | $this->registeredMethods[] = $methodName; 74 | return $this->provider->addListenerServiceBefore($before, $this->serviceName, $methodName, $type, $id); 75 | } 76 | 77 | /** 78 | * Adds a service listener to trigger before another existing listener. 79 | * 80 | * Note: The new listener is only guaranteed to come before the specified existing listener. No guarantee is made 81 | * regarding when it comes relative to any other listener. 82 | * 83 | * @param string $after 84 | * The ID of an existing listener. 85 | * @param string $methodName 86 | * The method name of the service that is the listener being registered. 87 | * @param ?string $id 88 | * The ID of this listener, so it can be referenced by other listeners. 89 | * @param ?string $type 90 | * The class or interface type of events for which this listener will be registered. 91 | * 92 | * @return string 93 | * The opaque ID of the listener. This can be used for future reference. 94 | */ 95 | public function addListenerAfter(string $after, string $methodName, ?string $id = null, ?string $type = null): string 96 | { 97 | $type = $type ?? $this->getServiceMethodType($methodName); 98 | $this->registeredMethods[] = $methodName; 99 | return $this->provider->addListenerServiceAfter($after, $this->serviceName, $methodName, $type, $id); 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | public function getRegisteredMethods(): array 106 | { 107 | return $this->registeredMethods; 108 | } 109 | 110 | /** 111 | * Safely gets the required Type for a given method from the service class. 112 | * 113 | * @param string $methodName 114 | * The method name of the listener being registered. 115 | * 116 | * @return string 117 | * The type required by the listener. 118 | * 119 | * @throws InvalidTypeException 120 | * If the method has invalid type-hinting, throws an error with a service/method trace. 121 | */ 122 | protected function getServiceMethodType(string $methodName): string 123 | { 124 | try { 125 | // We don't have a real object here, so we cannot use first-class-closures. 126 | // PHPStan complains that an array is not a callable, even though it is, because PHP. 127 | // @phpstan-ignore-next-line 128 | $type = $this->getParameterType([$this->serviceClass, $methodName]); 129 | } catch (\InvalidArgumentException $exception) { 130 | throw InvalidTypeException::fromClassCallable($this->serviceClass, $methodName, $exception); 131 | } 132 | return $type; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Order.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function getListenersForEvent(object $event): iterable 22 | { 23 | /** @var ListenerEntry $listener */ 24 | foreach ($this->listeners as $listener) { 25 | if ($event instanceof $listener->type) { 26 | yield $listener->listener; 27 | } 28 | } 29 | } 30 | 31 | protected function getListenerEntry(callable $listener, string $type): ListenerEntry 32 | { 33 | return new ListenerEntry($listener, $type); 34 | } 35 | 36 | public function listenerService( 37 | string $service, 38 | ?string $method = null, 39 | ?string $type = null, 40 | ?int $priority = null, 41 | array $before = [], 42 | array $after = [], 43 | ?string $id = null 44 | ): string { 45 | $method ??= $this->deriveMethod($service); 46 | 47 | if (!$type) { 48 | if (!class_exists($service)) { 49 | throw ServiceRegistrationClassNotExists::create($service); 50 | } 51 | // @phpstan-ignore-next-line 52 | $type = $this->getParameterType([$service, $method]); 53 | } 54 | 55 | $orderSpecified = !is_null($priority) || !empty($before) || !empty($after); 56 | 57 | // In the special case that the service is the class name, we can 58 | // leverage attributes. 59 | if (!$orderSpecified && class_exists($service)) { 60 | $listener = [$service, $method]; 61 | /** @var Listener $def */ 62 | $def = $this->classAnalyzer->analyze($service, Listener::class); 63 | $def = $def->methods[$method]; 64 | $id ??= $def?->id ?? $this->getListenerId($listener); 65 | 66 | return $this->listeners->add( 67 | item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type), 68 | id: $id, 69 | priority: $def->priority, 70 | before: $def->before, 71 | after: $def->after 72 | ); 73 | } 74 | 75 | $id ??= $service . '::' . $method; 76 | return $this->listeners->add( 77 | item: $this->getListenerEntry($this->makeListenerForService($service, $method), $type), 78 | id: $id, 79 | priority: $priority, 80 | before: $before, 81 | after: $after, 82 | ); 83 | } 84 | 85 | /** 86 | * Creates a callable that will proxy to the provided service and method. 87 | * 88 | * @param string $serviceName 89 | * The name of a service. 90 | * @param string $methodName 91 | * A method on the service. 92 | * @return callable 93 | * A callable that proxies to the the provided method and service. 94 | */ 95 | protected function makeListenerForService(string $serviceName, string $methodName): callable 96 | { 97 | if (!$this->container) { 98 | throw new ContainerMissingException(); 99 | } 100 | 101 | // We cannot verify the service name as existing at this time, as the container may be populated in any 102 | // order. Thus the referenced service may not be registered now but could be registered by the time the 103 | // listener is called. 104 | 105 | // Fun fact: We cannot auto-detect the listener target type from a container without instantiating it, which 106 | // defeats the purpose of a service registration. Therefore this method requires an explicit event type. Also, 107 | // the wrapping listener must listen to just object. The explicit $type means it will still get only 108 | // the right event type, and the real listener can still type itself properly. 109 | $container = $this->container; 110 | return static fn (object $event) => $container->get($serviceName)->$methodName($event); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/OrderedProviderInterface.php: -------------------------------------------------------------------------------- 1 | $before 18 | * A list of listener IDs this listener should come before. 19 | * @param array $after 20 | * A list of listener IDs this listener should come after. 21 | * @param ?string $id 22 | * The identifier by which this listener should be known. If not specified one will be generated. 23 | * @param ?string $type 24 | * The class or interface type of events for which this listener will be registered. If not provided 25 | * it will be derived based on the type declaration of the listener. 26 | * 27 | * @return string 28 | * The opaque ID of the listener. This can be used for future reference. */ 29 | public function listener( 30 | callable $listener, 31 | ?int $priority = null, 32 | array $before = [], 33 | array $after = [], 34 | ?string $id = null, 35 | ?string $type = null 36 | ): string; 37 | 38 | /** 39 | * Adds a method on a service as a listener. 40 | * 41 | * This method does not support attributes, as the class name is unknown at registration. 42 | * 43 | * @param string $service 44 | * The name of a service on which this listener lives. 45 | * @param string|null $method 46 | * The method name of the service that is the listener being registered. 47 | * If not specified, the collector will attempt to derive it on the 48 | * assumption the class and service name are the same. A single-method 49 | * class will use that single method. Otherwise, __invoke() will be assumed. 50 | * @param string|null $type 51 | * The class or interface type of events for which this listener will be registered. 52 | * If not specified, the collector will attempt to derive it on the assumption 53 | * that the class and service name are the same. 54 | * @param int|null $priority 55 | * The numeric priority of the listener. 56 | * @param array $before 57 | * A list of listener IDs this listener should come before. 58 | * @param array $after 59 | * A list of listener IDs this listener should come after. 60 | * @return string 61 | * The opaque ID of the listener. This can be used for future reference. 62 | */ 63 | public function listenerService( 64 | string $service, 65 | ?string $method = null, 66 | ?string $type = null, 67 | ?int $priority = null, 68 | array $before = [], 69 | array $after = [], 70 | ?string $id = null 71 | ): string; 72 | 73 | /** 74 | * Adds a listener to the provider. 75 | * 76 | * A Listener, ListenerBefore, or ListenerAfter attribute on the listener may also provide 77 | * the priority, id, or type. Values specified in the method call take priority over the attribute. 78 | * 79 | * @deprecated 80 | * @param callable $listener 81 | * The listener to register. 82 | * @param ?int $priority 83 | * The numeric priority of the listener. Higher numbers will trigger before lower numbers. 84 | * @param ?string $id 85 | * The identifier by which this listener should be known. If not specified one will be generated. 86 | * @param ?string $type 87 | * The class or interface type of events for which this listener will be registered. If not provided 88 | * it will be derived based on the type declaration of the listener. 89 | * 90 | * @return string 91 | * The opaque ID of the listener. This can be used for future reference. 92 | */ 93 | public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string; 94 | 95 | /** 96 | * Adds a listener to trigger before another existing listener. 97 | * 98 | * Note: The new listener is only guaranteed to come before the specified existing listener. No guarantee is made 99 | * regarding when it comes relative to any other listener. 100 | * 101 | * A Listener, ListenerBefore, or ListenerAfter attribute on the listener may also provide 102 | * the id or type. The $before parameter specified here will always be used and the type of attribute ignored. 103 | * 104 | * @deprecated 105 | * @param string $before 106 | * The ID of an existing listener. 107 | * @param callable $listener 108 | * The listener to register. 109 | * @param ?string $id 110 | * The identifier by which this listener should be known. If not specified one will be generated. 111 | * @param ?string $type 112 | * The class or interface type of events for which this listener will be registered. If not provided 113 | * it will be derived based on the type declaration of the listener. 114 | * 115 | * @return string 116 | * The opaque ID of the listener. This can be used for future reference. 117 | */ 118 | public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string; 119 | 120 | /** 121 | * Adds a listener to trigger after another existing listener. 122 | * 123 | * Note: The new listener is only guaranteed to come after the specified existing listener. No guarantee is made 124 | * regarding when it comes relative to any other listener. 125 | * 126 | * A Listener, ListenerBefore, or ListenerAfter attribute on the listener may also provide 127 | * the id or type. The $after parameter specified here will always be used and the type of attribute ignored. 128 | * 129 | * @deprecated 130 | * @param string $after 131 | * The ID of an existing listener. 132 | * @param callable $listener 133 | * The listener to register. 134 | * @param ?string $id 135 | * The identifier by which this listener should be known. If not specified one will be generated. 136 | * @param ?string $type 137 | * The class or interface type of events for which this listener will be registered. If not provided 138 | * it will be derived based on the type declaration of the listener. 139 | * 140 | * @return string 141 | * The opaque ID of the listener. This can be used for future reference. 142 | */ 143 | public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string; 144 | 145 | /** 146 | * Adds a method on a service as a listener. 147 | * 148 | * This method does not support attributes, as the class name is unknown at registration. 149 | * 150 | * @deprecated 151 | * @param string $service 152 | * The name of a service on which this listener lives. 153 | * @param string $method 154 | * The method name of the service that is the listener being registered. 155 | * @param string $type 156 | * The class or interface type of events for which this listener will be registered. 157 | * @param ?int $priority 158 | * The numeric priority of the listener. Higher numbers will trigger before lower numbers. 159 | * @param ?string $id 160 | * The identifier by which this listener should be known. If not specified one will be generated. 161 | * 162 | * @return string 163 | * The opaque ID of the listener. This can be used for future reference. 164 | */ 165 | public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string; 166 | 167 | /** 168 | * Adds a service listener to trigger before another existing listener. 169 | * 170 | * Note: The new listener is only guaranteed to come before the specified existing listener. No guarantee is made 171 | * regarding when it comes relative to any other listener. 172 | * 173 | * This method does not support attributes, as the class name is unknown at registration. 174 | * 175 | * @deprecated 176 | * @param string $before 177 | * The ID of an existing listener. 178 | * @param string $service 179 | * The name of a service on which this listener lives. 180 | * @param string $method 181 | * The method name of the service that is the listener being registered. 182 | * @param string $type 183 | * The class or interface type of events for which this listener will be registered. 184 | * @param ?string $id 185 | * The identifier by which this listener should be known. If not specified one will be generated. 186 | * 187 | * @return string 188 | * The opaque ID of the listener. This can be used for future reference. 189 | */ 190 | public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string; 191 | 192 | /** 193 | * Adds a service listener to trigger before another existing listener. 194 | * 195 | * Note: The new listener is only guaranteed to come before the specified existing listener. No guarantee is made 196 | * regarding when it comes relative to any other listener. 197 | * 198 | * This method does not support attributes, as the class name is unknown at registration. 199 | * 200 | * @deprecated 201 | * @param string $after 202 | * The ID of an existing listener. 203 | * @param string $service 204 | * The name of a service on which this listener lives. 205 | * @param string $method 206 | * The method name of the service that is the listener being registered. 207 | * @param string $type 208 | * The class or interface type of events for which this listener will be registered. 209 | * @param ?string $id 210 | * The identifier by which this listener should be known. If not specified one will be generated. 211 | * 212 | * @return string 213 | * The opaque ID of the listener. This can be used for future reference. 214 | */ 215 | public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string; 216 | 217 | /** 218 | * Registers all listener methods on a service as listeners. 219 | * 220 | * A public method on the specified class is a listener if either of these is true: 221 | * - It's name is in the form on*. onUpdate(), onUserLogin(), onHammerTime() will all be registered. 222 | * - It has a Listener/ListenerBefore/ListenerAfter/ListenerPriority attribute. 223 | * 224 | * The event type the listener is for will be derived from the type declaration in the method signature, 225 | * unless overriden by an attribute.. 226 | * 227 | * @param class-string $class 228 | * The class name to be registered as a subscriber. 229 | * @param null|string $service 230 | * The name of a service in the container. If not specified, it's assumed to be the same as the class. 231 | */ 232 | public function addSubscriber(string $class, ?string $service = null): void; 233 | } 234 | -------------------------------------------------------------------------------- /src/ProviderBuilder.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | protected array $optimizedEvents = []; 18 | 19 | /** 20 | * Pre-specify an event class that should have an optimized listener list built. 21 | * 22 | * @param class-string ...$events 23 | */ 24 | public function optimizeEvents(string ...$events): void 25 | { 26 | $this->optimizedEvents = [...$this->optimizedEvents, ...$events]; 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function getOptimizedEvents(): array 33 | { 34 | return $this->optimizedEvents; 35 | } 36 | 37 | public function listenerService( 38 | string $service, 39 | ?string $method = null, 40 | ?string $type = null, 41 | ?int $priority = null, 42 | array $before = [], 43 | array $after = [], 44 | ?string $id = null 45 | ): string { 46 | $method ??= $this->deriveMethod($service); 47 | 48 | if (!$type) { 49 | if (!class_exists($service)) { 50 | throw ServiceRegistrationClassNotExists::create($service); 51 | } 52 | // @phpstan-ignore-next-line 53 | $type = $this->getParameterType([$service, $method]); 54 | } 55 | 56 | $orderSpecified = !is_null($priority) || !empty($before) || !empty($after); 57 | 58 | // In the special case that the service is the class name, we can 59 | // leverage attributes. 60 | if (!$orderSpecified && class_exists($service)) { 61 | $listener = [$service, $method]; 62 | /** @var Listener $def */ 63 | $def = $this->classAnalyzer->analyze($service, Listener::class); 64 | $def = $def->methods[$method]; 65 | $id ??= $def?->id ?? $this->getListenerId($listener); 66 | 67 | $entry = new ListenerServiceEntry($service, $method, $type); 68 | return $this->listeners->add($entry, $id, priority: $def->priority, before: $def->before, after: $def->after); 69 | } 70 | 71 | $entry = new ListenerServiceEntry($service, $method, $type); 72 | $id ??= $service . '-' . $method; 73 | 74 | return $this->listeners->add($entry, $id, priority: $priority, before: $before, after: $after); 75 | } 76 | 77 | public function getIterator(): \Traversable 78 | { 79 | yield from $this->listeners; 80 | } 81 | 82 | protected function getListenerEntry(callable $listener, string $type): ListenerEntry 83 | { 84 | // We can't serialize a closure. 85 | if ($listener instanceof \Closure) { 86 | throw new \InvalidArgumentException('Closures cannot be used in a compiled listener provider.'); 87 | } 88 | // String means it's a function name, and that's safe. 89 | if (is_string($listener)) { 90 | return new ListenerFunctionEntry($listener, $type); 91 | } 92 | // This is how we recognize a static method call. 93 | if (is_array($listener) && isset($listener[0]) && is_string($listener[0])) { 94 | return new ListenerStaticMethodEntry($listener[0], $listener[1], $type); 95 | } 96 | // Anything else isn't safe to serialize, so reject it. 97 | throw new \InvalidArgumentException('That callable type cannot be used in a compiled listener provider.'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ProviderCollector.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected MultiOrderedCollection $listeners; 25 | 26 | public function __construct( 27 | protected readonly FunctionAnalyzer $funcAnalyzer = new MemoryCacheFunctionAnalyzer(new FuncAnalyzer()), 28 | protected readonly ClassAnalyzer $classAnalyzer = new MemoryCacheAnalyzer(new Analyzer()), 29 | ) { 30 | $this->listeners = new MultiOrderedCollection(); 31 | } 32 | 33 | public function listener( 34 | callable $listener, 35 | ?int $priority = null, 36 | array $before = [], 37 | array $after = [], 38 | ?string $id = null, 39 | ?string $type = null 40 | ): string { 41 | $orderSpecified = !is_null($priority) || !empty($before) || !empty($after); 42 | 43 | if (!$orderSpecified || !$type || !$id) { 44 | /** @var Listener $def */ 45 | $def = $this->getAttributeDefinition($listener); 46 | $id ??= $def?->id ?? $this->getListenerId($listener); 47 | $type ??= $def?->type ?? $this->getType($listener); 48 | 49 | // If any ordering is specified explicitly, that completely overrules any 50 | // attributes. 51 | if (!$orderSpecified) { 52 | $priority = $def->priority; 53 | $before = $def->before; 54 | $after = $def->after; 55 | } 56 | } 57 | 58 | $entry = $this->getListenerEntry($listener, $type); 59 | 60 | return $this->listeners->add( 61 | item: $entry, 62 | id: $id, 63 | priority: $priority, 64 | before: $before, 65 | after: $after 66 | ); 67 | } 68 | 69 | public function addListener(callable $listener, ?int $priority = null, ?string $id = null, ?string $type = null): string 70 | { 71 | return $this->listener($listener, priority: $priority, id: $id, type: $type); 72 | } 73 | 74 | public function addListenerBefore(string $before, callable $listener, ?string $id = null, ?string $type = null): string 75 | { 76 | return $this->listener($listener, before: [$before], id: $id, type: $type); 77 | } 78 | 79 | public function addListenerAfter(string $after, callable $listener, ?string $id = null, ?string $type = null): string 80 | { 81 | return $this->listener($listener, after: [$after], id: $id, type: $type); 82 | } 83 | 84 | public function addListenerService(string $service, string $method, string $type, ?int $priority = null, ?string $id = null): string 85 | { 86 | return $this->listenerService($service, $method, $type, priority: $priority, id: $id); 87 | } 88 | 89 | public function addListenerServiceBefore(string $before, string $service, string $method, string $type, ?string $id = null): string 90 | { 91 | return $this->listenerService($service, $method, $type, before: [$before], id: $id); 92 | } 93 | 94 | public function addListenerServiceAfter(string $after, string $service, string $method, string $type, ?string $id = null): string 95 | { 96 | return $this->listenerService($service, $method, $type, after: [$after], id: $id); 97 | } 98 | 99 | public function addSubscriber(string $class, ?string $service = null): void 100 | { 101 | $service ??= $class; 102 | 103 | // First allow manual registration through the proxy object. 104 | // This is deprecated. Please don't use it. 105 | $proxy = $this->addSubscribersByProxy($class, $service); 106 | 107 | $proxyRegisteredMethods = $proxy->getRegisteredMethods(); 108 | 109 | try { 110 | // Get all methods on the class, via AttributeUtils to handle reflection and caching. 111 | $methods = $this->classAnalyzer->analyze($class, Listener::class)->methods; 112 | 113 | /** 114 | * @var string $methodName 115 | * @var Listener $def 116 | */ 117 | foreach ($methods as $methodName => $def) { 118 | if (in_array($methodName, $proxyRegisteredMethods, true)) { 119 | // Exclude anything already registered by proxy. 120 | continue; 121 | } 122 | // If there was an attribute-based definition, that takes priority. 123 | if ($def->hasDefinition) { 124 | $this->listenerService($service, $methodName, $def->type, $def->priority, $def->before,$def->after, $def->id); 125 | } elseif (str_starts_with($methodName, 'on') && $def->paramCount === 1) { 126 | // Try to register it iff the method starts with "on" and has only one required parameter. 127 | // (More than one required parameter is guaranteed to fail when invoked.) 128 | if (!$def->type) { 129 | throw InvalidTypeException::fromClassCallable($class, $methodName); 130 | } 131 | $this->listenerService($service, $methodName, type: $def->type, id: $service . '-' . $methodName); 132 | } 133 | } 134 | } catch (\ReflectionException $e) { 135 | throw new \RuntimeException('Type error registering subscriber.', 0, $e); 136 | } 137 | } 138 | 139 | /** 140 | * @param class-string $class 141 | */ 142 | protected function addSubscribersByProxy(string $class, string $service): ListenerProxy 143 | { 144 | $proxy = new ListenerProxy($this, $service, $class); 145 | 146 | // Explicit registration is opt-in. 147 | if (in_array(SubscriberInterface::class, class_implements($class) ?: [], true)) { 148 | /** @var SubscriberInterface $class */ 149 | $class::registerListeners($proxy); 150 | } 151 | return $proxy; 152 | } 153 | 154 | /** 155 | * @param callable|array{0: class-string, 1: string}|array{0: object, 1: string} $listener 156 | */ 157 | protected function getAttributeDefinition(callable|array $listener): Listener 158 | { 159 | if ($this->isFunctionCallable($listener) || $this->isClosureCallable($listener)) { 160 | /** @var \Closure|string $listener */ 161 | return $this->funcAnalyzer->analyze($listener, Listener::class); 162 | } 163 | 164 | if ($this->isObjectCallable($listener)) { 165 | /** @var array{0: object, 1: string} $listener */ 166 | [$object, $method] = $listener; 167 | 168 | $def = $this->classAnalyzer->analyze($object::class, Listener::class); 169 | return $def->methods[$method]; 170 | } 171 | 172 | /** @var array{0: class-string, 1: string} $listener */ 173 | if ($this->isClassCallable($listener)) { 174 | /** @var array{0: class-string, 1: string} $listener */ 175 | [$class, $method] = $listener; 176 | 177 | $def = $this->classAnalyzer->analyze($class, Listener::class); 178 | return $def->staticMethods[$method]; 179 | } 180 | 181 | return new Listener(); 182 | } 183 | 184 | protected function deriveMethod(string $service): string 185 | { 186 | if (!class_exists($service)) { 187 | throw ServiceRegistrationClassNotExists::create($service); 188 | } 189 | $rClass = new \ReflectionClass($service); 190 | $rMethods = $rClass->getMethods(); 191 | 192 | // If the class has only one method, assume that's the listener. 193 | // Otherwise, use __invoke if not otherwise specified. 194 | // Otherwise, we cannot tell what to do so throw. 195 | return match (true) { 196 | count($rMethods) === 1 => $rMethods[0]->name, 197 | $rClass->hasMethod('__invoke') => '__invoke', 198 | default => throw ServiceRegistrationTooManyMethods::create($service), 199 | }; 200 | } 201 | 202 | /** 203 | * Tries to get the type of a callable listener. 204 | * 205 | * If unable, throws an exception with information about the listener whose type could not be fetched. 206 | * 207 | * @param callable $listener 208 | * The callable from which to extract a type. 209 | * 210 | * @return string 211 | * The type of the first argument. 212 | */ 213 | protected function getType(callable $listener): string 214 | { 215 | try { 216 | $type = $this->getParameterType($listener); 217 | } catch (\InvalidArgumentException $exception) { 218 | if ($this->isClassCallable($listener) || $this->isObjectCallable($listener)) { 219 | /** @var array{0: class-string, 1: string} $listener */ 220 | throw InvalidTypeException::fromClassCallable($listener[0], $listener[1], $exception); 221 | } 222 | if ($this->isFunctionCallable($listener) || $this->isClosureCallable($listener)) { 223 | throw InvalidTypeException::fromFunctionCallable($listener, $exception); 224 | } 225 | throw new InvalidTypeException($exception->getMessage(), $exception->getCode(), $exception); 226 | } 227 | return $type; 228 | } 229 | 230 | /** 231 | * Derives a predictable ID from the listener if possible. 232 | * 233 | * It's OK for this method to return null, as OrderedCollection will 234 | * generate a random ID if necessary. It will also handle duplicates 235 | * for us. This method is just a suggestion. 236 | * 237 | * @param callable|array{0: class-string, 1: string}|array{0: object, 1: string} $listener 238 | * The listener for which to derive an ID. 239 | * 240 | * @return string|null 241 | * The derived ID if possible or null if no reasonable ID could be derived. 242 | */ 243 | protected function getListenerId(callable|array $listener): ?string 244 | { 245 | if ($this->isFunctionCallable($listener)) { 246 | // Function callables are strings, so use that directly. 247 | /** @var string $listener */ 248 | return $listener; 249 | } 250 | 251 | if ($this->isObjectCallable($listener)) { 252 | /** @var array{0: object, 1: string} $listener */ 253 | return get_class($listener[0]) . '::' . $listener[1]; 254 | } 255 | 256 | if ($this->isClassCallable($listener)) { 257 | /** @var array{0: class-string, 1: string} $listener */ 258 | if ($listener[1] === '__invoke') { 259 | return $listener[0]; 260 | } 261 | return $listener[0] . '::' . $listener[1]; 262 | } 263 | 264 | // Anything else we can't derive an ID for logically. 265 | return null; 266 | } 267 | 268 | /** 269 | * Determines if a callable represents a function. 270 | * 271 | * Or at least a reasonable approximation, since a function name may not be defined yet. 272 | * 273 | * @param callable|array $callable 274 | * @return bool 275 | * True if the callable represents a function, false otherwise. 276 | */ 277 | protected function isFunctionCallable(callable|array $callable): bool 278 | { 279 | // We can't check for function_exists() because it may be included later by the time it matters. 280 | return is_string($callable); 281 | } 282 | 283 | /** 284 | * Determines if a callable represents a method on an object. 285 | * 286 | * @param callable|array $callable 287 | * @return bool 288 | * True if the callable represents a method object, false otherwise. 289 | */ 290 | protected function isObjectCallable(callable|array $callable): bool 291 | { 292 | return is_array($callable) && is_object($callable[0]); 293 | } 294 | 295 | /** 296 | * Determines if a callable represents a closure/anonymous function. 297 | * 298 | * @param callable|array $callable 299 | * @return bool 300 | * True if the callable represents a closure object, false otherwise. 301 | */ 302 | protected function isClosureCallable(callable|array $callable): bool 303 | { 304 | return $callable instanceof \Closure; 305 | } 306 | 307 | /** 308 | * Determines if a callable represents a static class method. 309 | * 310 | * The parameter here is untyped so that this method may be called with an 311 | * array that represents a class name and a non-static method. The routine 312 | * to determine the parameter type is identical to a static method, but such 313 | * an array is still not technically callable. Omitting the parameter type here 314 | * allows us to use this method to handle both cases. 315 | * 316 | * This method must therefore be called first above, as the array is not actually 317 | * an `is_callable()` and will fail `Closure::fromCallable()`. Because PHP. 318 | * 319 | * @param callable|array $callable 320 | * @return bool 321 | * True if the callable represents a static method, false otherwise. 322 | */ 323 | protected function isClassCallable($callable): bool 324 | { 325 | return is_array($callable) && is_string($callable[0]) && class_exists($callable[0]); 326 | } 327 | 328 | abstract protected function getListenerEntry(callable $listener, string $type): ListenerEntry; 329 | } 330 | -------------------------------------------------------------------------------- /src/ProviderCompiler.php: -------------------------------------------------------------------------------- 1 | createPreamble($class, $namespace)); 35 | 36 | $this->writeMainListenersList($listeners, $stream); 37 | 38 | $this->writeOptimizedList($listeners, $stream); 39 | 40 | fwrite($stream, $this->createClosing()); 41 | } 42 | 43 | /** 44 | * Compiles a provided ProviderBuilder to an anonymous class on disk. 45 | * 46 | * The generated class requires a container instance in its constructor, which 47 | * because it's anonymous has a pre-defined name of $container. That variable must 48 | * be in scope when the resulting file is require()ed/include()ed. The easiest way 49 | * to do that is to use the loadAnonymous() method of this class, but you may also 50 | * do so manually. 51 | * 52 | * @param ProviderBuilder $listeners 53 | * The set of listeners to compile. 54 | * @param resource $stream 55 | * A writeable stream to which to write the compiled class. 56 | */ 57 | public function compileAnonymous(ProviderBuilder $listeners, $stream): void 58 | { 59 | fwrite($stream, $this->createAnonymousPreamble()); 60 | 61 | $this->writeMainListenersList($listeners, $stream); 62 | 63 | $this->writeOptimizedList($listeners, $stream); 64 | 65 | fwrite($stream, $this->createAnonymousClosing()); 66 | } 67 | 68 | public function loadAnonymous(string $filename, ContainerInterface $container): ListenerProviderInterface 69 | { 70 | return require($filename); 71 | } 72 | 73 | /** 74 | * @param resource $stream 75 | * A writeable stream to which to write the compiled code. 76 | */ 77 | protected function writeMainListenersList(ProviderBuilder $listeners, $stream): void 78 | { 79 | fwrite($stream, $this->startMainListenersList()); 80 | 81 | /** @var CompileableListenerEntry $listenerEntry */ 82 | foreach ($listeners as $listenerEntry) { 83 | $item = $this->createEntry($listenerEntry); 84 | fwrite($stream, $item); 85 | } 86 | 87 | fwrite($stream, $this->endMainListenersList()); 88 | } 89 | 90 | /** 91 | * @param resource $stream 92 | * A writeable stream to which to write the compiled code. 93 | */ 94 | protected function writeOptimizedList(ProviderBuilder $listeners, $stream): void 95 | { 96 | fwrite($stream, $this->startOptimizedList()); 97 | 98 | $listenerDefs = iterator_to_array($listeners, false); 99 | 100 | foreach ($listeners->getOptimizedEvents() as $event) { 101 | $ancestors = $this->classAncestors($event); 102 | 103 | fwrite($stream, $this->startOptimizedEntry($event)); 104 | 105 | $relevantListeners = array_filter($listenerDefs, 106 | static fn(CompileableListenerEntry $entry) 107 | => in_array($entry->getProperties()['type'], $ancestors, true) 108 | ); 109 | 110 | /** @var CompileableListenerEntry $listenerEntry */ 111 | foreach ($relevantListeners as $listenerEntry) { 112 | $item = $this->createOptimizedEntry($listenerEntry); 113 | fwrite($stream, $item); 114 | } 115 | 116 | fwrite($stream, $this->endOptimizedEntry()); 117 | } 118 | 119 | fwrite($stream, $this->endOptimizedList()); 120 | } 121 | 122 | protected function startOptimizedEntry(string $event): string 123 | { 124 | return << [ 126 | END; 127 | } 128 | 129 | protected function endOptimizedEntry(): string 130 | { 131 | return <<<'END' 132 | ], 133 | END; 134 | } 135 | 136 | protected function createOptimizedEntry(CompileableListenerEntry $listenerEntry): string 137 | { 138 | $listener = $listenerEntry->getProperties(); 139 | $ret = match ($listener['entryType']) { 140 | ListenerFunctionEntry::class => "'{$listener['listener']}'", 141 | ListenerStaticMethodEntry::class => var_export([$listener['class'], $listener['method']], true), 142 | ListenerServiceEntry::class => sprintf('fn(object $event) => $this->container->get(\'%s\')->%s($event)', 143 | $listener['serviceName'], $listener['method']), 144 | default => throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', 145 | $listener['entryType'])), 146 | }; 147 | 148 | return $ret . ',' . PHP_EOL; 149 | } 150 | 151 | protected function createEntry(CompileableListenerEntry $listenerEntry): string 152 | { 153 | $listener = $listenerEntry->getProperties(); 154 | switch ($listener['entryType']) { 155 | case ListenerFunctionEntry::class: 156 | $ret = var_export(['type' => $listener['type'], 'callable' => $listener['listener']], true); 157 | break; 158 | case ListenerStaticMethodEntry::class: 159 | $ret = var_export(['type' => $listener['type'], 'callable' => [$listener['class'], $listener['method']]], true); 160 | break; 161 | case ListenerServiceEntry::class: 162 | $callable = sprintf('fn(object $event) => $this->container->get(\'%s\')->%s($event)', $listener['serviceName'], $listener['method']); 163 | $ret = << '{$listener['type']}', 166 | 'callable' => $callable, 167 | ] 168 | END; 169 | 170 | break; 171 | default: 172 | throw new \RuntimeException(sprintf('No such listener type found in compiled container definition: %s', $listener['entryType'])); 173 | } 174 | 175 | return $ret . ',' . PHP_EOL; 176 | } 177 | 178 | protected function createPreamble(string $class, string $namespace): string 179 | { 180 | return << 225 | */ 226 | protected function classAncestors(string $class, bool $includeClass = true): array 227 | { 228 | // These methods both return associative arrays, making + safe. 229 | /** @var array $ancestors */ 230 | $ancestors = (class_parents($class) ?: []) + (class_implements($class) ?: []); 231 | return $includeClass 232 | ? [$class => $class] + $ancestors 233 | : $ancestors 234 | ; 235 | } 236 | 237 | protected function startOptimizedList(): string 238 | { 239 | return <<optimized = [ 241 | 242 | END; 243 | } 244 | 245 | protected function endOptimizedList(): string 246 | { 247 | return <<<'END' 248 | ]; 249 | 250 | END; 251 | } 252 | 253 | protected function startMainListenersList(): string 254 | { 255 | return <<listeners = [ 257 | 258 | END; 259 | 260 | } 261 | 262 | protected function endMainListenersList(): string 263 | { 264 | return <<<'END' 265 | ]; 266 | 267 | END; 268 | } 269 | 270 | protected function createClosing(): string 271 | { 272 | return <<<'END' 273 | } // Close constructor 274 | } // Close class 275 | 276 | END; 277 | } 278 | 279 | protected function createAnonymousClosing(): string 280 | { 281 | return <<<'END' 282 | } // Close constructor 283 | }; // Close class 284 | 285 | END; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/ServiceRegistrationClassNotExists.php: -------------------------------------------------------------------------------- 1 | service = $service; 13 | $msg = 'Tukio can auto-detect the type and method for a listener service only if the service ID is a valid class name. The service "%s" does not exist. Please specify the $method and $type parameters explicitly, or check that you are using the right service name.'; 14 | 15 | $new->message = sprintf($msg, $service); 16 | 17 | return $new; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ServiceRegistrationTooManyMethods.php: -------------------------------------------------------------------------------- 1 | service = $service; 13 | $msg = 'Tukio can auto-detect a single method on a listener service, or use one named __invoke(). The "%s" service has too many methods not named __invoke(). Please check your class or use a subscriber.'; 14 | 15 | $new->message = sprintf($msg, $service); 16 | 17 | return $new; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/SubscriberInterface.php: -------------------------------------------------------------------------------- 1 |