diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ff202ff --- /dev/null +++ b/.gitattributes @@ -0,0 +1,16 @@ +* text=auto + +/.github export-ignore +/bin export-ignore +/config export-ignore +/tests export-ignore +/var export-ignore +/.env export-ignore +/.env.test export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/composer.lock export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon export-ignore +/phpunit.xml.dist export-ignore +/symfony.lock export-ignore diff --git a/README.md b/README.md index 261820c..ba7deee 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,43 @@ See what features are covered and what aren't (yet) [here](#feature-coverage). PHP 8.1 is required to run the hub. +### As a standalone Mercure hub + +```bash +composer create-project freddie/mercure-x freddie && cd freddie +bin/freddie +``` + +This will start a Freddie instance on `127.0.0.1:8080`, with anonymous subscriptions enabled. + +You can publish updates to the hub by generating a valid JWT signed with the `!ChangeMe!` key with `HMAC SHA256` algorithm. + +To change these values, see [Security](#security). + +### As a bundle of your existing Symfony application + +```bash +composer req freddie/mercure-x +``` + +You can then start the hub by doing: + ```bash -composer create-project freddie/mercure-x:"~0.2" freddie -cd freddie +bin/console freddie:serve ``` +You can override relevant env vars in your `.env.local` +and services in your `config/services.yaml` as usual. + +Then, you can inject `Freddie\Hub\HubInterface` in your services so that you can call `$hub->publish($update)`, +or listening to dispatched updates in a CLI context 👍 + +Keep in mind this only works when using the Redis transport. + +⚠️ **Freddie** uses its own routing/authentication system (because of async / event loop). + +The controllers it exposes cannot be imported in your `routes.yaml`, and get out of your `security.yaml` scope. + ## Usage ```bash @@ -35,6 +67,8 @@ To change this address, use the `X_LISTEN` environment variable: X_LISTEN="0.0.0.0:8000" ./bin/freddie ``` +### Security + The default JWT key is `!ChangeMe!` with a `HS256` signature. You can set different values by changing the environment variables (in `.env.local` or at the OS level): @@ -42,10 +76,18 @@ You can set different values by changing the environment variables (in `.env.loc Please refer to the [authorization](https://mercure.rocks/spec#authorization) section of the Mercure specification to authenticate as a publisher and/or a subscriber. -### Redis transport +### PHP Transport (default) + +By default, the hub will run as a simple event-dispatcher, in a single PHP process. + +It can fit common needs for a basic usage, but using this transport prevents scalability, +as opening another process won't share the same event emitter. -By default, the hub will run as a simple event-dispatcher, in a single PHP process. -It can fit common needs for a basic usage, but is not scalable (opening another process won't share the same event emitter). +It's still prefectly usable as soon as : +- You don't expect more than a few hundreds updates per second +- Your application is served from a single server. + +### Redis transport On the other hand, you can launch the hub on **multiple ports** and/or **multiple servers** with a Redis transport (as soon as they share the same Redis instance), and optionally use a load-balancer to distribute the traffic. diff --git a/bin/freddie b/bin/freddie index 469ad39..8fe1727 100755 --- a/bin/freddie +++ b/bin/freddie @@ -15,7 +15,7 @@ require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return static function (array $context) { $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); $application = new Application($kernel); - $application->setDefaultCommand('serve'); + $application->setDefaultCommand('freddie:serve'); return $application; }; diff --git a/composer.json b/composer.json index bdbbadb..0c003ec 100644 --- a/composer.json +++ b/composer.json @@ -14,17 +14,17 @@ "ext-ctype": "*", "ext-iconv": "*", "bentools/querystring": "^1.1", - "clue/framework-x": "dev-main", + "clue/framework-x": "dev-main#74fafca31a1d3d4c6faa3bf9276a9d018cb587fa", "clue/redis-react": "^2.5", "doctrine/annotations": "^1.0", "lcobucci/jwt": "^4.1", "nyholm/dsn": "^2.0", "phpdocumentor/reflection-docblock": "^5.3", - "react/async": "dev-main", + "react/async": "dev-main#ff11a7aa9eea7104af8f05bafbc85422dac4b8ab", "rize/uri-template": "^0.3.4", "symfony/console": "6.0.*", "symfony/dotenv": "6.0.*", - "symfony/flex": "^1.17", + "symfony/flex": "^1.17|^2", "symfony/framework-bundle": "6.0.*", "symfony/options-resolver": "6.0.*", "symfony/property-access": "6.0.*", @@ -64,11 +64,6 @@ } }, "bin": ["bin/freddie"], - "replace": { - "symfony/polyfill-ctype": "*", - "symfony/polyfill-iconv": "*", - "symfony/polyfill-php72": "*" - }, "scripts": { "post-install-cmd": [ "bin/freddie cache:clear" diff --git a/composer.lock b/composer.lock index d365ff0..49e0706 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "414602a649e069d55914f87b1714a23e", + "content-hash": "93ebe5fcb2a96088bc850e34bc31af6a", "packages": [ { "name": "bentools/querystring", @@ -318,32 +318,28 @@ }, { "name": "doctrine/lexer", - "version": "1.2.1", + "version": "1.2.2", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" + "reference": "9c50f840f257bbb941e6f4a0e94ccf5db5c3f76c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", - "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/9c50f840f257bbb941e6f4a0e94ccf5db5c3f76c", + "reference": "9c50f840f257bbb941e6f4a0e94ccf5db5c3f76c", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^8.2" + "doctrine/coding-standard": "^9.0", + "phpstan/phpstan": "1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.11" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" @@ -378,7 +374,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.1" + "source": "https://github.com/doctrine/lexer/tree/1.2.2" }, "funding": [ { @@ -394,7 +390,7 @@ "type": "tidelift" } ], - "time": "2020-05-25T17:44:05+00:00" + "time": "2022-01-12T08:27:12+00:00" }, { "name": "evenement/evenement", @@ -800,16 +796,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.5.1", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae" + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/a12f7e301eb7258bb68acd89d4aefa05c2906cae", - "reference": "a12f7e301eb7258bb68acd89d4aefa05c2906cae", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/93ebd0014cab80c4ea9f5e297ea48672f1b87706", + "reference": "93ebd0014cab80c4ea9f5e297ea48672f1b87706", "shasum": "" }, "require": { @@ -844,9 +840,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.5.1" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.0" }, - "time": "2021-10-02T14:08:47+00:00" + "time": "2022-01-04T19:58:01+00:00" }, { "name": "psr/cache", @@ -1109,12 +1105,12 @@ "source": { "type": "git", "url": "https://github.com/reactphp/async.git", - "reference": "80aa19fabcd2ca7c4a37c7079ced48b40bbb1c79" + "reference": "ff11a7aa9eea7104af8f05bafbc85422dac4b8ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/async/zipball/80aa19fabcd2ca7c4a37c7079ced48b40bbb1c79", - "reference": "80aa19fabcd2ca7c4a37c7079ced48b40bbb1c79", + "url": "https://api.github.com/repos/reactphp/async/zipball/ff11a7aa9eea7104af8f05bafbc85422dac4b8ab", + "reference": "ff11a7aa9eea7104af8f05bafbc85422dac4b8ab", "shasum": "" }, "require": { @@ -1180,7 +1176,7 @@ "type": "github" } ], - "time": "2021-11-22T14:32:42+00:00" + "time": "2022-01-05T06:37:17+00:00" }, { "name": "react/cache", @@ -1731,16 +1727,16 @@ }, { "name": "react/socket", - "version": "v1.10.0", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "d132fde589ea97f4165f2d94b5296499eac125ec" + "reference": "f474156aaab4f09041144fa8b57c7d70aed32a1c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/d132fde589ea97f4165f2d94b5296499eac125ec", - "reference": "d132fde589ea97f4165f2d94b5296499eac125ec", + "url": "https://api.github.com/repos/reactphp/socket/zipball/f474156aaab4f09041144fa8b57c7d70aed32a1c", + "reference": "f474156aaab4f09041144fa8b57c7d70aed32a1c", "shasum": "" }, "require": { @@ -1749,11 +1745,11 @@ "react/dns": "^1.8", "react/event-loop": "^1.2", "react/promise": "^2.6.0 || ^1.2.1", - "react/promise-timer": "^1.4.0", + "react/promise-timer": "^1.8", "react/stream": "^1.2" }, "require-dev": { - "clue/block-react": "^1.2", + "clue/block-react": "^1.5", "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", "react/promise-stream": "^1.2" }, @@ -1799,7 +1795,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.10.0" + "source": "https://github.com/reactphp/socket/tree/v1.11.0" }, "funding": [ { @@ -1811,7 +1807,7 @@ "type": "github" } ], - "time": "2021-11-29T10:08:24+00:00" + "time": "2022-01-14T10:14:32+00:00" }, { "name": "react/stream", @@ -2016,16 +2012,16 @@ }, { "name": "symfony/cache", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/cache.git", - "reference": "67213eb02dfc41aa9acb01b8ba260d133c523b79" + "reference": "41bdcb2d067c68f338b0cfd46a86abd8309b4153" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/cache/zipball/67213eb02dfc41aa9acb01b8ba260d133c523b79", - "reference": "67213eb02dfc41aa9acb01b8ba260d133c523b79", + "url": "https://api.github.com/repos/symfony/cache/zipball/41bdcb2d067c68f338b0cfd46a86abd8309b4153", + "reference": "41bdcb2d067c68f338b0cfd46a86abd8309b4153", "shasum": "" }, "require": { @@ -2089,7 +2085,7 @@ "psr6" ], "support": { - "source": "https://github.com/symfony/cache/tree/v6.0.1" + "source": "https://github.com/symfony/cache/tree/v6.0.2" }, "funding": [ { @@ -2105,7 +2101,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-29T13:00:11+00:00" }, { "name": "symfony/cache-contracts", @@ -2188,16 +2184,16 @@ }, { "name": "symfony/config", - "version": "v6.0.0", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "df4871981fd37f953c117b55feac03462be5a2d6" + "reference": "990e6d603da7b9556645e5689c7b082f564790e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/df4871981fd37f953c117b55feac03462be5a2d6", - "reference": "df4871981fd37f953c117b55feac03462be5a2d6", + "url": "https://api.github.com/repos/symfony/config/zipball/990e6d603da7b9556645e5689c7b082f564790e7", + "reference": "990e6d603da7b9556645e5689c7b082f564790e7", "shasum": "" }, "require": { @@ -2246,7 +2242,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.0.0" + "source": "https://github.com/symfony/config/tree/v6.0.2" }, "funding": [ { @@ -2262,20 +2258,20 @@ "type": "tidelift" } ], - "time": "2021-11-23T19:05:29+00:00" + "time": "2021-12-28T14:01:53+00:00" }, { "name": "symfony/console", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fafd9802d386bf1c267e0249ddb7ceb14dcfdad4" + "reference": "dd434fa8d69325e5d210f63070014d889511fcb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fafd9802d386bf1c267e0249ddb7ceb14dcfdad4", - "reference": "fafd9802d386bf1c267e0249ddb7ceb14dcfdad4", + "url": "https://api.github.com/repos/symfony/console/zipball/dd434fa8d69325e5d210f63070014d889511fcb3", + "reference": "dd434fa8d69325e5d210f63070014d889511fcb3", "shasum": "" }, "require": { @@ -2341,7 +2337,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.0.1" + "source": "https://github.com/symfony/console/tree/v6.0.2" }, "funding": [ { @@ -2357,20 +2353,20 @@ "type": "tidelift" } ], - "time": "2021-12-09T12:47:37+00:00" + "time": "2021-12-27T21:05:08+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "401f794b342585772c1d22288cafbce597485093" + "reference": "a9346ef44ea8a7b60f1f1edc8d802ffb91baa6a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/401f794b342585772c1d22288cafbce597485093", - "reference": "401f794b342585772c1d22288cafbce597485093", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/a9346ef44ea8a7b60f1f1edc8d802ffb91baa6a8", + "reference": "a9346ef44ea8a7b60f1f1edc8d802ffb91baa6a8", "shasum": "" }, "require": { @@ -2429,7 +2425,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.0.1" + "source": "https://github.com/symfony/dependency-injection/tree/v6.0.2" }, "funding": [ { @@ -2445,7 +2441,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-29T10:14:09+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2516,16 +2512,16 @@ }, { "name": "symfony/dotenv", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/dotenv.git", - "reference": "b8491dc698e76a2b6255c641346f1958bd65f5cf" + "reference": "5c43c5515e549a8c2c426be36d40f47daf196968" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dotenv/zipball/b8491dc698e76a2b6255c641346f1958bd65f5cf", - "reference": "b8491dc698e76a2b6255c641346f1958bd65f5cf", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/5c43c5515e549a8c2c426be36d40f47daf196968", + "reference": "5c43c5515e549a8c2c426be36d40f47daf196968", "shasum": "" }, "require": { @@ -2566,7 +2562,7 @@ "environment" ], "support": { - "source": "https://github.com/symfony/dotenv/tree/v6.0.1" + "source": "https://github.com/symfony/dotenv/tree/v6.0.2" }, "funding": [ { @@ -2582,20 +2578,20 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-16T22:05:41+00:00" }, { "name": "symfony/error-handler", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "944193d25c564c8c80411a5d02eb2be823d57d5c" + "reference": "3e677f0c14a529bc542025c96cfa9638227b4cc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/944193d25c564c8c80411a5d02eb2be823d57d5c", - "reference": "944193d25c564c8c80411a5d02eb2be823d57d5c", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/3e677f0c14a529bc542025c96cfa9638227b4cc6", + "reference": "3e677f0c14a529bc542025c96cfa9638227b4cc6", "shasum": "" }, "require": { @@ -2637,7 +2633,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.0.1" + "source": "https://github.com/symfony/error-handler/tree/v6.0.2" }, "funding": [ { @@ -2653,20 +2649,20 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-21T13:16:58+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4f06d19a5f78087061f9de6df3269c139c3d289d" + "reference": "7093f25359e2750bfe86842c80c4e4a6a852d05c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4f06d19a5f78087061f9de6df3269c139c3d289d", - "reference": "4f06d19a5f78087061f9de6df3269c139c3d289d", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7093f25359e2750bfe86842c80c4e4a6a852d05c", + "reference": "7093f25359e2750bfe86842c80c4e4a6a852d05c", "shasum": "" }, "require": { @@ -2720,7 +2716,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.0.1" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.0.2" }, "funding": [ { @@ -2736,7 +2732,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-21T10:43:13+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -2882,16 +2878,16 @@ }, { "name": "symfony/finder", - "version": "v6.0.0", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "07debda41a4d32d33e59e6ab302af1701e15f173" + "reference": "03d2833e677d48317cac852f9c0287fb048c3c5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/07debda41a4d32d33e59e6ab302af1701e15f173", - "reference": "07debda41a4d32d33e59e6ab302af1701e15f173", + "url": "https://api.github.com/repos/symfony/finder/zipball/03d2833e677d48317cac852f9c0287fb048c3c5c", + "reference": "03d2833e677d48317cac852f9c0287fb048c3c5c", "shasum": "" }, "require": { @@ -2923,7 +2919,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.0.0" + "source": "https://github.com/symfony/finder/tree/v6.0.2" }, "funding": [ { @@ -2939,32 +2935,32 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:34:37+00:00" + "time": "2021-12-20T16:21:45+00:00" }, { "name": "symfony/flex", - "version": "v1.17.6", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/symfony/flex.git", - "reference": "7a79135e1dc66b30042b4d968ecba0908f9374bc" + "reference": "3dbfa5c4e3308fd9def9a2006a20fa0c272a30a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/flex/zipball/7a79135e1dc66b30042b4d968ecba0908f9374bc", - "reference": "7a79135e1dc66b30042b4d968ecba0908f9374bc", + "url": "https://api.github.com/repos/symfony/flex/zipball/3dbfa5c4e3308fd9def9a2006a20fa0c272a30a2", + "reference": "3dbfa5c4e3308fd9def9a2006a20fa0c272a30a2", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0|^2.0", - "php": ">=7.1" + "composer-plugin-api": "^2.1", + "php": ">=8.0" }, "require-dev": { - "composer/composer": "^1.0.2|^2.0", - "symfony/dotenv": "^4.4|^5.0|^6.0", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/phpunit-bridge": "^4.4.12|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0" + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" }, "type": "composer-plugin", "extra": { @@ -2988,7 +2984,7 @@ "description": "Composer plugin for Symfony", "support": { "issues": "https://github.com/symfony/flex/issues", - "source": "https://github.com/symfony/flex/tree/v1.17.6" + "source": "https://github.com/symfony/flex/tree/v2.0.1" }, "funding": [ { @@ -3004,20 +3000,20 @@ "type": "tidelift" } ], - "time": "2021-11-29T15:39:37+00:00" + "time": "2021-11-29T15:40:20+00:00" }, { "name": "symfony/framework-bundle", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "15131cd085d3f5568634015f6885b85cf8a2a2f7" + "reference": "d0598be96b193c4c2e5abab56af512a8e10b3540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/15131cd085d3f5568634015f6885b85cf8a2a2f7", - "reference": "15131cd085d3f5568634015f6885b85cf8a2a2f7", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/d0598be96b193c4c2e5abab56af512a8e10b3540", + "reference": "d0598be96b193c4c2e5abab56af512a8e10b3540", "shasum": "" }, "require": { @@ -3138,7 +3134,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v6.0.1" + "source": "https://github.com/symfony/framework-bundle/tree/v6.0.2" }, "funding": [ { @@ -3154,20 +3150,20 @@ "type": "tidelift" } ], - "time": "2021-12-09T12:47:37+00:00" + "time": "2021-12-22T00:01:56+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "4c55dff16ba400dc81c56b6234e5942f9b9c7bcc" + "reference": "ac1cd9b84bdea9a3a06cd2127e910afc8b276798" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/4c55dff16ba400dc81c56b6234e5942f9b9c7bcc", - "reference": "4c55dff16ba400dc81c56b6234e5942f9b9c7bcc", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ac1cd9b84bdea9a3a06cd2127e910afc8b276798", + "reference": "ac1cd9b84bdea9a3a06cd2127e910afc8b276798", "shasum": "" }, "require": { @@ -3210,7 +3206,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.0.1" + "source": "https://github.com/symfony/http-foundation/tree/v6.0.2" }, "funding": [ { @@ -3226,20 +3222,20 @@ "type": "tidelift" } ], - "time": "2021-12-09T12:47:37+00:00" + "time": "2021-12-28T17:22:37+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "cd7ed5337e67e1be91526184006fe7c1603283cb" + "reference": "00743bc336421a9be4f3e04464969ba8ef3517ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cd7ed5337e67e1be91526184006fe7c1603283cb", - "reference": "cd7ed5337e67e1be91526184006fe7c1603283cb", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/00743bc336421a9be4f3e04464969ba8ef3517ad", + "reference": "00743bc336421a9be4f3e04464969ba8ef3517ad", "shasum": "" }, "require": { @@ -3319,7 +3315,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.0.1" + "source": "https://github.com/symfony/http-kernel/tree/v6.0.2" }, "funding": [ { @@ -3335,7 +3331,7 @@ "type": "tidelift" } ], - "time": "2021-12-09T13:42:47+00:00" + "time": "2021-12-29T14:07:16+00:00" }, { "name": "symfony/options-resolver", @@ -3404,18 +3400,100 @@ ], "time": "2021-11-23T19:05:29+00:00" }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.24.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "30885182c981ab175d4d034db0f6f469898070ab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.23-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.24.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" + }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.23.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535" + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/16880ba9c5ebe3642d1995ab866db29270b36535", - "reference": "16880ba9c5ebe3642d1995ab866db29270b36535", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", "shasum": "" }, "require": { @@ -3467,7 +3545,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.24.0" }, "funding": [ { @@ -3483,11 +3561,11 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2021-11-23T21:10:46+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.23.0", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3551,7 +3629,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.24.0" }, "funding": [ { @@ -3571,21 +3649,24 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.23.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6" + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9174a3d80210dca8daa7f31fec659150bbeabfc6", - "reference": "9174a3d80210dca8daa7f31fec659150bbeabfc6", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { "ext-mbstring": "For best performance" }, @@ -3631,7 +3712,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.23.1" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" }, "funding": [ { @@ -3647,20 +3728,20 @@ "type": "tidelift" } ], - "time": "2021-05-27T12:26:48+00:00" + "time": "2021-11-30T18:21:41+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.23.0", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "e66119f3de95efc359483f810c4c3e6436279436" + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/e66119f3de95efc359483f810c4c3e6436279436", - "reference": "e66119f3de95efc359483f810c4c3e6436279436", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", + "reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f", "shasum": "" }, "require": { @@ -3710,7 +3791,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.24.0" }, "funding": [ { @@ -3726,25 +3807,28 @@ "type": "tidelift" } ], - "time": "2021-05-21T13:25:03+00:00" + "time": "2021-09-13T13:58:11+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.23.0", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "9165effa2eb8a31bb3fa608df9d529920d21ddd9" + "reference": "7529922412d23ac44413d0f308861d50cf68d3ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/9165effa2eb8a31bb3fa608df9d529920d21ddd9", - "reference": "9165effa2eb8a31bb3fa608df9d529920d21ddd9", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/7529922412d23ac44413d0f308861d50cf68d3ee", + "reference": "7529922412d23ac44413d0f308861d50cf68d3ee", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-uuid": "*" + }, "suggest": { "ext-uuid": "For best performance" }, @@ -3789,7 +3873,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.23.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.24.0" }, "funding": [ { @@ -3805,20 +3889,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2021-10-20T20:35:02+00:00" }, { "name": "symfony/property-access", - "version": "v6.0.0", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/property-access.git", - "reference": "e0b66975319b4648e0cbf267878b07d8e2d11e2e" + "reference": "3f237ffd38a54592181ac62f63c6cabbca00051f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-access/zipball/e0b66975319b4648e0cbf267878b07d8e2d11e2e", - "reference": "e0b66975319b4648e0cbf267878b07d8e2d11e2e", + "url": "https://api.github.com/repos/symfony/property-access/zipball/3f237ffd38a54592181ac62f63c6cabbca00051f", + "reference": "3f237ffd38a54592181ac62f63c6cabbca00051f", "shasum": "" }, "require": { @@ -3868,7 +3952,7 @@ "reflection" ], "support": { - "source": "https://github.com/symfony/property-access/tree/v6.0.0" + "source": "https://github.com/symfony/property-access/tree/v6.0.2" }, "funding": [ { @@ -3884,20 +3968,20 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:34:37+00:00" + "time": "2021-12-11T16:36:28+00:00" }, { "name": "symfony/property-info", - "version": "v6.0.0", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/property-info.git", - "reference": "56e98f48ee2dc89688d1870e66d834627a17db6d" + "reference": "fc23cfd8fe8faa0712a8909f956cf2e46c72f6cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/property-info/zipball/56e98f48ee2dc89688d1870e66d834627a17db6d", - "reference": "56e98f48ee2dc89688d1870e66d834627a17db6d", + "url": "https://api.github.com/repos/symfony/property-info/zipball/fc23cfd8fe8faa0712a8909f956cf2e46c72f6cf", + "reference": "fc23cfd8fe8faa0712a8909f956cf2e46c72f6cf", "shasum": "" }, "require": { @@ -3957,7 +4041,7 @@ "validator" ], "support": { - "source": "https://github.com/symfony/property-info/tree/v6.0.0" + "source": "https://github.com/symfony/property-info/tree/v6.0.2" }, "funding": [ { @@ -3973,7 +4057,7 @@ "type": "tidelift" } ], - "time": "2021-11-04T18:05:01+00:00" + "time": "2021-12-27T21:05:08+00:00" }, { "name": "symfony/routing", @@ -4141,16 +4225,16 @@ }, { "name": "symfony/serializer", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/serializer.git", - "reference": "60637437ca5bfa519e4085e9ea28ead456f9d85e" + "reference": "2282e7512a3264ef964cefce0a4a8037cd1044e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/serializer/zipball/60637437ca5bfa519e4085e9ea28ead456f9d85e", - "reference": "60637437ca5bfa519e4085e9ea28ead456f9d85e", + "url": "https://api.github.com/repos/symfony/serializer/zipball/2282e7512a3264ef964cefce0a4a8037cd1044e5", + "reference": "2282e7512a3264ef964cefce0a4a8037cd1044e5", "shasum": "" }, "require": { @@ -4164,6 +4248,7 @@ "symfony/dependency-injection": "<5.4", "symfony/property-access": "<5.4", "symfony/property-info": "<5.4", + "symfony/uid": "<5.4", "symfony/yaml": "<5.4" }, "require-dev": { @@ -4221,7 +4306,7 @@ "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/serializer/tree/v6.0.1" + "source": "https://github.com/symfony/serializer/tree/v6.0.2" }, "funding": [ { @@ -4237,7 +4322,7 @@ "type": "tidelift" } ], - "time": "2021-12-01T16:45:37+00:00" + "time": "2021-12-25T20:10:03+00:00" }, { "name": "symfony/service-contracts", @@ -4323,16 +4408,16 @@ }, { "name": "symfony/string", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "0cfed595758ec6e0a25591bdc8ca733c1896af32" + "reference": "bae261d0c3ac38a1f802b4dfed42094296100631" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/0cfed595758ec6e0a25591bdc8ca733c1896af32", - "reference": "0cfed595758ec6e0a25591bdc8ca733c1896af32", + "url": "https://api.github.com/repos/symfony/string/zipball/bae261d0c3ac38a1f802b4dfed42094296100631", + "reference": "bae261d0c3ac38a1f802b4dfed42094296100631", "shasum": "" }, "require": { @@ -4388,7 +4473,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.0.1" + "source": "https://github.com/symfony/string/tree/v6.0.2" }, "funding": [ { @@ -4404,20 +4489,20 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-16T22:13:01+00:00" }, { "name": "symfony/uid", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "6ef7c361b84f8ae666843279d08b2b8ce8006033" + "reference": "6750d730b5d1c002b366ec40ac811317d142d1f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/6ef7c361b84f8ae666843279d08b2b8ce8006033", - "reference": "6ef7c361b84f8ae666843279d08b2b8ce8006033", + "url": "https://api.github.com/repos/symfony/uid/zipball/6750d730b5d1c002b366ec40ac811317d142d1f3", + "reference": "6750d730b5d1c002b366ec40ac811317d142d1f3", "shasum": "" }, "require": { @@ -4458,10 +4543,11 @@ "homepage": "https://symfony.com", "keywords": [ "UID", + "ulid", "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.0.1" + "source": "https://github.com/symfony/uid/tree/v6.0.2" }, "funding": [ { @@ -4477,20 +4563,20 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-16T22:13:01+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9ca4948ec35bb15175e5475ba83dfdb13042a86c" + "reference": "41e46f64084a205459a862751158ce2190bd5cb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9ca4948ec35bb15175e5475ba83dfdb13042a86c", - "reference": "9ca4948ec35bb15175e5475ba83dfdb13042a86c", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41e46f64084a205459a862751158ce2190bd5cb5", + "reference": "41e46f64084a205459a862751158ce2190bd5cb5", "shasum": "" }, "require": { @@ -4549,7 +4635,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.0.1" + "source": "https://github.com/symfony/var-dumper/tree/v6.0.2" }, "funding": [ { @@ -4565,7 +4651,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-29T10:14:09+00:00" }, { "name": "symfony/var-exporter", @@ -4641,16 +4727,16 @@ }, { "name": "symfony/yaml", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d34390fe7e8c0fe7e4192c67c27ecf58bc7d3ed7" + "reference": "ed602f38b8636a2ea21af760d2578f3d2f92fc60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d34390fe7e8c0fe7e4192c67c27ecf58bc7d3ed7", - "reference": "d34390fe7e8c0fe7e4192c67c27ecf58bc7d3ed7", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ed602f38b8636a2ea21af760d2578f3d2f92fc60", + "reference": "ed602f38b8636a2ea21af760d2578f3d2f92fc60", "shasum": "" }, "require": { @@ -4695,7 +4781,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.0.1" + "source": "https://github.com/symfony/yaml/tree/v6.0.2" }, "funding": [ { @@ -4711,7 +4797,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-16T22:13:01+00:00" }, { "name": "webmozart/assert", @@ -4779,12 +4865,12 @@ "source": { "type": "git", "url": "https://github.com/clue/reactphp-eventsource.git", - "reference": "8ca8624895f1576e5c3bcdd9c210841e6c45e400" + "reference": "7fc5855a52740d7e24b781bb8be45480fcd1d7aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-eventsource/zipball/8ca8624895f1576e5c3bcdd9c210841e6c45e400", - "reference": "8ca8624895f1576e5c3bcdd9c210841e6c45e400", + "url": "https://api.github.com/repos/clue/reactphp-eventsource/zipball/7fc5855a52740d7e24b781bb8be45480fcd1d7aa", + "reference": "7fc5855a52740d7e24b781bb8be45480fcd1d7aa", "shasum": "" }, "require": { @@ -4837,7 +4923,7 @@ "type": "github" } ], - "time": "2021-12-20T08:12:47+00:00" + "time": "2021-12-22T12:01:58+00:00" }, { "name": "doctrine/instantiator", @@ -4963,16 +5049,16 @@ }, { "name": "filp/whoops", - "version": "2.14.4", + "version": "2.14.5", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "f056f1fe935d9ed86e698905a957334029899895" + "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/f056f1fe935d9ed86e698905a957334029899895", - "reference": "f056f1fe935d9ed86e698905a957334029899895", + "url": "https://api.github.com/repos/filp/whoops/zipball/a63e5e8f26ebbebf8ed3c5c691637325512eb0dc", + "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc", "shasum": "" }, "require": { @@ -5022,7 +5108,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.14.4" + "source": "https://github.com/filp/whoops/tree/2.14.5" }, "funding": [ { @@ -5030,7 +5116,7 @@ "type": "github" } ], - "time": "2021-10-03T12:00:00+00:00" + "time": "2022-01-07T12:00:00+00:00" }, { "name": "myclabs/deep-copy", @@ -5148,31 +5234,31 @@ }, { "name": "nunomaduro/collision", - "version": "dev-develop", + "version": "v6.0.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "5f7d8c370e1602ac22032c2688b04af4c28e221d" + "reference": "5338ecc3909ef3ed150f6408f14e44cdb62bfdd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/5f7d8c370e1602ac22032c2688b04af4c28e221d", - "reference": "5f7d8c370e1602ac22032c2688b04af4c28e221d", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/5338ecc3909ef3ed150f6408f14e44cdb62bfdd0", + "reference": "5338ecc3909ef3ed150f6408f14e44cdb62bfdd0", "shasum": "" }, "require": { "facade/ignition-contracts": "^1.0.2", - "filp/whoops": "^2.14.3", + "filp/whoops": "^2.14.5", "php": "^8.0.0", - "symfony/console": "^6.0.0" + "symfony/console": "^6.0.2" }, "require-dev": { - "laravel/framework": "8.x-dev", - "nunomaduro/larastan": "^0.7.12", + "brianium/paratest": "^6.4.1", + "laravel/framework": "^9.0", + "nunomaduro/larastan": "^1.0.2", "nunomaduro/mock-final-classes": "^1.1.0", "orchestra/testbench": "^7.0.0", - "phpstan/phpstan": "^0.12.94", - "phpunit/phpunit": "^9.5.8" + "phpunit/phpunit": "^9.5.11" }, "type": "library", "extra": { @@ -5219,7 +5305,7 @@ }, "funding": [ { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "url": "https://www.paypal.com/paypalme/enunomaduro", "type": "custom" }, { @@ -5231,7 +5317,7 @@ "type": "patreon" } ], - "time": "2021-09-20T15:19:57+00:00" + "time": "2022-01-10T17:17:19+00:00" }, { "name": "pestphp/pest", @@ -5598,16 +5684,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.2.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee" + "reference": "72b04d97b5e6e60a081f17c416fef35bd521120b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", - "reference": "cbe085f9fdead5b6d62e4c022ca52dc9427a10ee", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/72b04d97b5e6e60a081f17c416fef35bd521120b", + "reference": "72b04d97b5e6e60a081f17c416fef35bd521120b", "shasum": "" }, "require": { @@ -5623,7 +5709,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -5638,7 +5724,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/1.2.0" + "source": "https://github.com/phpstan/phpstan/tree/1.4.0" }, "funding": [ { @@ -5658,7 +5744,7 @@ "type": "tidelift" } ], - "time": "2021-11-18T14:09:01+00:00" + "time": "2022-01-14T15:58:47+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5980,16 +6066,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.10", + "version": "9.5.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a" + "reference": "2406855036db1102126125537adb1406f7242fdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c814a05837f2edb0d1471d6e3f4ab3501ca3899a", - "reference": "c814a05837f2edb0d1471d6e3f4ab3501ca3899a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2406855036db1102126125537adb1406f7242fdd", + "reference": "2406855036db1102126125537adb1406f7242fdd", "shasum": "" }, "require": { @@ -6067,11 +6153,11 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.10" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.11" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "url": "https://phpunit.de/sponsors.html", "type": "custom" }, { @@ -6079,7 +6165,7 @@ "type": "github" } ], - "time": "2021-09-25T07:38:51+00:00" + "time": "2021-12-25T07:07:57+00:00" }, { "name": "react/child-process", @@ -7182,16 +7268,16 @@ }, { "name": "symfony/http-client", - "version": "v6.0.1", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "99e42b54cedf061d898aa796a0b3758598021607" + "reference": "7f1cbd44590cb0acc6208c1711a52733e9a91663" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/99e42b54cedf061d898aa796a0b3758598021607", - "reference": "99e42b54cedf061d898aa796a0b3758598021607", + "url": "https://api.github.com/repos/symfony/http-client/zipball/7f1cbd44590cb0acc6208c1711a52733e9a91663", + "reference": "7f1cbd44590cb0acc6208c1711a52733e9a91663", "shasum": "" }, "require": { @@ -7246,7 +7332,7 @@ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-client/tree/v6.0.1" + "source": "https://github.com/symfony/http-client/tree/v6.0.2" }, "funding": [ { @@ -7262,7 +7348,7 @@ "type": "tidelift" } ], - "time": "2021-12-08T15:13:44+00:00" + "time": "2021-12-29T10:14:09+00:00" }, { "name": "symfony/http-client-contracts", @@ -7344,16 +7430,16 @@ }, { "name": "symfony/process", - "version": "v6.0.0", + "version": "v6.0.2", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d970c45c2186aa4331d1656950a82df64e232580" + "reference": "71da2b7f3fdba460fcf61a97c8d3d14bbf3391ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d970c45c2186aa4331d1656950a82df64e232580", - "reference": "d970c45c2186aa4331d1656950a82df64e232580", + "url": "https://api.github.com/repos/symfony/process/zipball/71da2b7f3fdba460fcf61a97c8d3d14bbf3391ad", + "reference": "71da2b7f3fdba460fcf61a97c8d3d14bbf3391ad", "shasum": "" }, "require": { @@ -7385,7 +7471,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.0.0" + "source": "https://github.com/symfony/process/tree/v6.0.2" }, "funding": [ { @@ -7401,7 +7487,7 @@ "type": "tidelift" } ], - "time": "2021-11-28T15:34:37+00:00" + "time": "2021-12-27T21:05:08+00:00" }, { "name": "theseer/tokenizer", diff --git a/config/bundles.php b/config/bundles.php index 49d3fb6..0a9c74a 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -2,4 +2,5 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], + Freddie\FreddieBundle::class => ['all' => true], ]; diff --git a/config/services.yaml b/config/services.yaml deleted file mode 100644 index dca119f..0000000 --- a/config/services.yaml +++ /dev/null @@ -1,97 +0,0 @@ -parameters: - env(TRANSPORT_DSN): 'php://default' - env(ALLOW_ANONYMOUS): true - env(JWT_SECRET_KEY): "!ChangeMe!" - env(JWT_PUBLIC_KEY): ~ - env(JWT_ALGORITHM): "HS256" - transport_dsn: '%env(TRANSPORT_DSN)%' - allow_anonymous: '%env(bool:ALLOW_ANONYMOUS)%' - -services: - _instanceof: - Freddie\Hub\HubControllerInterface: - tags: ['mercure.controller'] - Freddie\Hub\Transport\TransportFactoryInterface: - tags: ['mercure.transport_factory'] - Lcobucci\JWT\Validation\Constraint: - tags: ['lcobucci.jwt.validation_constraint'] - - _defaults: - autowire: true - autoconfigure: true - bind: - iterable $controllers: !tagged_iterator mercure.controller - - - Freddie\: - resource: '%kernel.project_dir%/src/' - exclude: - - '%kernel.project_dir%/src/Hub/Transport/Redis/RedisPublisher.php' - - '%kernel.project_dir%/src/Hub/Transport/Redis/RedisListener.php' - - '%kernel.project_dir%/src/Hub/Transport/Redis/RedisTransport.php' - - '%kernel.project_dir%/src/Kernel.php' - - '%kernel.project_dir%/src/functions.php' - - Freddie\Hub\Controller\SubscribeController: - arguments: - $options: - allow_anonymous: '%allow_anonymous%' - - Freddie\Security\JWT\Extractor\PSR7TokenExtractorInterface: '@Freddie\Security\JWT\Extractor\ChainTokenExtractor' - Freddie\Hub\Transport\TransportFactory: - arguments: - $factories: !tagged_iterator mercure.transport_factory - - Freddie\Hub\Transport\TransportInterface: - factory: ['@Freddie\Hub\Transport\TransportFactory', 'create'] - arguments: ['%transport_dsn%'] - - Freddie\Security\JWT\Configuration\ValidationConstraints: - arguments: - - !tagged_iterator lcobucci.jwt.validation_constraint - - FrameworkX\App: - arguments: - - '@Freddie\Hub\Middleware\HttpExceptionConverterMiddleware' - - '@Freddie\Hub\Middleware\TokenExtractorMiddleware' - - Evenement\EventEmitter: ~ - Evenement\EventEmitterInterface: '@Evenement\EventEmitter' - Clue\React\Redis\Factory: ~ - - Lcobucci\JWT\Configuration: - factory: '@Freddie\Security\JWT\Configuration\ConfigurationFactory' - arguments: - - '%env(JWT_ALGORITHM)%' - - '%env(resolve:JWT_SECRET_KEY)%' - - '%env(string:default::resolve:JWT_PUBLIC_KEY)%' - - '%env(string:default::JWT_PASSPHRASE)%' - - Lcobucci\JWT\Parser: - factory: ['@Lcobucci\JWT\Configuration', 'parser'] - - Lcobucci\JWT\Validator: - factory: ['@Lcobucci\JWT\Configuration', 'validator'] - - jwt.verification_key: - class: Lcobucci\JWT\Signer\Key - factory: '@Freddie\Security\JWT\Configuration\VerificationKeyFactory' - - DateTimeZone: - class: DateTimeZone - factory: '@Freddie\Security\JWT\Configuration\DateTimeZoneFactory' - - Lcobucci\Clock\SystemClock: - arguments: - - '@DateTimeZone' - - Lcobucci\Clock\Clock: '@Lcobucci\Clock\SystemClock' - - Lcobucci\JWT\Signer: - factory: '@Freddie\Security\JWT\Configuration\SignerFactory' - - Lcobucci\JWT\Validation\Constraint\LooseValidAt: ~ - Lcobucci\JWT\Validation\Constraint\SignedWith: - arguments: - $key: '@jwt.verification_key' - diff --git a/phpstan.neon b/phpstan.neon index 1ff472c..9f62798 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,6 @@ parameters: level: 8 paths: - src + + ignoreErrors: + - '#React\\Promise\\PromiseInterface is not generic#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f411a25..9d57607 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -30,5 +30,8 @@ src + + src/DependencyInjection + diff --git a/src/Command/ServeCommand.php b/src/Command/ServeCommand.php index 65d9515..9474601 100644 --- a/src/Command/ServeCommand.php +++ b/src/Command/ServeCommand.php @@ -14,7 +14,9 @@ use function sprintf; #[AsCommand( - name: 'serve' + name: 'freddie:serve', + description: 'Start Freddie server, the PHP Mercure Hub.', + aliases: ['serve'], )] final class ServeCommand extends Command { diff --git a/src/DependencyInjection/FreddieExtension.php b/src/DependencyInjection/FreddieExtension.php new file mode 100644 index 0000000..40fe1c9 --- /dev/null +++ b/src/DependencyInjection/FreddieExtension.php @@ -0,0 +1,22 @@ + $configs + */ + public function load(array $configs, ContainerBuilder $container): void + { + $loader = new PhpFileLoader($container, new FileLocator(__DIR__)); + $loader->load('services.php'); + } +} diff --git a/src/DependencyInjection/services.php b/src/DependencyInjection/services.php new file mode 100644 index 0000000..261c357 --- /dev/null +++ b/src/DependencyInjection/services.php @@ -0,0 +1,174 @@ +parameters(); + $params->set('env(TRANSPORT_DSN)', 'php://default'); + $params->set('env(ALLOW_ANONYMOUS)', Hub::DEFAULT_OPTIONS['allow_anonymous']); + $params->set('env(JWT_SECRET_KEY)', '!ChangeMe!'); + $params->set('env(JWT_PUBLIC_KEY)', null); + $params->set('env(JWT_ALGORITHM)', 'HS256'); + $params->set('transport_dsn', '%env(resolve:TRANSPORT_DSN)%'); + $params->set('allow_anonymous', '%env(bool:ALLOW_ANONYMOUS)%'); + + $services = $container->services(); + $services + ->defaults() + ->private() + ->autoconfigure() + ->autowire() + ->bind('iterable $controllers', tagged_iterator('mercure.controller')) + ; + + $services + ->instanceof(HubControllerInterface::class) + ->tag('mercure.controller'); + + $services + ->instanceof(TransportFactoryInterface::class) + ->tag('mercure.transport_factory'); + + $services + ->instanceof(Constraint::class) + ->tag('lcobucci.jwt.validation_constraint'); + + $services + ->load('Freddie\\', dirname(__DIR__)) + ->exclude([ + dirname(__DIR__) . '/Hub/DependencyInjection/*', + dirname(__DIR__) . '/Hub/Transport/Redis/RedisPublisher.php', + dirname(__DIR__) . '/Hub/Transport/Redis/RedisListener.php', + dirname(__DIR__) . '/Hub/Transport/Redis/RedisTransport.php', + dirname(__DIR__) . '/FreddieBundle.php', + dirname(__DIR__) . '/functions.php', + dirname(__DIR__) . '/Kernel.php', + ]); + + $services + ->set(SubscribeController::class); + + $services + ->alias(PSR7TokenExtractorInterface::class, ChainTokenExtractor::class); + + $services + ->set(TransportFactory::class) + ->arg('$factories', tagged_iterator('mercure.transport_factory')); + + $services + ->set(TransportInterface::class) + ->factory([service(TransportFactory::class), 'create']) + ->arg('$dsn', param('transport_dsn')); + + $services + ->set(Hub::class) + ->arg('$options', ['allow_anonymous' => param('allow_anonymous')]); + + $services->alias(HubInterface::class, Hub::class); + + $services + ->set(ValidationConstraints::class) + ->arg('$validationConstraints', tagged_iterator('lcobucci.jwt.validation_constraint')); + + $services + ->set(App::class) + ->args([ + service(HttpExceptionConverterMiddleware::class), + service(TokenExtractorMiddleware::class), + ]); + + $services + ->set(EventEmitter::class); + + $services + ->alias(EventEmitterInterface::class, EventEmitter::class); + + $services + ->set(Factory::class); + + $services + ->set(Configuration::class) + ->factory(service(ConfigurationFactory::class)) + ->args([ + param('env(JWT_ALGORITHM)'), + param('env(resolve:JWT_SECRET_KEY)'), + param('env(string:default::resolve:JWT_PUBLIC_KEY)'), + param('env(string:default::JWT_PASSPHRASE)'), + ]); + + $services + ->set(Parser::class) + ->factory([service(Configuration::class), 'parser']); + + $services + ->set(Validator::class) + ->factory([service(Configuration::class), 'validator']); + + $services + ->set('jwt.verification_key') + ->class(Key::class) + ->factory(service(VerificationKeyFactory::class)); + + $services + ->set(DateTimeZone::class) + ->class(DateTimeZone::class) + ->factory(service(DateTimeZoneFactory::class)); + + $services + ->set(SystemClock::class) + ->args([ + service(DateTimeZone::class), + ]); + + $services + ->alias(Clock::class, SystemClock::class); + + $services + ->set(Signer::class) + ->factory(service(SignerFactory::class)); + + $services + ->set(Constraint\LooseValidAt::class); + + $services + ->set(Constraint\SignedWith::class) + ->arg('$key', service('jwt.verification_key')); +}; diff --git a/src/FreddieBundle.php b/src/FreddieBundle.php new file mode 100644 index 0000000..c0d1aa2 --- /dev/null +++ b/src/FreddieBundle.php @@ -0,0 +1,11 @@ +hub = $hub; + + return $this; } /** @@ -66,7 +69,7 @@ public function __invoke(ServerRequestInterface $request): ResponseInterface throw new AccessDeniedHttpException('Your rights are not sufficient to publish this update.'); } - $this->transport->publish($update); + $this->hub->publish($update); return new Response(201, body: (string) $update->message->id); } diff --git a/src/Hub/Controller/SubscribeController.php b/src/Hub/Controller/SubscribeController.php index 200b4cf..3975d82 100644 --- a/src/Hub/Controller/SubscribeController.php +++ b/src/Hub/Controller/SubscribeController.php @@ -6,8 +6,7 @@ use Freddie\Helper\FlatQueryParser; use Freddie\Hub\HubControllerInterface; -use Freddie\Hub\Transport\PHP\PHPTransport; -use Freddie\Hub\Transport\TransportInterface; +use Freddie\Hub\HubInterface; use Freddie\Message\Update; use Lcobucci\JWT\UnencryptedToken; use Psr\Http\Message\ResponseInterface; @@ -18,7 +17,6 @@ use React\Stream\WritableStreamInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\OptionsResolver\OptionsResolver; use function Freddie\extract_last_event_id; use function BenTools\QueryString\query_string; @@ -26,22 +24,13 @@ final class SubscribeController implements HubControllerInterface { - /** - * @var array - */ - private array $options; + private HubInterface $hub; - /** - * @param array $options - */ - public function __construct( - array $options = [], - private TransportInterface $transport = new PHPTransport(), - ) { - $resolver = new OptionsResolver(); - $resolver->setRequired('allow_anonymous'); - $resolver->setAllowedTypes('allow_anonymous', 'bool'); - $this->options = $resolver->resolve($options); + public function setHub(HubInterface $hub): self + { + $this->hub = $hub; + + return $this; } /** @@ -71,20 +60,20 @@ public function __invoke( if (null !== $lastEventId) { async( function () use ($lastEventId, $stream, $subscribedTopics, $allowedTopics) { - foreach ($this->transport->reconciliate($lastEventId) as $update) { + foreach ($this->hub->reconciliate($lastEventId) as $update) { $this->sendUpdate($update, $stream, $subscribedTopics, $allowedTopics); } } - ); + )(); } async( function () use ($stream, $subscribedTopics, $allowedTopics) { - $this->transport->subscribe(function (Update $update) use ($stream, $subscribedTopics, $allowedTopics) { + $this->hub->subscribe(function (Update $update) use ($stream, $subscribedTopics, $allowedTopics) { $this->sendUpdate($update, $stream, $subscribedTopics, $allowedTopics); }); } - ); + )(); return new Response( 200, @@ -103,7 +92,7 @@ private function sendUpdate( array $subscribedTopics, ?array $allowedTopics, ): void { - if (!$update->canBeReceived($subscribedTopics, $allowedTopics, $this->options['allow_anonymous'])) { + if (!$update->canBeReceived($subscribedTopics, $allowedTopics, $this->hub->getOption('allow_anonymous'))) { return; } @@ -131,7 +120,7 @@ private function extractAllowedTopics(ServerRequestInterface $request): ?array /** @var UnencryptedToken|null $jwt */ $jwt = $request->getAttribute('token'); if (null === $jwt) { - if (!$this->options['allow_anonymous']) { + if (!$this->hub->getOption('allow_anonymous')) { throw new AccessDeniedHttpException('Anonymous subscriptions are not allowed on this hub.'); } diff --git a/src/Hub/Hub.php b/src/Hub/Hub.php index 44265e1..e2f7827 100644 --- a/src/Hub/Hub.php +++ b/src/Hub/Hub.php @@ -6,18 +6,48 @@ use FrameworkX\App; use Freddie\Hub\Middleware\HttpExceptionConverterMiddleware; +use Freddie\Hub\Transport\PHP\PHPTransport; +use Freddie\Hub\Transport\TransportInterface; +use Freddie\Message\Update; +use Generator; +use InvalidArgumentException; +use React\EventLoop\Loop; +use React\Promise\PromiseInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; -final class Hub +use function array_key_exists; +use function sprintf; + +final class Hub implements HubInterface { + public const DEFAULT_OPTIONS = [ + 'allow_anonymous' => true, + ]; + + /** + * @var array + */ + private array $options; + + private bool $started = false; + /** * @codeCoverageIgnore + * @param array $options * @param iterable $controllers */ public function __construct( private App $app = new App(new HttpExceptionConverterMiddleware()), + private TransportInterface $transport = new PHPTransport(), + array $options = [], iterable $controllers = [], ) { + $resolver = new OptionsResolver(); + $resolver->setDefaults(self::DEFAULT_OPTIONS); + $resolver->setAllowedTypes('allow_anonymous', 'bool'); + $this->options = $resolver->resolve($options); foreach ($controllers as $controller) { + $controller->setHub($this); $method = $controller->getMethod(); $route = $controller->getRoute(); $this->app->{$method}($route, $controller); @@ -29,6 +59,38 @@ public function __construct( */ public function run(): void { + $this->started = true; $this->app->run(); } + + public function publish(Update $update): PromiseInterface + { + return $this->transport->publish($update) + ->then(function (Update $update) { + if (false === $this->started) { + Loop::stop(); + } + + return $update; + }); + } + + public function subscribe(callable $callback): void + { + $this->transport->subscribe($callback); + } + + public function reconciliate(string $lastEventID): Generator + { + return $this->transport->reconciliate($lastEventID); + } + + public function getOption(string $name): mixed + { + if (!array_key_exists($name, $this->options)) { + throw new InvalidArgumentException(sprintf('Invalid option `%s`.', $name)); + } + + return $this->options[$name]; + } } diff --git a/src/Hub/HubControllerInterface.php b/src/Hub/HubControllerInterface.php index 8247c78..8e2da12 100644 --- a/src/Hub/HubControllerInterface.php +++ b/src/Hub/HubControllerInterface.php @@ -11,5 +11,6 @@ interface HubControllerInterface { public function getMethod(): string; public function getRoute(): string; + public function setHub(HubInterface $hub): self; public function __invoke(ServerRequestInterface $request): ResponseInterface; } diff --git a/src/Hub/HubInterface.php b/src/Hub/HubInterface.php new file mode 100644 index 0000000..0e3b081 --- /dev/null +++ b/src/Hub/HubInterface.php @@ -0,0 +1,12 @@ +store($update); $this->eventEmitter->emit('mercureUpdate', [$update]); + + return resolve($update); } public function subscribe(callable $callback): void diff --git a/src/Hub/Transport/Redis/RedisTransport.php b/src/Hub/Transport/Redis/RedisTransport.php index 39a6ea8..2ad3f3d 100644 --- a/src/Hub/Transport/Redis/RedisTransport.php +++ b/src/Hub/Transport/Redis/RedisTransport.php @@ -9,8 +9,10 @@ use Freddie\Message\Update; use Generator; use React\EventLoop\Loop; +use React\Promise\PromiseInterface; use function React\Async\await; +use function React\Promise\resolve; final class RedisTransport implements TransportInterface { @@ -35,12 +37,14 @@ public function subscribe(callable $callback): void }); } - public function publish(Update $update): void + public function publish(Update $update): PromiseInterface { $this->init(); $payload = $this->serializer->serialize($update); - $this->redis->publish($this->channel, $payload); // @phpstan-ignore-line - $this->store($update); + + return $this->redis->publish($this->channel, $payload) // @phpstan-ignore-line + ->then(fn() => $this->store($update)) + ->then(fn() => $update); } public function reconciliate(string $lastEventID): Generator @@ -63,14 +67,14 @@ public function reconciliate(string $lastEventID): Generator } } - private function store(Update $update): void + private function store(Update $update): PromiseInterface { $this->init(); if ($this->size <= 0) { - return; + return resolve(); } - $this->redis->rpush($this->storageKey, $this->serializer->serialize($update)); // @phpstan-ignore-line + return $this->redis->rpush($this->storageKey, $this->serializer->serialize($update)); // @phpstan-ignore-line } private function init(): void diff --git a/src/Hub/Transport/TransportInterface.php b/src/Hub/Transport/TransportInterface.php index d0d7bf4..189d0ec 100644 --- a/src/Hub/Transport/TransportInterface.php +++ b/src/Hub/Transport/TransportInterface.php @@ -6,12 +6,16 @@ use Freddie\Message\Update; use Generator; +use React\Promise\PromiseInterface; interface TransportInterface { public const EARLIEST = 'earliest'; - public function publish(Update $update): void; + /** + * @return PromiseInterface + */ + public function publish(Update $update): PromiseInterface; public function subscribe(callable $callback): void; diff --git a/symfony.lock b/symfony.lock index 5d6f238..6e2dde9 100644 --- a/symfony.lock +++ b/symfony.lock @@ -323,6 +323,9 @@ "symfony/options-resolver": { "version": "v6.0.0-RC1" }, + "symfony/polyfill-ctype": { + "version": "v1.24.0" + }, "symfony/polyfill-intl-grapheme": { "version": "v1.23.1" }, diff --git a/tests/Unit/Hub/Controller/PublishControllerTest.php b/tests/Unit/Hub/Controller/PublishControllerTest.php index 137a020..98d9a9f 100644 --- a/tests/Unit/Hub/Controller/PublishControllerTest.php +++ b/tests/Unit/Hub/Controller/PublishControllerTest.php @@ -6,6 +6,7 @@ use FrameworkX\App; use Freddie\Hub\Controller\PublishController; +use Freddie\Hub\Hub; use Freddie\Hub\Middleware\HttpExceptionConverterMiddleware; use Freddie\Hub\Middleware\TokenExtractorMiddleware; use Freddie\Hub\Transport\PHP\PHPTransport; @@ -31,7 +32,7 @@ ?Update $expectedUpdate ) { $transport = new PHPTransport(size: 1); - $controller = new PublishController($transport); + $controller = new PublishController(); $app = new App( new TokenExtractorMiddleware( jwt_config()->parser(), @@ -40,6 +41,8 @@ new HttpExceptionConverterMiddleware(), $controller, ); + $hub = new Hub($app, $transport); + $controller->setHub($hub); $transportRefl = new ReflectionClass($transport); $updates = $transportRefl->getProperty('updates'); $updates->setAccessible(true); diff --git a/tests/Unit/Hub/Controller/SubscribeControllerTest.php b/tests/Unit/Hub/Controller/SubscribeControllerTest.php index 231c2aa..a79be72 100644 --- a/tests/Unit/Hub/Controller/SubscribeControllerTest.php +++ b/tests/Unit/Hub/Controller/SubscribeControllerTest.php @@ -6,6 +6,7 @@ use FrameworkX\App; use Freddie\Hub\Controller\SubscribeController; +use Freddie\Hub\Hub; use Freddie\Hub\Middleware\HttpExceptionConverterMiddleware; use Freddie\Hub\Transport\PHP\PHPTransport; use Freddie\Message\Message; @@ -20,11 +21,8 @@ it('receives updates and dumps them into the stream', function () { $transport = new PHPTransport(size: 1000); - $controller = new SubscribeController(['allow_anonymous' => true], $transport); - $app = new App( - new HttpExceptionConverterMiddleware(), - $controller, - ); + $controller = new SubscribeController(); + $controller->setHub(new Hub(transport: $transport)); $stream = new ThroughStreamStub(); // Given @@ -57,7 +55,8 @@ it('receives private updates when authorized', function () { $transport = new PHPTransport(size: 1000); - $controller = new SubscribeController(['allow_anonymous' => true], $transport); + $controller = new SubscribeController(); + $controller->setHub(new Hub(transport: $transport)); $stream = new ThroughStreamStub(); // Given @@ -97,7 +96,8 @@ }); it('yells if user doesn\'t subscribe to at least one topic', function () { - $controller = new SubscribeController(['allow_anonymous' => true]); + $controller = new SubscribeController(); + $controller->setHub(new Hub(options: ['allow_anonymous' => true])); // Given $request = new ServerRequest( @@ -116,7 +116,8 @@ ); it('yells when anonymous subscriptions are forbidden and user doesn\'t provide a JWT', function () { - $controller = new SubscribeController(['allow_anonymous' => false]); + $controller = new SubscribeController(); + $controller->setHub(new Hub(options: ['allow_anonymous' => false])); // Given $request = new ServerRequest( @@ -135,7 +136,8 @@ ); it('complains if JWT is invalid', function () { - $controller = new SubscribeController(['allow_anonymous' => false]); + $controller = new SubscribeController(); + $controller->setHub(new Hub(options: ['allow_anonymous' => false])); // Given $jwt = create_jwt(['mercure' => ['publish' => ['*']]]) . 'foo'; diff --git a/tests/Unit/Hub/HubTest.php b/tests/Unit/Hub/HubTest.php new file mode 100644 index 0000000..98738d6 --- /dev/null +++ b/tests/Unit/Hub/HubTest.php @@ -0,0 +1,62 @@ +called['publish'] = func_get_args(); + + return resolve($update); + } + + public function subscribe(callable $callback): void + { + $this->called['subscribe'] = func_get_args(); + } + + public function reconciliate(string $lastEventID): Generator + { + $this->called['reconciliate'] = func_get_args(); + yield; + } + }; + + // Given + $hub = new Hub(transport: $transport); + $update = new Update(['foo'], new Message(Ulid::generate())); + $subscribeFn = fn () => 'bar'; + $lastEventId = Ulid::generate(); + + // When + $hub->publish($update); + $hub->subscribe($subscribeFn); + iterator_to_array($hub->reconciliate($lastEventId)); + + // Then + expect($transport->called['publish'])->toBe([$update]); + expect($transport->called['subscribe'])->toBe([$subscribeFn]); + expect($transport->called['reconciliate'])->toBe([$lastEventId]); +}); + +it('complains when requesting an unrecognized option', function () { + $hub = new Hub(); + $hub->getOption('foo'); +})->throws(InvalidArgumentException::class, 'Invalid option `foo`.'); diff --git a/tests/Unit/Hub/Transport/Redis/RedisClientStub.php b/tests/Unit/Hub/Transport/Redis/RedisClientStub.php index a84a8c4..1485c00 100644 --- a/tests/Unit/Hub/Transport/Redis/RedisClientStub.php +++ b/tests/Unit/Hub/Transport/Redis/RedisClientStub.php @@ -8,13 +8,14 @@ use Clue\React\Redis\Client; use Evenement\EventEmitter; use Evenement\EventEmitterInterface; -use Evenement\EventEmitterTrait; use Pest\Exceptions\ShouldNotHappen; +use React\Promise\PromiseInterface; use function abs; use function array_splice; use function count; use function React\Async\async; +use function React\Promise\resolve; final class RedisClientStub implements Client { @@ -31,16 +32,20 @@ public function subscribe(string $channel): void $this->subscribedChannels[] = $channel; } - public function publish(string $channel, string $payload): void + public function publish(string $channel, string $payload): PromiseInterface { $this->emit('message', [$channel, $payload]); + + return resolve(true); } - public function rpush(string $key, string ...$items): void + public function rpush(string $key, string ...$items): PromiseInterface { foreach ($items as $item) { $this->storage[$key][] = $item; } + + return resolve(true); } public function lrange(string $key, int $from, int $to) @@ -54,12 +59,12 @@ public function lrange(string $key, int $from, int $to) $length -= $from; - return async(fn () => array_splice($items, $firstIndex, $length)); + return async(fn () => array_splice($items, $firstIndex, $length))(); } public function ltrim(string $key, int $from, int $to) { - return async(fn () => $this->lrange($key, $from, $to)) + return async(fn () => $this->lrange($key, $from, $to))() ->then(fn (array $items) => $this->storage[$key] = $items); }