From 62a11670552ab8b68c70044c90ce2eaa17273c74 Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Thu, 2 Apr 2026 22:29:35 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + LICENSE | 309 +++ README.md | 63 + composer.json | 24 + composer.lock | 2049 ++++++++++++++++++++ doc/json-protos.md | 12 + doc/pack-ref.md | 34 + doc/recipes.md | 48 + doc/text-protos.md | 35 + examples/jsonproto.php | 13 + examples/jsonprotostream.php | 23 + examples/lineproto.php | 9 + examples/upgrading.php | 55 + phpstan.neon | 5 + phpunit.xml | 25 + src/Binary/BinaryProtocol.php | 142 ++ src/Json/JsonProtocol.php | 148 ++ src/Json/JsonRpcProtocol.php | 43 + src/Json/NativeMessagingProtocol.php | 28 + src/Line/HttpLikeProtocol.php | 87 + src/Line/LineProtocol.php | 64 + src/ProtocolException.php | 10 + src/ProtocolInterface.php | 50 + src/ProtocolStream.php | 72 + tests/Binary/BinaryProtocolTest.php | 92 + tests/Json/JsonProtocolTest.php | 109 ++ tests/Json/JsonRpcProtocolTest.php | 25 + tests/Json/NativeMessagingProtocolTest.php | 23 + tests/Line/HttpLikeProtocolTest.php | 45 + tests/Line/LineProtocolTest.php | 33 + tests/ProtocolStreamTest.php | 82 + 31 files changed, 3759 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 doc/json-protos.md create mode 100644 doc/pack-ref.md create mode 100644 doc/recipes.md create mode 100644 doc/text-protos.md create mode 100644 examples/jsonproto.php create mode 100644 examples/jsonprotostream.php create mode 100644 examples/lineproto.php create mode 100644 examples/upgrading.php create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 src/Binary/BinaryProtocol.php create mode 100644 src/Json/JsonProtocol.php create mode 100644 src/Json/JsonRpcProtocol.php create mode 100644 src/Json/NativeMessagingProtocol.php create mode 100644 src/Line/HttpLikeProtocol.php create mode 100644 src/Line/LineProtocol.php create mode 100644 src/ProtocolException.php create mode 100644 src/ProtocolInterface.php create mode 100644 src/ProtocolStream.php create mode 100644 tests/Binary/BinaryProtocolTest.php create mode 100644 tests/Json/JsonProtocolTest.php create mode 100644 tests/Json/JsonRpcProtocolTest.php create mode 100644 tests/Json/NativeMessagingProtocolTest.php create mode 100644 tests/Line/HttpLikeProtocolTest.php create mode 100644 tests/Line/LineProtocolTest.php create mode 100644 tests/ProtocolStreamTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36c79f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +/.phpunit.* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..322e434 --- /dev/null +++ b/LICENSE @@ -0,0 +1,309 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to most +of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for this service if you wish), +that you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs; and that you know you can +do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a +fee, you must give the recipients all the rights that you have. You must make +sure that they, too, receive or can get the source code. And you must show them +these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer +you this license which gives you legal permission to copy, distribute and/or +modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced by +others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish +to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's free +use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program or +work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included without +limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is not +restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as +you receive it, in any medium, provided that you conspicuously and appropriately +publish on each copy an appropriate copyright notice and disclaimer of warranty; +keep intact all the notices that refer to this License and to the absence of any +warranty; and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at +your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating +that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or +in part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + + c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under these +conditions, and telling the user how to view a copy of this License. (Exception: +if the Program itself is interactive but does not normally print such an +announcement, your work based on the Program is not required to print an +announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the entire whole, +and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on the +Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under Section +2) in object code or executable form under the terms of Sections 1 and 2 above +provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source +code, which must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to +give any third party, for a charge no more than your cost of physically +performing source distribution, a complete machine-readable copy of the +corresponding source code, to be distributed under the terms of Sections 1 and 2 +above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to +distribute corresponding source code. (This alternative is allowed only for +noncommercial distribution and only if you received the program in object code +or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all the +source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code distributed +need not include anything that is normally distributed (in either source or +binary form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component itself +accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source code +from the same place counts as distribution of the source code, even though third +parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or any +work based on the Program), you indicate your acceptance of this License to do +so, and all its terms and conditions for copying, distributing or modifying the +Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by third +parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement +or for any other reason (not limited to patent issues), conditions are imposed +on you (whether by court order, agreement or otherwise) that contradict the +conditions of this License, they do not excuse you from the conditions of this +License. If you cannot distribute so as to satisfy simultaneously your +obligations under this License and any other pertinent obligations, then as a +consequence you may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by all those +who receive copies directly or indirectly through you, then the only way you +could satisfy both it and this License would be to refrain entirely from +distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and the +section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In such +case, this License incorporates the limitation as if written in the body of this +License. + +9. The Free Software Foundation may publish revised and/or new versions of the +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that version +or of any later version published by the Free Software Foundation. If the +Program does not specify a version number of this License, you may choose any +version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE +PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED +IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS +IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT +NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE +PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, +SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY +TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF +THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use +to the public, the best way to achieve this is to make it free software which +everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright +(C) yyyy name of author + + This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the Free +Software Foundation; either version 2 of the License, or (at your option) any +later version. + + This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., 51 +Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information +on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes +with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, +and you are welcome to redistribute it under certain conditions; type `show c' +for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here is +a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program +`Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/README.md b/README.md new file mode 100644 index 0000000..9932eab --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Protocol Base for ReactPHP + +Use this library to build protocols. It doesn't do any single protocol to any greater +extent, but it provides callbacks and scaffolding for you to build and stream most +protocol data. + +## About the protocols + +This library is intended to be generic, so only protocols that have no external +dependencies will be included. + +## Using + +### Protocols + +```php +// Basic line-oriented protocol, think shell or SMTP +$proto = new LineProtocol($stream, [ + 'lineSeparator' => "\n", + 'fieldSeparator' => " ", + 'fieldQuote' => '"', +]); + +// encode your frames and send it however you desire +$data = $proto->packFrame([ "mv", "foo.txt", "bar.txt" ]); +// → mv foo.txt bar.txt\n + +// Or use JSON if you so desire +$proto = new JsonProtocol($stream, [ + 'frameSeparator' => "\0", +]); +$data = $proto->packFrame([ 'foo' => 'bar' ]); +// → {"foo":"bar"}\0 +``` + +Unpacking works as expected: + +```php +$proto = new LineProtocol($stream, [ + 'lineSeparator' => "\n", + 'fieldSeparator' => " ", + 'fieldQuote' => '"', +]); + +// encode your frames and send it however you desire +$cmdline = $proto->unpackFrame("mv foo.txt bar.txt\n" ]); +// → [ "mv", "foo.txt", "bar.txt" ] +``` + +### Protocols with ProtocolStream + +ProtocolStream combines a ProtocolInterface and a DuplexStreamInterface into +one package. It will emit an `message` event when a new frame has been +decoded, an `error` event if something goes wrong, and an `overflow` event +if the buffer overflows a configured max size. + +```php +$stream = new ProtocolStream($proto, $someduplexstream); +$stream->on("message", function (array $message) { + // ... +}); +$stream->send([ 'data' => 'here' ]); +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..13194b3 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "noccylabs/react-protocol", + "description": "Generic wire protocols for ReactPHP applications", + "type": "library", + "license": "GPL-2.0-or-later", + "autoload": { + "psr-4": { + "NoccyLabs\\React\\Protocol\\": "src/" + } + }, + "authors": [ + { + "name": "Christopher Vagnetoft", + "email": "labs@noccy.com" + } + ], + "require": { + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0", + "phpstan/phpstan": "^2.1" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..9b5b926 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2049 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4ad8f04716c950de93536db70a2a0a75", + "packages": [ + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2025-11-17T20:46:25+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "2.1.46", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-04-01T09:25:14+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "13.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "a8b58fde2f4fbc69a064e1f80ff917607cf7737c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/a8b58fde2f4fbc69a064e1f80ff917607cf7737c", + "reference": "a8b58fde2f4fbc69a064e1f80ff917607cf7737c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.4", + "phpunit/php-file-iterator": "^7.0", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.0", + "sebastian/lines-of-code": "^5.0", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/13.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-02-06T06:05:15+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:33:26+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:36:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "13.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9e426f7282c313c9138eeb9f25461e1a6be1e647" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9e426f7282c313c9138eeb9f25461e1a6be1e647", + "reference": "9e426f7282c313c9138eeb9f25461e1a6be1e647", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^13.0.1", + "phpunit/php-file-iterator": "^7.0.0", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.0", + "sebastian/comparator": "^8.0.0", + "sebastian/diff": "^8.0.0", + "sebastian/environment": "^9.1.0", + "sebastian/exporter": "^8.0.0", + "sebastian/global-state": "^9.0.0", + "sebastian/object-enumerator": "^8.0.0", + "sebastian/recursion-context": "^8.0.0", + "sebastian/type": "^7.0.0", + "sebastian/version": "^7.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.0-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.0.6" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-03-31T06:44:39+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/48a4654fa5e48c1c81214e9930048a572d4b23ca", + "reference": "48a4654fa5e48c1c81214e9930048a572d4b23ca", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:39:44+00:00" + }, + { + "name": "sebastian/comparator", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "29b232ddc29c2b114c0358c69b3084e7c3da0d58" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/29b232ddc29c2b114c0358c69b3084e7c3da0d58", + "reference": "29b232ddc29c2b114c0358c69b3084e7c3da0d58", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^8.0", + "sebastian/exporter": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:40:39+00:00" + }, + { + "name": "sebastian/complexity", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00" + }, + { + "name": "sebastian/diff", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2b6d09d7729ee87d605a439469f9dcc39be5ea3", + "reference": "a2b6d09d7729ee87d605a439469f9dcc39be5ea3", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:42:27+00:00" + }, + { + "name": "sebastian/environment", + "version": "9.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "c4a2dc54b1a24e13ef1839cbb5947b967cbae853" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/c4a2dc54b1a24e13ef1839cbb5947b967cbae853", + "reference": "c4a2dc54b1a24e13ef1839cbb5947b967cbae853", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-03-22T06:31:50+00:00" + }, + { + "name": "sebastian/exporter", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea", + "reference": "dc31f1f8e0186c8f0bb3e48fd4d51421d8905fea", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:44:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e52e3dc22441e6218c710afe72c3042f8fc41ea7", + "reference": "e52e3dc22441e6218c710afe72c3042f8fc41ea7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:45:13+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/4f21bb7768e1c997722ccc7efb1d6b5c11bfd471", + "reference": "4f21bb7768e1c997722ccc7efb1d6b5c11bfd471", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:45:54+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:46:36+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:47:13+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/74c5af21f6a5833e91767ca068c4d3dfec15317e", + "reference": "74c5af21f6a5833e91767ca068c4d3dfec15317e", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:51:28+00:00" + }, + { + "name": "sebastian/type", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "42412224607bd3931241bbd17f38e0f972f5a916" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/42412224607bd3931241bbd17f38e0f972f5a916", + "reference": "42412224607bd3931241bbd17f38e0f972f5a916", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:52:09+00:00" + }, + { + "name": "sebastian/version", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:52:52+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/doc/json-protos.md b/doc/json-protos.md new file mode 100644 index 0000000..947d237 --- /dev/null +++ b/doc/json-protos.md @@ -0,0 +1,12 @@ +# JSON Protocols + +The JSON protocol simply serializes + + + + +## Available sub-formats + +### JsonRpcProtocol + +### NativeMessagingProtocol diff --git a/doc/pack-ref.md b/doc/pack-ref.md new file mode 100644 index 0000000..6f620b1 --- /dev/null +++ b/doc/pack-ref.md @@ -0,0 +1,34 @@ +# PHP pack/unpack reference + +| Code | Description | +| ---- | ------------------------------------------------------------ | +| a | NUL-padded string | +| A | SPACE-padded string | +| h | Hex string, low nibble first | +| H | Hex string, high nibble first | +| c | signed char | +| C | unsigned char | +| s | signed short (always 16 bit, machine byte order) | +| S | unsigned short (always 16 bit, machine byte order) | +| n | unsigned short (always 16 bit, big endian byte order) | +| v | unsigned short (always 16 bit, little endian byte order) | +| i | signed integer (machine dependent size and byte order) | +| I | unsigned integer (machine dependent size and byte order) | +| l | signed long (always 32 bit, machine byte order) | +| L | unsigned long (always 32 bit, machine byte order) | +| N | unsigned long (always 32 bit, big endian byte order) | +| V | unsigned long (always 32 bit, little endian byte order) | +| q | signed long long (always 64 bit, machine byte order) | +| Q | unsigned long long (always 64 bit, machine byte order) | +| J | unsigned long long (always 64 bit, big endian byte order) | +| P | unsigned long long (always 64 bit, little endian byte order) | +| f | float (machine dependent size and representation) | +| g | float (machine dependent size, little endian byte order) | +| G | float (machine dependent size, big endian byte order) | +| d | double (machine dependent size and representation) | +| e | double (machine dependent size, little endian byte order) | +| E | double (machine dependent size, big endian byte order) | +| x | NUL byte | +| X | Back up one byte | +| Z | NUL-terminated (ASCIIZ) string, will be NUL padded | +| @ | NUL-fill to absolute position | diff --git a/doc/recipes.md b/doc/recipes.md new file mode 100644 index 0000000..90939cc --- /dev/null +++ b/doc/recipes.md @@ -0,0 +1,48 @@ +# Recipes + +These are some suggestions and ideas for when building your own protocols using the +various protocol classes. + +## Marshalling objects + +You can use the beforePackCb and afterUnpackCb to serialize and unserialize parameters +as desired. Consider the security implications of this before you do anything stupid +though, as a spoofed frame could have unexpected consequences when unserialized. + +```php +$proto = new JsonProtocol( + beforePackCb: function (array $frame): array { + $frame['obj'] = serialize($frame['obj']); + return $frame; + }, + afterUnpackCb: function (array $frame): array { + $frame['obj'] = unserialize($frame['obj]); + return $frame; + }, +); +``` + +## Building line-based messages + +By using the `beforePackCb` callback, you can take messages in a structured format +and return them as a list for line-based protocols. You can do the same in reverse +with the `afterUnpackCb` callback. + +```php +$proto = new LineProtocol( + beforePackCb: function (array $frame): array { + switch ($frame['cmd']) { + case 'search': + return [ "search", $frame['query'] ]; + // .. + } + }, + afterUnpackCb: function (array $frame): array { + switch ($frame[0]) { + case 'search': + return [ 'cmd' => $frame[0], 'query' => $frame[1] ]; + // .. + } + } +) +``` \ No newline at end of file diff --git a/doc/text-protos.md b/doc/text-protos.md new file mode 100644 index 0000000..791f8db --- /dev/null +++ b/doc/text-protos.md @@ -0,0 +1,35 @@ +# Text/Line Protocols + + + + + +## Available sub-protocols + +### HttpLikeProtocol + +This is as the name says a HTTP-like parser. It can be used for HTTP, STOMP +and more. All messages are expected to follow the following style: + +```text +query-line <$lineSeparator> +header-line* <$lineSeparator> +<$lineSeparator> +optional-body +``` + +Line separators default to `\n`, but can be set to any sequence or the value `true` +to attempt to automatically detect the endings used. + +The presence of a body is determined by consulting the headers. The `$contentLengthHeader` +constructor parameter defaults to 'content-length', and can be set to null to disable parsing +of bodies entirely. + +```php +$proto = new HttpLikeProtocol( + queryFormat: '{command} {path}', + lineSeparator: true, +) + + +``` \ No newline at end of file diff --git a/examples/jsonproto.php b/examples/jsonproto.php new file mode 100644 index 0000000..f7aca75 --- /dev/null +++ b/examples/jsonproto.php @@ -0,0 +1,13 @@ +packFrame([ 'hello'=>'world', 'answer'=>42 ]); \ No newline at end of file diff --git a/examples/jsonprotostream.php b/examples/jsonprotostream.php new file mode 100644 index 0000000..6f6ae8e --- /dev/null +++ b/examples/jsonprotostream.php @@ -0,0 +1,23 @@ +send([ 'hello' => 'world' ]); +$stream->send([ 'foo' => 'bar', 'answer' => 42 ]); + diff --git a/examples/lineproto.php b/examples/lineproto.php new file mode 100644 index 0000000..78ee60c --- /dev/null +++ b/examples/lineproto.php @@ -0,0 +1,9 @@ +packFrame([ 'hello', 'world' ]); diff --git a/examples/upgrading.php b/examples/upgrading.php new file mode 100644 index 0000000..2deae9b --- /dev/null +++ b/examples/upgrading.php @@ -0,0 +1,55 @@ +on("message", function (array $msg) use ($stream, $jsonProto) { + if (array_is_list($msg)) { + if (reset($msg) == 'upgrade') { + $stream->send([ 'OK', 'Upgrading to JSON protocol']); + $stream->upgrade($jsonProto); + $stream->send([ 'info' => 'Now using JSON proto' ]); + } else { + $stream->send([ 'echo', json_encode($msg)]); + } + } else { + $stream->send([ 'echo' => $msg ]); + } +}); \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..0a40905 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 5 + paths: + - src + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ce0df9c --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,25 @@ + + + + + tests + + + + + + src + + + diff --git a/src/Binary/BinaryProtocol.php b/src/Binary/BinaryProtocol.php new file mode 100644 index 0000000..5b358ec --- /dev/null +++ b/src/Binary/BinaryProtocol.php @@ -0,0 +1,142 @@ +beforePackCb)) + $frame = call_user_func($this->beforePackCb, $frame); + + $data = $frame['payload']; + + // append separator + $data = $data . $this->frameSeparator; + + // prepend size + if ($this->prependSizeBytes > 0) { + $data = $this->packSizeBytes(strlen($data)) . $data; + } + + if (is_callable($this->afterPackCb)) + $data = call_user_func($this->afterPackCb, $data); + + return $data; + } + + public function unpackFrame(string $data): array + { + if (is_callable($this->beforeUnpackCb)) + $data = call_user_func($this->beforeUnpackCb, $data); + + if ($this->prependSizeBytes > 0) { + if ($this->prependSizeBytes > strlen($data)) { + // not enough data to parse size... + throw new ProtocolException("BinaryProtocol: Not enough data to parse size"); + } + $len = $this->unpackSizeBytes($data); + if ($len <= 0) { + // unparsable? + throw new ProtocolException("BinaryProtocol: Invalid size decoded from frame"); + } + if ($len > strlen($data) - $this->prependSizeBytes) { + // insufficient data + throw new ProtocolException("BinaryProtocol: Insufficient data for unpacking (want {$len} but got ".(strlen($data)).")"); + } + $data = substr($data, $this->prependSizeBytes); + } + + if ($this->frameSeparator) { + $data = substr($data, 0, -strlen($this->frameSeparator)); + } + + $frame = [ 'payload' => $data ]; + // echo "[{$data}]"; var_dump($frame); + if (!$frame) { + // invalid json + throw new ProtocolException("Unparsable frame received: {$data}"); + } + + if (is_callable($this->afterUnpackCb)) + $frame = call_user_func($this->afterUnpackCb, $frame); + + return $frame; + } + + public function consumeFrame(string &$data): ?array + { + // check for $this->prependSizeBytes + if ($this->prependSizeBytes > 0) { + $len = $this->unpackSizeBytes($data); + // if size is greater than data (i.e. incomplete) + if ($len > strlen($data) - $this->prependSizeBytes) return null; + $p = $len; + // $data = substr($data, $this->prependSizeBytes); + } elseif ($this->frameSeparator) { + // check for $this->frameSeparator + $p = strpos($data, $this->frameSeparator); + if ($p === false) { + return null; + } + } + + $frame = substr($data, 0, $p + strlen($this->frameSeparator) + $this->prependSizeBytes); + $data = substr($data, $p + strlen($this->frameSeparator) + $this->prependSizeBytes); + + return $this->unpackFrame($frame); + } + + public function packSizeBytes(int $size): string + { + $endian = function($b,$l,$s) { + return match ($this->prependSizeEndian) { + 'b' => $b, + 'l' => $l, + default => $s + }; + }; + return match ($this->prependSizeBytes) { + 1 => pack($endian("C","C","C"), $size), + 2 => pack($endian("n","v","S"), $size), + 4 => pack($endian("N","V","L"), $size), + default => throw new ProtocolException("BinaryProtocol: Invalid message size length") + }; + } + + public function unpackSizeBytes(string $data): int + { + $bytes = substr($data, 0, $this->prependSizeBytes); + $endian = function($b,$l,$s) { + return match ($this->prependSizeEndian) { + 'b' => $b, + 'l' => $l, + default => $s + }; + }; + return match ($this->prependSizeBytes) { + 1 => unpack($endian("C","C","C")."len", $bytes)['len'], + 2 => unpack($endian("n","v","S")."len", $bytes)['len'], + 4 => unpack($endian("N","V","L")."len", $bytes)['len'], + default => throw new ProtocolException("BinaryProtocol: Invalid message size length") + }; + } + +} \ No newline at end of file diff --git a/src/Json/JsonProtocol.php b/src/Json/JsonProtocol.php new file mode 100644 index 0000000..fafa32b --- /dev/null +++ b/src/Json/JsonProtocol.php @@ -0,0 +1,148 @@ +beforePackCb)) + $frame = call_user_func($this->beforePackCb, $frame); + + $jsonOpts = ($this->unescapedSlashes?\JSON_UNESCAPED_SLASHES:0); + $data = @json_encode($frame, $jsonOpts); + if (!$data) { + throw new ProtocolException("JsonProtocol: Empty data after serializing"); + } + + // append separator + $data = $data . $this->frameSeparator; + + // prepend size + if ($this->prependSizeBytes > 0) { + $data = $this->packSizeBytes(strlen($data)) . $data; + } + + if (is_callable($this->afterPackCb)) + $data = call_user_func($this->afterPackCb, $data); + + return $data; + } + + public function unpackFrame(string $data): array + { + if (is_callable($this->beforeUnpackCb)) + $data = call_user_func($this->beforeUnpackCb, $data); + + if ($this->prependSizeBytes > 0) { + if ($this->prependSizeBytes > strlen($data)) { + // not enough data to parse size... + throw new ProtocolException("JsonProtocol: Not enough data to parse size"); + } + $len = $this->unpackSizeBytes($data); + if ($len <= 0) { + // unparsable? + throw new ProtocolException("JsonProtocol: Invalid size decoded from frame"); + } + if ($len > strlen($data) - $this->prependSizeBytes) { + // insufficient data + throw new ProtocolException("JsonProtocol: Insufficient data for unpacking"); + } + $data = substr($data, $this->prependSizeBytes); + } + + if ($this->frameSeparator) { + $data = substr($data, 0, -strlen($this->frameSeparator)); + } + + $frame = @json_decode($data, true); + // echo "[{$data}]"; var_dump($frame); + if (!$frame) { + // invalid json + throw new ProtocolException("Unparsable frame received: {$data}"); + } + + if (is_callable($this->afterUnpackCb)) + $frame = call_user_func($this->afterUnpackCb, $frame); + + return $frame; + } + + public function consumeFrame(string &$data): ?array + { + // check for $this->prependSizeBytes + if ($this->prependSizeBytes > 0) { + $len = $this->unpackSizeBytes($data); + // if size is greater than data (i.e. incomplete) + if ($len > strlen($data) - $this->prependSizeBytes) return null; + $p = $len; + // $data = substr($data, $this->prependSizeBytes); + } elseif ($this->frameSeparator) { + // check for $this->frameSeparator + $p = strpos($data, $this->frameSeparator); + if ($p === false) { + return null; + } + } + + $frame = substr($data, 0, $p + strlen($this->frameSeparator) + $this->prependSizeBytes); + $data = substr($data, $p + strlen($this->frameSeparator) + $this->prependSizeBytes); + + return $this->unpackFrame($frame); + } + + public function packSizeBytes(int $size): string + { + $endian = function($b,$l,$s) { + return match ($this->prependSizeEndian) { + 'b' => $b, + 'l' => $l, + default => $s + }; + }; + return match ($this->prependSizeBytes) { + 1 => pack($endian("C","C","C"), $size), + 2 => pack($endian("n","v","S"), $size), + 4 => pack($endian("N","V","L"), $size), + default => throw new ProtocolException("JsonProtocol: Invalid message size length") + }; + } + + public function unpackSizeBytes(string $data): int + { + $bytes = substr($data, 0, $this->prependSizeBytes); + $endian = function($b,$l,$s) { + return match ($this->prependSizeEndian) { + 'b' => $b, + 'l' => $l, + default => $s + }; + }; + return match ($this->prependSizeBytes) { + 1 => unpack($endian("C","C","C")."len", $bytes)['len'], + 2 => unpack($endian("n","v","S")."len", $bytes)['len'], + 4 => unpack($endian("N","V","L")."len", $bytes)['len'], + default => throw new ProtocolException("JsonProtocol: Invalid message size length") + }; + } +} + diff --git a/src/Json/JsonRpcProtocol.php b/src/Json/JsonRpcProtocol.php new file mode 100644 index 0000000..7795521 --- /dev/null +++ b/src/Json/JsonRpcProtocol.php @@ -0,0 +1,43 @@ +beforePack(...), + afterPackCb: null, + beforeUnpackCb: null, + afterUnpackCb: $this->afterUnpack(...) + ); + } + + private function beforePack(array $frame): array + { + $frame['jsonrpc'] ??= "2.0"; + if (!(isset($frame['method']) || isset($frame['result']))) { + throw new ProtocolException("JsonRpcProtocol: Either method or result key must be present"); + } + return $frame; + } + + private function afterUnpack(array $frame): array + { + return $frame; + } +} \ No newline at end of file diff --git a/src/Json/NativeMessagingProtocol.php b/src/Json/NativeMessagingProtocol.php new file mode 100644 index 0000000..59c738a --- /dev/null +++ b/src/Json/NativeMessagingProtocol.php @@ -0,0 +1,28 @@ +beforePackCb)) + $frame = call_user_func($this->beforePackCb, $frame); + + $headers = []; + foreach ($frame['headers']??[] as $header => $values) { + foreach ($values as $value) { + $headers[] = sprintf("%s: %s", $header, $value); + } + } + + if (is_callable($this->afterPackCb)) + $frame = call_user_func($this->afterPackCb, $frame); + + return join($this->lineSeparator, [ + $frame['query'], + ...$headers, + null, + null, + ]); + } + + public function unpackFrame(string $data): array + { + if (is_callable($this->beforeUnpackCb)) + $data = call_user_func($this->beforeUnpackCb, $data); + + $lines = explode($this->lineSeparator, $data); + $query = array_shift($lines); + $headers = []; + foreach ($lines as $line) { + if (!trim($line) && $line == end($lines)) continue; + $line = trim($line); + if (!str_contains($line, ":")) { + throw new ProtocolException("Invalid header line: {$line}"); + } + [$header, $value] = array_map(trim(...), explode(":", $line, 2)); + if (!array_key_exists($header, $headers)) $headers[$header] = []; + $headers[$header][] = $value; + } + + $frame = [ + 'query' => $query, + 'headers' => $headers, + ]; + + if (is_callable($this->afterUnpackCb)) + $frame = call_user_func($this->afterUnpackCb, $frame); + + return $frame; + } + + public function consumeFrame(string &$data): ?array + { + if ($this->wantedPayloadSize > 0) { + // consume and emit a "payload" event + } + return parent::consumeFrame($data); + } +} \ No newline at end of file diff --git a/src/Line/LineProtocol.php b/src/Line/LineProtocol.php new file mode 100644 index 0000000..2fcb953 --- /dev/null +++ b/src/Line/LineProtocol.php @@ -0,0 +1,64 @@ +beforePackCb)) + $frame = call_user_func($this->beforePackCb, $frame); + + if ($this->escapeSpecial) { + $frame = array_map(quotemeta(...), $frame); + } + + if ($this->quoteStrings) { + $frame = array_map(fn($v) => is_string($v) ? ('"'.str_replace('"',"\\\"",$v).'"') : $v, $frame); + } + + $data = join(" ", $frame); + $data .= $this->lineBreak; + + if (is_callable($this->afterPackCb)) + $data = call_user_func($this->afterPackCb, $data); + + return $data; + } + + public function unpackFrame(string $data): array + { + return str_getcsv($data, ' ', '"', "\\"); + } + + public function consumeFrame(string &$data): ?array + { + // check for $this->lineBreak + $p = strpos($data, $this->lineBreak); + if ($p === false) { + return null; + } + + // break on separator + $frame = substr($data, 0, $p); + $data = substr($data, $p + strlen($this->lineBreak)); + + return $this->unpackFrame($frame); + } +} diff --git a/src/ProtocolException.php b/src/ProtocolException.php new file mode 100644 index 0000000..faebee5 --- /dev/null +++ b/src/ProtocolException.php @@ -0,0 +1,10 @@ +protocol = $protocol; + $stream->on("data", $this->receive(...)); + } + + /** + * Upgrade the protocol, switching from f.ex. a text based to a JSON + * based protocol without having to re-create the ProtocolStream. + * + * @param ProtocolInterface $protocol + * @return void + */ + public function upgrade(ProtocolInterface $protocol): void + { + $this->protocol = $protocol; + } + + /** + * Undocumented function + * + * @param array $frame + * @return void + */ + public function send(array $frame): void + { + $data = $this->protocol->packFrame($frame); + $this->stream->write($data); + } + + private function receive(string $data): void + { + $this->readBuffer .= $data; + + try { + while ($frame = $this->protocol->consumeFrame($this->readBuffer)) { + $this->emit("message", [ $frame ]); + } + } catch (ProtocolException $e) { + $this->emit("error", [ $e ]); + return; + } + + if (strlen($this->readBuffer) > $this->maxBuffer) { + $this->emit("overflow"); + if ($this->closeOnOverflow) { + $this->stream->close(); + } + } + } +} \ No newline at end of file diff --git a/tests/Binary/BinaryProtocolTest.php b/tests/Binary/BinaryProtocolTest.php new file mode 100644 index 0000000..e18e02c --- /dev/null +++ b/tests/Binary/BinaryProtocolTest.php @@ -0,0 +1,92 @@ +packFrame([ 'payload' => 'foobar' ]); + $this->assertEquals("foobar\0", $res1); + + $proto = new BinaryProtocol( + frameSeparator: "", + prependSizeBytes: 2, + ); + $res1 = $proto->packFrame([ 'payload' => 'foobar' ]); + $this->assertEquals("\x06\x00foobar", $res1); + + } + + public function testUnpackingFrames() + { + $proto = new BinaryProtocol( + frameSeparator: "\0" + ); + $res1 = $proto->unpackFrame("foobar\0"); + $this->assertEquals([ 'payload' => 'foobar' ], $res1); + + $proto = new BinaryProtocol( + frameSeparator: "", + prependSizeBytes: 2, + ); + $res1 = $proto->unpackFrame("\x06\x00foobar"); + $this->assertEquals([ 'payload' => 'foobar' ], $res1); + + } + + public function testUnpackingIncompleteFrames() + { + $proto = new BinaryProtocol( + frameSeparator: "\0" + ); + $res1 = $proto->unpackFrame("foobar\0"); + $this->assertEquals([ 'payload' => 'foobar' ], $res1); + + $proto = new BinaryProtocol( + frameSeparator: "", + prependSizeBytes: 2, + ); + + $this->expectException(ProtocolException::class); + $res0 = $proto->unpackFrame("\x06"); + $this->expectException(ProtocolException::class); + $res1 = $proto->unpackFrame("\x00foo"); + $res2 = $proto->unpackFrame("bar"); + + $this->assertNull($res1); + $this->assertEquals([ 'payload' => 'foobar' ], $res2); + } + + public function testConsumingFrames() + { + $proto = new BinaryProtocol( + frameSeparator: "\0" + ); + $res1 = $proto->unpackFrame("foobar\0"); + $this->assertEquals([ 'payload' => 'foobar' ], $res1); + + $proto = new BinaryProtocol( + frameSeparator: "", + prependSizeBytes: 2, + ); + + $buffer = "\x06\x00foobar\x06\x00foobaz"; + + $res1 = $proto->consumeFrame($buffer); + $res2 = $proto->consumeFrame($buffer); + + $this->assertEquals([ 'payload' => 'foobar' ], $res1); + $this->assertEquals([ 'payload' => 'foobaz' ], $res2); + + } + +} \ No newline at end of file diff --git a/tests/Json/JsonProtocolTest.php b/tests/Json/JsonProtocolTest.php new file mode 100644 index 0000000..3b7aee0 --- /dev/null +++ b/tests/Json/JsonProtocolTest.php @@ -0,0 +1,109 @@ +packFrame(['foo' => 'bar']); + $this->assertEquals("{\"foo\":\"bar\"}\0", $packed); + + $proto = new JsonProtocol("\n"); + $packed = $proto->packFrame(['foo' => 'bar']); + $this->assertEquals("{\"foo\":\"bar\"}\n", $packed); + + } + + public function testFrameSizePrefixes() + { + $proto = new JsonProtocol(prependSizeBytes: 1); + $packed = $proto->packFrame(['foo' => 'bar']); + $this->assertEquals("\x0E{\"foo\":\"bar\"}\0", $packed); + + $proto = new JsonProtocol(prependSizeBytes: 2, prependSizeEndian: 'l'); + $packed = $proto->packFrame(['foo' => 'bar']); + $this->assertEquals("\x0E\x00{\"foo\":\"bar\"}\0", $packed); + + $proto = new JsonProtocol(prependSizeBytes: 2, prependSizeEndian: 'b'); + $packed = $proto->packFrame(['foo' => 'bar']); + $this->assertEquals("\x00\x0E{\"foo\":\"bar\"}\0", $packed); + + $proto = new JsonProtocol(prependSizeBytes: 4, prependSizeEndian: 'l'); + $packed = $proto->packFrame(['foo' => 'bar']); + $this->assertEquals("\x0E\x00\x00\x00{\"foo\":\"bar\"}\0", $packed); + + $proto = new JsonProtocol(prependSizeBytes: 4, prependSizeEndian: 'b'); + $packed = $proto->packFrame(['foo' => 'bar']); + $this->assertEquals("\x00\x00\x00\x0E{\"foo\":\"bar\"}\0", $packed); + + } + + public function testPackingCallbacks() + { + $proto = new JsonProtocol( + prependSizeBytes: 0, + beforePackCb: function (array $data) { + $data['a'] = true; + return $data; + }, + afterPackCb: function (string $data) { + return $data."a"; + }, + ); + $packed = $proto->packFrame(['foo' => 'bar']); + $this->assertEquals("{\"foo\":\"bar\",\"a\":true}\0a", $packed); + } + + public function testUnpackingCallbacks() + { + $proto = new JsonProtocol( + prependSizeBytes: 0, + beforeUnpackCb: function (string $data) { + return strtolower($data); + }, + afterUnpackCb: function (array $data) { + unset($data['a']); + return $data; + }, + ); + $unpacked = $proto->unpackFrame("{\"FOO\":\"bar\",\"a\":true}\0"); + $expect = ['foo' => 'bar']; + $this->assertEquals($expect, $unpacked); + } + + public function testConsumingFrames() + { + $proto = new JsonProtocol( + frameSeparator: "\n" + ); + + $buffer = '{"foo":true}'."\n".'{"bar":false}'."\n"; + $msg1 = $proto->consumeFrame($buffer); + $msg2 = $proto->consumeFrame($buffer); + + $this->assertEquals(['foo'=>true], $msg1); + $this->assertEquals(['bar'=>false], $msg2); + + $proto = new JsonProtocol( + frameSeparator: "\n", + prependSizeBytes: 4, + prependSizeEndian: 'l', + ); + + $buffer = "\x0c\x00\x00\x00".'{"foo":true}'."\n"."\x0d\x00\x00\x00".'{"bar":false}'."\n"; + $msg1 = $proto->consumeFrame($buffer); + $msg2 = $proto->consumeFrame($buffer); + + $this->assertEquals(['foo'=>true], $msg1); + $this->assertEquals(['bar'=>false], $msg2); + + } +} \ No newline at end of file diff --git a/tests/Json/JsonRpcProtocolTest.php b/tests/Json/JsonRpcProtocolTest.php new file mode 100644 index 0000000..905b016 --- /dev/null +++ b/tests/Json/JsonRpcProtocolTest.php @@ -0,0 +1,25 @@ +packFrame([ 'method' => 'foo', 'params' => [ 'bar' => true ] ]); + + $this->assertEquals('{"method":"foo","params":{"bar":true},"jsonrpc":"2.0"}'."\n", $packed); + } + +} \ No newline at end of file diff --git a/tests/Json/NativeMessagingProtocolTest.php b/tests/Json/NativeMessagingProtocolTest.php new file mode 100644 index 0000000..c764f09 --- /dev/null +++ b/tests/Json/NativeMessagingProtocolTest.php @@ -0,0 +1,23 @@ +packFrame([ 'method' => 'foo', 'params' => [ 'bar' => true ] ]); + + $this->assertEquals("\x26\x00\x00\x00".'{"method":"foo","params":{"bar":true}}', $packed); + } + +} \ No newline at end of file diff --git a/tests/Line/HttpLikeProtocolTest.php b/tests/Line/HttpLikeProtocolTest.php new file mode 100644 index 0000000..73920fb --- /dev/null +++ b/tests/Line/HttpLikeProtocolTest.php @@ -0,0 +1,45 @@ +packFrame(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]]); + $this->assertEquals("GET / HTTP/1.1\nfoo: bar\n\n", $packed); + } + + public function testUnpackingFrames() + { + $proto = new HttpLikeProtocol( + lineSeparator: "\n" + ); + + $msg1 = $proto->unpackFrame("GET / HTTP/1.1\nfoo: bar\n\n"); + $this->assertEquals(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]], $msg1); + } + + public function testConsumingFrames() + { + $proto = new HttpLikeProtocol( + lineSeparator: "\n" + ); + + $buffer = "GET / HTTP/1.1\nfoo: bar\n\n"; + $msg1 = $proto->consumeFrame($buffer); + // $msg2 = $proto->consumeFrame($buffer); + + $this->assertEquals(['query' => 'GET / HTTP/1.1', 'headers' => [ 'foo' => [ 'bar' ]]], $msg1); + // $this->assertEquals(['bar','false'], $msg2); + } +} \ No newline at end of file diff --git a/tests/Line/LineProtocolTest.php b/tests/Line/LineProtocolTest.php new file mode 100644 index 0000000..94daf07 --- /dev/null +++ b/tests/Line/LineProtocolTest.php @@ -0,0 +1,33 @@ +packFrame(['foo', 'bar']); + $this->assertEquals("foo bar\n", $packed); + } + + public function testConsumingFrames() + { + $proto = new LineProtocol( + lineBreak: "\n" + ); + + $buffer = "foo true\nbar false\n"; + $msg1 = $proto->consumeFrame($buffer); + $msg2 = $proto->consumeFrame($buffer); + + $this->assertEquals(['foo','true'], $msg1); + $this->assertEquals(['bar','false'], $msg2); + } +} \ No newline at end of file diff --git a/tests/ProtocolStreamTest.php b/tests/ProtocolStreamTest.php new file mode 100644 index 0000000..fad8dd7 --- /dev/null +++ b/tests/ProtocolStreamTest.php @@ -0,0 +1,82 @@ +on("data", function ($v) use (&$written) { $written = $v; }); + + $stream->send(["helloworld"]); + + $this->assertEquals("helloworld\n", $written); + + $cs->close(); + } + + public function testReceivingFrames() + { + $is = new ThroughStream(); + $os = new ThroughStream(); + $cs = new CompositeStream($is, $os); + $ws = new CompositeStream($os, $is); + + $proto = new LineProtocol(); + $stream = new ProtocolStream($cs, $proto); + + $read = null; + $stream->on("message", function ($v) use (&$read) { $read = $v; }); + + $ws->write("helloworld\n"); + + $this->assertEquals([ "helloworld" ], $read); + + $cs->close(); + } + + public function testPartialFrames() + { + $is = new ThroughStream(); + $os = new ThroughStream(); + $cs = new CompositeStream($is, $os); + $ws = new CompositeStream($os, $is); + + $proto = new JsonProtocol( + frameSeparator: "\n" + ); + $stream = new ProtocolStream($cs, $proto); + + $read = null; + $stream->on("message", function ($v) use (&$read) { $read = $v; }); + + $ws->write('{"hello":'); + $ws->write('"world"}'); + $ws->write("\n"); + + $this->assertEquals([ "hello" => "world" ], $read); + + $cs->close(); + + } + +} \ No newline at end of file