├── .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 |