diff --git a/.github/workflows/split_monorepo.yml b/.github/workflows/split_monorepo.yml
index b50af0112..602eea909 100644
--- a/.github/workflows/split_monorepo.yml
+++ b/.github/workflows/split_monorepo.yml
@@ -38,6 +38,8 @@ jobs:
split_repository: 'auth-http'
- local_path: 'Boot'
split_repository: 'boot'
+ - local_path: 'Broadcasting'
+ split_repository: 'broadcasting'
- local_path: 'Cache'
split_repository: 'cache'
- local_path: 'Config'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0c35563e..ef62ab6a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@
Please, use standalone package `spiral/data-grid-bridge` instead.
- Component `spiral/data-grid` is removed from `spiral/framework` repository.
Please, use standalone package `spiral/data-grid` instead.
- - `Spiral\Boot\ExceptionHandler` has been eliminated. New `Spiral\Exceptions\ExceptionHandler` with interfaces
+ - `Spiral\Boot\ExceptionHandler` has been eliminated. New `Spiral\Exceptions\ExceptionHandler` with interfaces
`Spiral\Exceptions\ExceptionHandlerInterface`, `Spiral\Exceptions\ExceptionRendererInterface` and
`Spiral\Exceptions\ExceptionReporterInterface` have been added.
- Added `ExceptionHandlerBootloader` that adds renderers and reporters in the `ExceptionHandler`.
@@ -39,7 +39,7 @@
- Removed `Spiral\Http\SapiDispatcher` and `Spiral\Http\Emitter\SapiEmitter`. Please, use package `spiral/sapi-bridge` instead.
- Bootloader `Spiral\Bootloader\Http\DiactorosBootloader` moved to the `Spiral\Http\Bootloader\DiactorosBootloader`.
- Classes `Spiral\Http\Diactoros\ResponseFactory`, `Spiral\Http\Diactoros\ServerRequestFactory`, `Spiral\Http\Diactoros\StreamFactory`,
- `Spiral\Http\Diactoros\UploadedFileFactory`, `Spiral\Http\Diactoros\UriFactory`
+ `Spiral\Http\Diactoros\UploadedFileFactory`, `Spiral\Http\Diactoros\UriFactory`
is removed and replaced to a `Nyholm\Psr7\Factory\Psr17Factory` class from `nyholm/psr7` package in `DiactorosBootloader`.
- [spiral/exceptions] All handlers have been renamed into renderers. `HandlerInterface` has been deleted.
- [spiral/exceptions] Added `Spiral\Exceptions\Verbosity` enum.
@@ -111,7 +111,7 @@
- [spiral/boot] `Spiral\Boot\AbstractKernel` constructor is protected now.
- [spiral/boot] Added return type `mixed` to the method `loadData`,
added return type `void` and `mixed` parameter type of `$data` to the method `saveData` in `Spiral\Boot\MemoryInterface` interface.
- - [spiral/boot] In `Bootloaders`, the name of the method has been changed from `boot` to `init`.
+ - [spiral/boot] In `Bootloaders`, the name of the method has been changed from `boot` to `init`.
In the code of custom Bootloaders, need to change the name of the method.
- [spiral/console] Added return type `void` to the method `writeHeader`, added return type `void` to the method `execute`,
method `whiteFooter` renamed to `writeFooter`, added return type `void` to the method `writeFooter`
@@ -142,7 +142,7 @@
config `Spiral\Bootloader\Storage\StorageConfig` moved to the `Spiral\Storage\Config\StorageConfig`.
- [spiral/validation] Bootloader `Spiral\Bootloader\Security\ValidationBootloader` moved to the `Spiral\Validation\Bootloader\ValidationBootloader`.
- [spiral/views] Bootloader `Spiral\Bootloader\Views\ViewsBootloader` moved to the `Spiral\Views\Bootloader\ViewsBootloader`.
- - [spiral/boot] By default, overwriting of environment variable values is disabled, the default value for `$overwrite`
+ - [spiral/boot] By default, overwriting of environment variable values is disabled, the default value for `$overwrite`
changed from `true` to `false` in the `Spiral\Boot\Environment`.
- **Medium Impact Changes**
- A minimal version of `PHP` increased to `^8.1`
@@ -156,11 +156,32 @@
- [spiral/debug] Added `Spiral\Debug\StateConsumerInterface`.
- [spiral/boot] Added new `boot` method in `Bootloaders`. It will be executed after the `init` method is executed in all `Bootloaders`.
The old `boot` method has been renamed to `init`. See **High Impact Changes** section.
- - [spiral/boot] Added automatic booting of `Bootloaders` requested in the `init` and `boot` methods.
+ - [spiral/boot] Added automatic booting of `Bootloaders` requested in the `init` and `boot` methods.
They no longer need to be specified explicitly in `DEPENDENCIES` property or in `defineDependencies` method.
- - [spiral/monolog-bridge] Added the ability to configure the default channel using the configuration file or
+ - [spiral/monolog-bridge] Added the ability to configure the default channel using the configuration file or
environment variable `MONOLOG_DEFAULT_CHANNEL`.
-
+
+## v2.14.0 - Unreleased
+- **High Impact Changes**
+- **Medium Impact Changes**
+- **Low Impact Changes**
+- **Other Features**
+- **Bug Fixes**
+
+## v2.13.0 - 2022-04-28
+- **Medium Impact Changes**
+ - Dispatcher `Spiral\Http\SapiDispatcher` is deprecated. Will be moved to `spiral/sapi-bridge` and removed in v3.0
+ - Classes `Spiral\Http\Emitter\SapiEmitter`, `Spiral\Http\Exception\EmitterException`, `Spiral\Http\EmitterInterface`,
+ `Spiral\Http\SapiRequestFactory` is deprecated. Will be removed in version v3.0.
+ After the release of v3.0, must use the package `spiral/sapi-bridge` for SAPI functionality.
+ - The `dumper` component is deprecated and will be removed in v3.0
+- **Other Features**
+ - [spiral/http] Added parameter `chunkSize` in the `http` configuration file.
+ - [spiral/queue] Added attribute `Queueable` to mark classes that can be queued.
+ Added `Spiral\Queue\QueueableDetector` class to easily check if an object should be queued or not and get the queue
+ from an attribute or getQueue method on the object.
+ - [spiral/broadcasting] New component with common interfaces (RR2.0 support)
+
## v2.12.0 - 2022-04-07
- **Medium Impact Changes**
- Bootloaders `Spiral\Bootloader\Broadcast\BroadcastBootloader`, `Spiral\Bootloader\Http\WebsocketsBootloader`
diff --git a/composer.json b/composer.json
index 15d917d25..89b3a1943 100644
--- a/composer.json
+++ b/composer.json
@@ -68,6 +68,7 @@
"src/AuthHttp/src"
],
"Spiral\\Boot\\": "src/Boot/src",
+ "Spiral\\Broadcasting\\": "src/Broadcasting/src",
"Spiral\\Cache\\": "src/Cache/src",
"Spiral\\Config\\": "src/Config/src",
"Spiral\\Console\\": "src/Console/src",
@@ -133,7 +134,7 @@
"spiral/code-style": "^1.0",
"symfony/var-dumper": "^5.2|^6.0",
"symplify/monorepo-builder": "^10.0",
- "vimeo/psalm": "^4.22"
+ "vimeo/psalm": "4.21"
},
"autoload-dev": {
"psr-4": {
@@ -143,6 +144,7 @@
"src/Auth/tests",
"src/AuthHttp/tests"
],
+ "Spiral\\Tests\\Broadcasting\\": "src/Broadcasting/tests",
"Spiral\\Tests\\Boot\\": "src/Boot/tests",
"Spiral\\Tests\\Cache\\": "src/Cache/tests",
"Spiral\\Tests\\Config\\": "src/Config/tests",
@@ -200,6 +202,7 @@
"spiral/auth": "self.version",
"spiral/auth-http": "self.version",
"spiral/boot": "self.version",
+ "spiral/broadcasting": "self.version",
"spiral/cache": "self.version",
"spiral/config": "self.version",
"spiral/console": "self.version",
diff --git a/monorepo-builder.php b/monorepo-builder.php
index c31fce8de..549db74f3 100644
--- a/monorepo-builder.php
+++ b/monorepo-builder.php
@@ -89,7 +89,7 @@
'spiral/code-style' => '^1.0',
'laminas/laminas-hydrator' => '^3.0|^4.0',
'symplify/monorepo-builder' => '^10.0',
- 'vimeo/psalm' => '^4.22',
+ 'vimeo/psalm' => '^4.21',
],
]);
diff --git a/src/AnnotatedRoutes/composer.json b/src/AnnotatedRoutes/composer.json
index 36f72e754..a20b0731f 100644
--- a/src/AnnotatedRoutes/composer.json
+++ b/src/AnnotatedRoutes/composer.json
@@ -16,8 +16,8 @@
],
"require": {
"php": ">=8.1",
- "spiral/attributes": "^2.13",
- "spiral/router": "^2.13"
+ "spiral/attributes": "^2.14",
+ "spiral/router": "^2.14"
},
"autoload": {
"psr-4": {
@@ -28,8 +28,8 @@
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
"laminas/laminas-diactoros": "^2.8",
- "spiral/boot": "^2.13",
- "spiral/console": "^2.13"
+ "spiral/boot": "^2.14",
+ "spiral/console": "^2.14"
},
"autoload-dev": {
"psr-4": {
@@ -38,7 +38,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Attributes/composer.json b/src/Attributes/composer.json
index e25cd3641..1fd56d8ee 100644
--- a/src/Attributes/composer.json
+++ b/src/Attributes/composer.json
@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"scripts": {
diff --git a/src/Auth/composer.json b/src/Auth/composer.json
index fa0db7bc8..4e685b983 100644
--- a/src/Auth/composer.json
+++ b/src/Auth/composer.json
@@ -32,7 +32,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/AuthHttp/composer.json b/src/AuthHttp/composer.json
index 66982ad1c..cebc58748 100644
--- a/src/AuthHttp/composer.json
+++ b/src/AuthHttp/composer.json
@@ -16,15 +16,15 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
- "spiral/auth": "^2.13",
+ "spiral/auth": "^2.14",
"psr/http-message": "^1.0",
"psr/http-server-middleware": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
- "spiral/cookies": "^2.13",
- "spiral/http": "^2.13",
- "spiral/debug": "^2.13",
+ "spiral/cookies": "^2.14",
+ "spiral/http": "^2.14",
+ "spiral/debug": "^2.14",
"laminas/laminas-diactoros": "^2.8"
},
"autoload": {
@@ -39,7 +39,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Boot/composer.json b/src/Boot/composer.json
index 36f738e1f..d6bd6de08 100644
--- a/src/Boot/composer.json
+++ b/src/Boot/composer.json
@@ -16,11 +16,11 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/files": "^2.13",
- "spiral/config": "^2.13",
- "spiral/debug": "^2.13",
- "spiral/exceptions": "^2.13"
+ "spiral/core": "^2.14",
+ "spiral/files": "^2.14",
+ "spiral/config": "^2.14",
+ "spiral/debug": "^2.14",
+ "spiral/exceptions": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
@@ -41,7 +41,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Bridge/Dotenv/composer.json b/src/Bridge/Dotenv/composer.json
index b07227b83..db2729177 100644
--- a/src/Bridge/Dotenv/composer.json
+++ b/src/Bridge/Dotenv/composer.json
@@ -16,7 +16,7 @@
],
"require": {
"php": ">=8.1",
- "spiral/boot": "^2.13",
+ "spiral/boot": "^2.14",
"vlucas/phpdotenv": "^5.4"
},
"require-dev": {
@@ -34,7 +34,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Bridge/Monolog/composer.json b/src/Bridge/Monolog/composer.json
index 9fedc8f36..8a753c4e1 100644
--- a/src/Bridge/Monolog/composer.json
+++ b/src/Bridge/Monolog/composer.json
@@ -16,7 +16,7 @@
],
"require": {
"php": ">=8.1",
- "spiral/boot": "^2.13",
+ "spiral/boot": "^2.14",
"monolog/monolog": "^2.2"
},
"require-dev": {
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Bridge/Stempler/composer.json b/src/Bridge/Stempler/composer.json
index d011f8cf2..f6754f8ed 100644
--- a/src/Bridge/Stempler/composer.json
+++ b/src/Bridge/Stempler/composer.json
@@ -16,19 +16,19 @@
],
"require": {
"php": ">=8.1",
- "spiral/stempler": "^2.13",
- "spiral/boot": "^2.13",
- "spiral/files": "^2.13",
- "spiral/config": "^2.13",
- "spiral/translator": "^2.13",
- "spiral/router": "^2.13",
- "spiral/views": "^2.13",
- "spiral/core": "^2.13"
+ "spiral/stempler": "^2.14",
+ "spiral/boot": "^2.14",
+ "spiral/files": "^2.14",
+ "spiral/config": "^2.14",
+ "spiral/translator": "^2.14",
+ "spiral/router": "^2.14",
+ "spiral/views": "^2.14",
+ "spiral/core": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
- "spiral/dumper": "^2.13"
+ "spiral/dumper": "^2.14"
},
"autoload": {
"psr-4": {
@@ -42,7 +42,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Broadcasting/.gitattributes b/src/Broadcasting/.gitattributes
new file mode 100644
index 000000000..f68d9df1d
--- /dev/null
+++ b/src/Broadcasting/.gitattributes
@@ -0,0 +1,7 @@
+/.github export-ignore
+/tests export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.travis.yml export-ignore
+phpunit.xml export-ignore
diff --git a/src/Broadcasting/.gitignore b/src/Broadcasting/.gitignore
new file mode 100644
index 000000000..5b340c146
--- /dev/null
+++ b/src/Broadcasting/.gitignore
@@ -0,0 +1,19 @@
+# IDEA
+.idea/
+*.iml
+
+# Composer
+vendor/
+composer.lock
+composer.phar
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Other
+.phpunit.result.cache
+.php_cs.cache
+clover.xml
+.env
+builds
diff --git a/src/Broadcasting/LICENSE b/src/Broadcasting/LICENSE
new file mode 100644
index 000000000..80ffcbeb1
--- /dev/null
+++ b/src/Broadcasting/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 Spiral Scout
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/src/Broadcasting/README.md b/src/Broadcasting/README.md
new file mode 100644
index 000000000..df7aaf227
--- /dev/null
+++ b/src/Broadcasting/README.md
@@ -0,0 +1,5 @@
+# Spiral: Common Broadcasting Interfaces
+
+[![Latest Stable Version](https://poser.pugx.org/spiral/broadcasting/version)](https://packagist.org/packages/spiral/broadcasting)
+[![Build Status](https://travis-ci.org/spiral/broadcasting.svg?branch=master)](https://travis-ci.org/spiral/broadcasting)
+[![Codecov](https://codecov.io/gh/spiral/broadcasting/branch/master/graph/badge.svg)](https://codecov.io/gh/spiral/broadcasting/)
diff --git a/src/Broadcasting/composer.json b/src/Broadcasting/composer.json
new file mode 100644
index 000000000..75b11c0e4
--- /dev/null
+++ b/src/Broadcasting/composer.json
@@ -0,0 +1,54 @@
+{
+ "name": "spiral/broadcasting",
+ "type": "library",
+ "description": "Common Broadcasting Interfaces",
+ "license": "MIT",
+ "homepage": "https://spiral.dev",
+ "support": {
+ "issues": "https://github.com/spiral/framework/issues",
+ "source": "https://github.com/spiral/core"
+ },
+ "authors": [
+ {
+ "name": "Anton Titov (wolfy-j)",
+ "email": "wolfy-j@spiralscout.com"
+ },
+ {
+ "name": "Pavel Buchnev",
+ "email": "pavel.buchnev@spiralscout.com"
+ }
+ ],
+ "require": {
+ "php": ">=7.4",
+ "psr/http-message": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "psr/log": "1 - 3",
+ "spiral/core": "^2.14",
+ "spiral/config": "^2.14"
+ },
+ "autoload": {
+ "psr-4": {
+ "Spiral\\Broadcasting\\": "src"
+ }
+ },
+ "require-dev": {
+ "spiral/boot": "^2.14",
+ "mockery/mockery": "^1.5",
+ "phpunit/phpunit": "^8.5|^9.5"
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Spiral\\Tests\\Broadcasting\\": "tests"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.14.x-dev"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/src/Broadcasting/phpunit.xml b/src/Broadcasting/phpunit.xml
new file mode 100644
index 000000000..a988141bb
--- /dev/null
+++ b/src/Broadcasting/phpunit.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Broadcasting/src/AuthorizationStatus.php b/src/Broadcasting/src/AuthorizationStatus.php
new file mode 100644
index 000000000..d4516b6c7
--- /dev/null
+++ b/src/Broadcasting/src/AuthorizationStatus.php
@@ -0,0 +1,64 @@
+success = $success;
+ $this->topics = $topics;
+ $this->attributes = $attributes;
+ $this->response = $response;
+ }
+
+ /**
+ * Check if authorization status is successful.
+ */
+ public function isSuccessful(): bool
+ {
+ return $this->success;
+ }
+
+ /**
+ * Get list of authorized topics.
+ */
+ public function getTopics(): array
+ {
+ return $this->topics;
+ }
+
+ public function getAttributes(): array
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * Check if response is set.
+ */
+ public function hasResponse(): bool
+ {
+ return $this->response !== null;
+ }
+
+ /**
+ * Get response object.
+ */
+ public function getResponse(): ?ResponseInterface
+ {
+ return $this->response;
+ }
+}
diff --git a/src/Broadcasting/src/Bootloader/BroadcastingBootloader.php b/src/Broadcasting/src/Bootloader/BroadcastingBootloader.php
new file mode 100644
index 000000000..c5d841a87
--- /dev/null
+++ b/src/Broadcasting/src/Bootloader/BroadcastingBootloader.php
@@ -0,0 +1,81 @@
+ BroadcastManager::class,
+ BroadcastInterface::class => [self::class, 'initDefaultBroadcast'],
+ TopicRegistryInterface::class => [self::class, 'initTopicRegistry'],
+ ];
+
+ private ConfiguratorInterface $config;
+
+ public function __construct(ConfiguratorInterface $config)
+ {
+ $this->config = $config;
+ }
+
+ public function registerDriverAlias(string $driverClass, string $alias): void
+ {
+ $this->config->modify(
+ BroadcastConfig::CONFIG,
+ new Append('driverAliases', $alias, $driverClass)
+ );
+ }
+
+ public function boot(EnvironmentInterface $env): void
+ {
+ $this->initConfig($env);
+ }
+
+ private function initConfig(EnvironmentInterface $env): void
+ {
+ $this->config->setDefaults(
+ BroadcastConfig::CONFIG,
+ [
+ 'default' => $env->get('BROADCAST_CONNECTION', 'null'),
+ 'authorize' => [
+ 'path' => $env->get('BROADCAST_AUTHORIZE_PATH'),
+ 'topics' => [],
+ ],
+ 'aliases' => [],
+ 'connections' => [
+ 'null' => [
+ 'driver' => 'null',
+ ],
+ ],
+ 'driverAliases' => [
+ 'null' => NullBroadcast::class,
+ 'log' => LogBroadcast::class,
+ ],
+ ]
+ );
+ }
+
+ private function initDefaultBroadcast(BroadcastManagerInterface $manager): BroadcastInterface
+ {
+ return $manager->connection();
+ }
+
+ private function initTopicRegistry(BroadcastConfig $config): TopicRegistryInterface
+ {
+ return new TopicRegistry($config->getTopics());
+ }
+}
diff --git a/src/Broadcasting/src/Bootloader/WebsocketsBootloader.php b/src/Broadcasting/src/Bootloader/WebsocketsBootloader.php
new file mode 100644
index 000000000..6b36514a1
--- /dev/null
+++ b/src/Broadcasting/src/Bootloader/WebsocketsBootloader.php
@@ -0,0 +1,42 @@
+bindSingleton(AuthorizationMiddleware::class, static function (
+ BroadcastInterface $broadcast,
+ ResponseFactoryInterface $responseFactory,
+ BroadcastConfig $config
+ ): AuthorizationMiddleware {
+ return new AuthorizationMiddleware(
+ $broadcast,
+ $responseFactory,
+ $config->getAuthorizationPath()
+ );
+ });
+
+
+ if ($config->getAuthorizationPath() !== null) {
+ $http->addMiddleware(AuthorizationMiddleware::class);
+ }
+ }
+}
diff --git a/src/Broadcasting/src/BroadcastInterface.php b/src/Broadcasting/src/BroadcastInterface.php
new file mode 100644
index 000000000..8c606d904
--- /dev/null
+++ b/src/Broadcasting/src/BroadcastInterface.php
@@ -0,0 +1,38 @@
+ | non-empty-list | string | Stringable
+ * @psalm-type MessagesList = non-empty-list | non-empty-list | string | Stringable
+ */
+interface BroadcastInterface
+{
+ /**
+ * Method to send messages to the required topic (channel).
+ *
+ * $broadcast->publish('topic', 'message');
+ * $broadcast->publish('topic', ['message 1', 'message 2']);
+ *
+ * $broadcast->publish(['topic 1', 'topic 2'], 'message');
+ * $broadcast->publish(['topic 1', 'topic 2'], ['message 1', 'message 2']);
+ *
+ *
+ * Note: In future major releases, the signature of this method will be
+ * changed to include follow type-hints.
+ *
+ *
+ * public function publish(iterable|string|\Stringable $topics, iterable|string $messages): void;
+ *
+ *
+ * @param TopicsList $topics
+ * @param MessagesList $messages
+ * @throws BroadcastException
+ */
+ public function publish($topics, $messages): void;
+}
diff --git a/src/Broadcasting/src/BroadcastManager.php b/src/Broadcasting/src/BroadcastManager.php
new file mode 100644
index 000000000..cc2ea14ae
--- /dev/null
+++ b/src/Broadcasting/src/BroadcastManager.php
@@ -0,0 +1,46 @@
+factory = $factory;
+ $this->config = $config;
+ }
+
+ public function connection(?string $name = null): BroadcastInterface
+ {
+ $name = $name ?: $this->config->getDefaultConnection();
+
+ // Replaces alias with real storage name
+ $name = $this->config->getAliases()[$name] ?? $name;
+
+ if (isset($this->connections[$name])) {
+ return $this->connections[$name];
+ }
+
+ return $this->connections[$name] = $this->resolve($name);
+ }
+
+ private function resolve(string $name): BroadcastInterface
+ {
+ $config = $this->config->getConnectionConfig($name);
+
+ return $this->factory->make($config['driver'], $config);
+ }
+}
diff --git a/src/Broadcasting/src/BroadcastManagerInterface.php b/src/Broadcasting/src/BroadcastManagerInterface.php
new file mode 100644
index 000000000..2896e396d
--- /dev/null
+++ b/src/Broadcasting/src/BroadcastManagerInterface.php
@@ -0,0 +1,14 @@
+ [
+ 'path' => null,
+ 'topics' => [],
+ ],
+ 'default' => 'null',
+ 'aliases' => [],
+ 'connections' => [],
+ 'driverAliases' => [],
+ ];
+
+ /**
+ * Get registerer broadcast topics.
+ *
+ * @return array
+ */
+ public function getTopics(): array
+ {
+ return (array)($this->config['authorize']['topics'] ?? []);
+ }
+
+ /**
+ * Get authorization path for broadcasting topics.
+ */
+ public function getAuthorizationPath(): ?string
+ {
+ return $this->config['authorize']['path'] ?? null;
+ }
+
+ /**
+ * Get broadcast driver aliases
+ */
+ public function getAliases(): array
+ {
+ return (array)($this->config['aliases'] ?? []);
+ }
+
+ /**
+ * Get default broadcast connection
+ */
+ public function getDefaultConnection(): string
+ {
+ if (!isset($this->config['default']) || empty($this->config['default'])) {
+ throw new InvalidArgumentException('Default broadcast connection is not defined.');
+ }
+
+ if (!\is_string($this->config['default'])) {
+ throw new InvalidArgumentException('Default broadcast connection config value must be a string');
+ }
+
+ return $this->config['default'];
+ }
+
+ public function getConnectionConfig(string $name): array
+ {
+ if (!isset($this->config['connections'][$name])) {
+ throw new InvalidArgumentException(
+ sprintf('Config for connection `%s` is not defined.', $name)
+ );
+ }
+
+ $config = $this->config['connections'][$name];
+
+ if (!isset($config['driver'])) {
+ throw new InvalidArgumentException(
+ sprintf('Driver for `%s` connection is not defined.', $name)
+ );
+ }
+
+ if (!\is_string($config['driver'])) {
+ throw new InvalidArgumentException(
+ \sprintf('Driver value for `%s` connection must be a string', $name)
+ );
+ }
+
+ if (isset($this->config['driverAliases'][$config['driver']])) {
+ $config['driver'] = $this->config['driverAliases'][$config['driver']];
+ }
+
+ return $config;
+ }
+}
diff --git a/src/Broadcasting/src/Driver/AbstractBroadcast.php b/src/Broadcasting/src/Driver/AbstractBroadcast.php
new file mode 100644
index 000000000..ed162d666
--- /dev/null
+++ b/src/Broadcasting/src/Driver/AbstractBroadcast.php
@@ -0,0 +1,42 @@
+|T $entries
+ * @return array
+ */
+ protected function toArray($entries): array
+ {
+ switch (true) {
+ case \is_array($entries):
+ return $entries;
+
+ case $entries instanceof \Traversable:
+ return \iterator_to_array($entries, false);
+
+ default:
+ return [$entries];
+ }
+ }
+}
diff --git a/src/Broadcasting/src/Driver/LogBroadcast.php b/src/Broadcasting/src/Driver/LogBroadcast.php
new file mode 100644
index 000000000..1470a50a2
--- /dev/null
+++ b/src/Broadcasting/src/Driver/LogBroadcast.php
@@ -0,0 +1,31 @@
+logger = $logger;
+ $this->level = $level;
+ }
+
+ public function publish($topics, $messages): void
+ {
+ $topics = implode(', ', $this->formatTopics($this->toArray($topics)));
+
+ /** @var string $message */
+ foreach ($this->toArray($messages) as $message) {
+ assert(\is_string($message), 'Message argument must be a type of string');
+ $this->logger->log($this->level, 'Broadcasting on channels [' . $topics . '] with payload: ' . $message);
+ }
+ }
+}
diff --git a/src/Broadcasting/src/Driver/NullBroadcast.php b/src/Broadcasting/src/Driver/NullBroadcast.php
new file mode 100644
index 000000000..ca2b4c348
--- /dev/null
+++ b/src/Broadcasting/src/Driver/NullBroadcast.php
@@ -0,0 +1,15 @@
+responseFactory = $responseFactory;
+ $this->broadcast = $broadcast;
+ $this->authorizationPath = $authorizationPath;
+ }
+
+ public function process(
+ ServerRequestInterface $request,
+ RequestHandlerInterface $handler
+ ): ResponseInterface {
+ if ($request->getUri()->getPath() !== $this->authorizationPath) {
+ return $handler->handle($request);
+ }
+
+ if ($this->broadcast instanceof GuardInterface) {
+ $status = $this->broadcast->authorize($request);
+
+ if ($status->hasResponse()) {
+ return $status->getResponse();
+ }
+
+ if (!$status->isSuccessful()) {
+ return $this->responseFactory->createResponse(403);
+ }
+ }
+
+ return $this->responseFactory->createResponse(200);
+ }
+}
diff --git a/src/Broadcasting/src/TopicRegistry.php b/src/Broadcasting/src/TopicRegistry.php
new file mode 100644
index 000000000..bcf70bb48
--- /dev/null
+++ b/src/Broadcasting/src/TopicRegistry.php
@@ -0,0 +1,46 @@
+ $callback) {
+ $this->register($topic, $callback);
+ }
+ }
+
+ public function register(string $topic, callable $callback): void
+ {
+ $this->patterns[$this->compilePattern($topic)] = $callback;
+ }
+
+ public function findCallback(string $topic, array &$matches): ?callable
+ {
+ foreach ($this->patterns as $pattern => $callback) {
+ if (preg_match($pattern, $topic, $matches)) {
+ return $callback;
+ }
+ }
+
+ return null;
+ }
+
+ private function compilePattern(string $topic): string
+ {
+ $replaces = [];
+ if (preg_match_all('/\{(\w+):?(.*?)?\}/', $topic, $matches)) {
+ $variables = array_combine($matches[1], $matches[2]);
+ foreach ($variables as $key => $_) {
+ $replaces['{' . $key . '}'] = '(?P<' . $key . '>[^\/\.]+)';
+ }
+ }
+
+ return '/^' . strtr($topic, $replaces + ['/' => '\\/', '[' => '(?:', ']' => ')?', '.' => '\.']) . '$/iu';
+ }
+}
diff --git a/src/Broadcasting/src/TopicRegistryInterface.php b/src/Broadcasting/src/TopicRegistryInterface.php
new file mode 100644
index 000000000..4752cd941
--- /dev/null
+++ b/src/Broadcasting/src/TopicRegistryInterface.php
@@ -0,0 +1,12 @@
+assertFalse($false->isSuccessful());
+
+ $true = new AuthorizationStatus(true, []);
+ $this->assertTrue($true->isSuccessful());
+ }
+
+ public function testGetsTopics(): void
+ {
+ $status = new AuthorizationStatus(false, $topics = ['topic1', 'topic2']);
+ $this->assertSame($topics, $status->getTopics());
+ }
+
+ public function testGetsAttributes(): void
+ {
+ $status = new AuthorizationStatus(false, ['topic1'], $attributes = ['foo' => 'bar']);
+ $this->assertSame($attributes, $status->getAttributes());
+ }
+
+ public function testGetsNullResponse(): void
+ {
+ $status = new AuthorizationStatus(false, ['topic1'], ['foo' => 'bar']);
+ $this->assertNull($status->getResponse());
+ $this->assertFalse($status->hasResponse());
+ }
+
+ public function testGetsResponse(): void
+ {
+ $status = new AuthorizationStatus(
+ false,
+ ['topic1'],
+ ['foo' => 'bar'],
+ $response = m::mock(ResponseInterface::class)
+ );
+
+ $this->assertSame($response, $status->getResponse());
+ $this->assertTrue($status->hasResponse());
+ }
+}
diff --git a/src/Broadcasting/tests/BroadcastManagerTest.php b/src/Broadcasting/tests/BroadcastManagerTest.php
new file mode 100644
index 000000000..bc4a97d60
--- /dev/null
+++ b/src/Broadcasting/tests/BroadcastManagerTest.php
@@ -0,0 +1,124 @@
+ 'log',
+ 'aliases' => [
+ 'firebase' => 'log',
+ ],
+ 'driverAliases' => [
+ 'pusher' => 'pusher-driver-class',
+ ],
+ 'connections' => [
+ 'log' => [
+ 'driver' => 'pusher-driver-class',
+ 'foo' => 'bar',
+ ],
+ 'null' => [
+ 'driver' => 'null-driver-class',
+ 'foo' => 'baz',
+ ],
+ 'inMemory' => [
+ 'driver' => 'pusher',
+ 'bar' => 'baz',
+ ],
+ ],
+ ]);
+
+ $this->factory = m::mock(FactoryInterface::class);
+ $this->manager = new BroadcastManager($this->factory, $config);
+ }
+
+ public function testGetDefaultConnection(): void
+ {
+ $connection = m::mock(BroadcastInterface::class);
+
+ $this->factory->shouldReceive('make')->once()->with('pusher-driver-class', [
+ 'driver' => 'pusher-driver-class',
+ 'foo' => 'bar',
+ ])->andReturn($connection);
+
+ $this->assertSame($connection, $this->manager->connection());
+ }
+
+ public function testGetConnectionByName(): void
+ {
+ $connection = m::mock(BroadcastInterface::class);
+
+ $this->factory->shouldReceive('make')->once()->with('null-driver-class', [
+ 'driver' => 'null-driver-class',
+ 'foo' => 'baz',
+ ])->andReturn($connection);
+
+ $this->assertSame($connection, $this->manager->connection('null'));
+ }
+
+ public function testGetConnectionWithDriverAlias(): void
+ {
+ $connection = m::mock(BroadcastInterface::class);
+
+ $this->factory->shouldReceive('make')->once()->with('pusher-driver-class', [
+ 'driver' => 'pusher-driver-class',
+ 'bar' => 'baz',
+ ])->andReturn($connection);
+
+ $this->assertSame($connection, $this->manager->connection('inMemory'));
+ }
+
+ public function testGetConnectionByAlias(): void
+ {
+ $connection = m::mock(BroadcastInterface::class);
+
+ $this->factory->shouldReceive('make')->once()->with('pusher-driver-class', [
+ 'driver' => 'pusher-driver-class',
+ 'foo' => 'bar',
+ ])->andReturn($connection);
+
+ $this->assertSame($connection, $this->manager->connection('firebase'));
+ }
+
+ public function testConnectionShouldBeCreatedOnlyOnce(): void
+ {
+ $connection1 = m::mock(BroadcastInterface::class);
+ $connection2 = m::mock(BroadcastInterface::class);
+
+ $this->factory->shouldReceive('make')->once()->with('pusher-driver-class', [
+ 'driver' => 'pusher-driver-class',
+ 'foo' => 'bar',
+ ])->andReturn($connection1);
+
+ $this->assertSame($connection1, $this->manager->connection());
+ $this->assertSame($connection1, $this->manager->connection());
+
+ $this->factory->shouldReceive('make')->once()->with('null-driver-class', [
+ 'driver' => 'null-driver-class',
+ 'foo' => 'baz',
+ ])->andReturn($connection2);
+
+ $this->assertSame($connection2, $this->manager->connection('null'));
+ $this->assertSame($connection2, $this->manager->connection('null'));
+ }
+}
diff --git a/src/Broadcasting/tests/Config/BroadcastConfigTest.php b/src/Broadcasting/tests/Config/BroadcastConfigTest.php
new file mode 100644
index 000000000..a48d15160
--- /dev/null
+++ b/src/Broadcasting/tests/Config/BroadcastConfigTest.php
@@ -0,0 +1,122 @@
+config = new BroadcastConfig([
+ 'authorize' => [
+ 'path' => 'foo-path',
+ 'topics' => [
+ 'bar-topic.{id}' => fn ($id) => $id,
+ 'foo-topic' => fn () => 'foo',
+ ],
+ ],
+ 'default' => 'firebase',
+ 'aliases' => [
+ 'users-data' => 'firebase',
+ 'foo-data' => 'foo',
+ ],
+
+ 'driverAliases' => [
+ 'log' => 'log-driver',
+ ],
+
+ 'connections' => [
+ 'firebase' => [
+ 'driver' => 'log',
+ ],
+ 'null' => [
+ 'driver' => 'null-driver',
+ ],
+ 'memory' => [],
+ ],
+ ]);
+ }
+
+
+ public function testGetsDefaultConnection(): void
+ {
+ $this->assertSame(
+ 'firebase',
+ $this->config->getDefaultConnection()
+ );
+ }
+
+ public function testNotDefinedDefaultKeyShouldThrowAnException(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectErrorMessage('Default broadcast connection is not defined.');
+
+ $config = new BroadcastConfig();
+
+ $config->getDefaultConnection();
+ }
+
+ public function testGetsConnectionConfigByName(): void
+ {
+ $this->assertSame(
+ [
+ 'driver' => 'null-driver',
+ ],
+ $this->config->getConnectionConfig('null')
+ );
+ }
+
+ public function testGetsConnectionWithAliasDriverShouldBeReplacedWithRealDriver(): void
+ {
+ $this->assertSame(
+ [
+ 'driver' => 'log-driver',
+ ],
+ $this->config->getConnectionConfig('firebase')
+ );
+ }
+
+ public function testNotDefinedConnectionShouldThrowAnException(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectErrorMessage('Config for connection `foo` is not defined.');
+
+ $this->config->getConnectionConfig('foo');
+ }
+
+ public function testConnectionWithoutDefinedDriverShouldThrowAnException(): void
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectErrorMessage('Driver for `memory` connection is not defined.');
+
+ $this->config->getConnectionConfig('memory');
+ }
+
+ public function testGetAuthorizationPath(): void
+ {
+ $this->assertSame('foo-path', $this->config->getAuthorizationPath());
+ }
+
+ public function testNotDefinedAuthorizationPathShouldReturnNull(): void
+ {
+ $config = new BroadcastConfig();
+ $this->assertNull($config->getAuthorizationPath());
+ }
+
+ public function testGetsTopics(): void
+ {
+ $this->assertSame(
+ $this->config['authorize']['topics'],
+ $this->config->getTopics()
+ );
+ }
+}
diff --git a/src/Broadcasting/tests/Driver/LogBroadcastTest.php b/src/Broadcasting/tests/Driver/LogBroadcastTest.php
new file mode 100644
index 000000000..c196e1083
--- /dev/null
+++ b/src/Broadcasting/tests/Driver/LogBroadcastTest.php
@@ -0,0 +1,78 @@
+driver = new LogBroadcast(
+ $this->logger = m::mock(LoggerInterface::class)
+ );
+ }
+
+ public function testPublishMessageToTopicWithLoglevel(): void
+ {
+ $driver = new LogBroadcast(
+ $logger = m::mock(LoggerInterface::class),
+ LogLevel::DEBUG
+ );
+
+ $logger->shouldReceive('log')->once()
+ ->with(LogLevel::DEBUG, 'Broadcasting on channels [topic] with payload: message');
+ $driver->publish('topic', 'message');
+ }
+
+ public function testPublishMessageToTopic(): void
+ {
+ $this->logger->shouldReceive('log')->once()
+ ->with(LogLevel::INFO, 'Broadcasting on channels [topic] with payload: message');
+ $this->driver->publish('topic', 'message');
+ }
+
+ public function testPublishMessagesToTopic(): void
+ {
+ $this->logger->shouldReceive('log')->once()
+ ->with(LogLevel::INFO, 'Broadcasting on channels [topic] with payload: message1');
+ $this->logger->shouldReceive('log')->once()
+ ->with(LogLevel::INFO, 'Broadcasting on channels [topic] with payload: message2');
+
+ $this->driver->publish('topic', ['message1', 'message2']);
+ }
+
+ public function testPublishMessageToTopics(): void
+ {
+ $this->logger->shouldReceive('log')->once()
+ ->with(LogLevel::INFO, 'Broadcasting on channels [topic1, topic2] with payload: message');
+
+ $this->driver->publish(['topic1', 'topic2'], 'message');
+ }
+
+ public function testPublishMessagesToTopics(): void
+ {
+ $this->logger->shouldReceive('log')->once()
+ ->with(LogLevel::INFO, 'Broadcasting on channels [topic1, topic2] with payload: message1');
+ $this->logger->shouldReceive('log')->once()
+ ->with(LogLevel::INFO, 'Broadcasting on channels [topic1, topic2] with payload: message2');
+
+ $this->driver->publish(['topic1', 'topic2'], ['message1', 'message2']);
+ }
+}
diff --git a/src/Broadcasting/tests/Driver/NullBroadcastTest.php b/src/Broadcasting/tests/Driver/NullBroadcastTest.php
new file mode 100644
index 000000000..7651ed035
--- /dev/null
+++ b/src/Broadcasting/tests/Driver/NullBroadcastTest.php
@@ -0,0 +1,29 @@
+driver = new NullBroadcast();
+ }
+
+ public function testPublishMessageToTopic(): void
+ {
+ $this->driver->publish('topic', 'message');
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/Broadcasting/tests/Middleware/AuthorizationMiddlewareTest.php b/src/Broadcasting/tests/Middleware/AuthorizationMiddlewareTest.php
new file mode 100644
index 000000000..76da57e3c
--- /dev/null
+++ b/src/Broadcasting/tests/Middleware/AuthorizationMiddlewareTest.php
@@ -0,0 +1,130 @@
+shouldReceive('getUri')->once()->andReturn($uri = m::mock(UriInterface::class));
+ $uri->shouldReceive('getPath')->once()->andReturn('/');
+
+ $handler->shouldReceive('handle')->once()->with($request);
+
+ $middleware->process($request, $handler);
+ }
+
+ public function testNotGuardedBroadcastShouldReturnOkResponse(): void
+ {
+ $middleware = new AuthorizationMiddleware(
+ $broadcast = m::mock(BroadcastInterface::class),
+ $responseFactory = m::mock(ResponseFactoryInterface::class),
+ '/auth',
+ );
+
+ $request = m::mock(ServerRequestInterface::class);
+ $handler = m::mock(RequestHandlerInterface::class);
+
+ $request->shouldReceive('getUri')->once()->andReturn($uri = m::mock(UriInterface::class));
+ $uri->shouldReceive('getPath')->once()->andReturn('/auth');
+
+ $responseFactory->shouldReceive('createResponse')->once()->with(200)->andReturn(m::mock(ResponseInterface::class));
+
+ $middleware->process($request, $handler);
+ }
+
+ public function testGuardedBroadcastWithAuthorizedRequestShouldReturnOkResponse(): void
+ {
+ $middleware = new AuthorizationMiddleware(
+ $broadcast = m::mock(BroadcastInterface::class, GuardInterface::class),
+ $responseFactory = m::mock(ResponseFactoryInterface::class),
+ '/auth',
+ );
+
+ $request = m::mock(ServerRequestInterface::class);
+ $handler = m::mock(RequestHandlerInterface::class);
+
+ $request->shouldReceive('getUri')->once()->andReturn($uri = m::mock(UriInterface::class));
+ $uri->shouldReceive('getPath')->once()->andReturn('/auth');
+
+ $broadcast->shouldReceive('authorize')->once()->with($request)
+ ->andReturn(new AuthorizationStatus(
+ true, ['topic_name'], ['foo' => 'bar']
+ ));
+
+ $responseFactory->shouldReceive('createResponse')->once()->with(200)->andReturn(m::mock(ResponseInterface::class));
+
+ $middleware->process($request, $handler);
+ }
+
+ public function testGuardedBroadcastWithNotAuthorizedRequestShouldReturnForbidResponse(): void
+ {
+ $middleware = new AuthorizationMiddleware(
+ $broadcast = m::mock(BroadcastInterface::class, GuardInterface::class),
+ $responseFactory = m::mock(ResponseFactoryInterface::class),
+ '/auth',
+ );
+
+ $request = m::mock(ServerRequestInterface::class);
+ $handler = m::mock(RequestHandlerInterface::class);
+
+ $request->shouldReceive('getUri')->once()->andReturn($uri = m::mock(UriInterface::class));
+ $uri->shouldReceive('getPath')->once()->andReturn('/auth');
+
+ $broadcast->shouldReceive('authorize')->once()->with($request)
+ ->andReturn(new AuthorizationStatus(
+ false, ['topic_name'], ['foo' => 'bar']
+ ));
+
+ $responseFactory->shouldReceive('createResponse')->once()->with(403)->andReturn(m::mock(ResponseInterface::class));
+
+ $middleware->process($request, $handler);
+ }
+
+ public function testGuardedBroadcastWithCustomResponseShouldReturnIt(): void
+ {
+ $middleware = new AuthorizationMiddleware(
+ $broadcast = m::mock(BroadcastInterface::class, GuardInterface::class),
+ $responseFactory = m::mock(ResponseFactoryInterface::class),
+ '/auth',
+ );
+
+ $request = m::mock(ServerRequestInterface::class);
+ $handler = m::mock(RequestHandlerInterface::class);
+
+ $request->shouldReceive('getUri')->once()->andReturn($uri = m::mock(UriInterface::class));
+ $uri->shouldReceive('getPath')->once()->andReturn('/auth');
+
+ $broadcast->shouldReceive('authorize')->once()->with($request)
+ ->andReturn(new AuthorizationStatus(
+ false, ['topic_name'], ['foo' => 'bar'], $response = m::mock(ResponseInterface::class)
+ ));
+
+ $this->assertSame($response, $middleware->process($request, $handler));
+ }
+}
diff --git a/src/Broadcasting/tests/TopicRegistryTest.php b/src/Broadcasting/tests/TopicRegistryTest.php
new file mode 100644
index 000000000..406337484
--- /dev/null
+++ b/src/Broadcasting/tests/TopicRegistryTest.php
@@ -0,0 +1,69 @@
+registry = new TopicRegistry([
+ 'bar-topic.{id}' => fn($id) => $id,
+ 'foo-topic' => fn() => 'foo',
+ ]);
+ }
+
+ public function testRegisterTopic(): void
+ {
+ $this->registry->register('baz-topic', fn() => 'baz');
+ $this->registry->register('baz.{uuid}', fn(string $uuid) => $uuid);
+
+
+ $params = [];
+ $this->assertSame(
+ 'baz',
+ call_user_func($this->registry->findCallback('baz-topic', $params))
+ );
+
+
+ $params = [];
+ $this->assertSame(
+ 'hello',
+ call_user_func($this->registry->findCallback('baz.hello', $params), 'hello')
+ );
+ $this->assertSame([0 => 'baz.hello', 'uuid' => 'hello', 1 => 'hello'], $params);
+ }
+
+ public function testFindsTopicCallback(): void
+ {
+ $params = [];
+ $this->assertSame(
+ 'foo',
+ call_user_func($this->registry->findCallback('foo-topic', $params))
+ );
+
+ $this->assertSame([0 => 'foo-topic'], $params);
+
+ $params = [];
+ $this->assertSame(
+ 5,
+ call_user_func($this->registry->findCallback('bar-topic.5', $params), 5)
+ );
+ $this->assertSame([0 => 'bar-topic.5', 'id' => '5', 1 => '5'], $params);
+
+
+ $params = [];
+ $this->assertNull(
+ $this->registry->findCallback('baz-topic', $params)
+ );
+ $this->assertSame([], $params);
+ }
+}
diff --git a/src/Cache/composer.json b/src/Cache/composer.json
index 46f002a1c..82c99cf77 100644
--- a/src/Cache/composer.json
+++ b/src/Cache/composer.json
@@ -16,9 +16,9 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/config": "^2.13",
- "spiral/files": "^2.13",
+ "spiral/core": "^2.14",
+ "spiral/config": "^2.14",
+ "spiral/files": "^2.14",
"psr/simple-cache": "2 - 3"
},
"autoload": {
@@ -27,7 +27,7 @@
}
},
"require-dev": {
- "spiral/boot": "^2.13",
+ "spiral/boot": "^2.14",
"mockery/mockery": "^1.5",
"phpunit/phpunit": "^9.5.5"
},
@@ -38,7 +38,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Config/composer.json b/src/Config/composer.json
index 32476f3e6..da068a8c3 100644
--- a/src/Config/composer.json
+++ b/src/Config/composer.json
@@ -17,7 +17,7 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
- "spiral/core": "^2.13"
+ "spiral/core": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Console/composer.json b/src/Console/composer.json
index 3426deea3..97544955a 100644
--- a/src/Console/composer.json
+++ b/src/Console/composer.json
@@ -16,12 +16,12 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
+ "spiral/core": "^2.14",
"symfony/console": "^5.3.7|^6.0"
},
"require-dev": {
- "spiral/boot": "^2.13",
- "spiral/tokenizer": "^2.13",
+ "spiral/boot": "^2.14",
+ "spiral/tokenizer": "^2.14",
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5"
},
@@ -37,7 +37,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Cookies/composer.json b/src/Cookies/composer.json
index faa524f99..84a3ded5d 100644
--- a/src/Cookies/composer.json
+++ b/src/Cookies/composer.json
@@ -16,14 +16,14 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/encrypter": "^2.13",
+ "spiral/core": "^2.14",
+ "spiral/encrypter": "^2.14",
"psr/http-server-middleware": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
- "spiral/http": "^2.13",
+ "spiral/http": "^2.14",
"laminas/laminas-diactoros": "^2.8"
},
"autoload": {
@@ -38,7 +38,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Core/composer.json b/src/Core/composer.json
index 8597c7084..358edb0a0 100644
--- a/src/Core/composer.json
+++ b/src/Core/composer.json
@@ -34,7 +34,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Csrf/composer.json b/src/Csrf/composer.json
index a835b8c30..9ddd5fa9f 100644
--- a/src/Csrf/composer.json
+++ b/src/Csrf/composer.json
@@ -16,14 +16,14 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/cookies": "^2.13",
+ "spiral/core": "^2.14",
+ "spiral/cookies": "^2.14",
"psr/http-server-middleware": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
- "spiral/http": "^2.13",
+ "spiral/http": "^2.14",
"laminas/laminas-diactoros": "^2.8"
},
"autoload": {
@@ -38,7 +38,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Debug/composer.json b/src/Debug/composer.json
index e57a71310..2f1c82919 100644
--- a/src/Debug/composer.json
+++ b/src/Debug/composer.json
@@ -16,7 +16,7 @@
],
"require": {
"php": ">=8.1",
- "spiral/logger": "^2.13"
+ "spiral/logger": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5"
@@ -33,7 +33,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Distribution/composer.json b/src/Distribution/composer.json
index d05b5cb07..54a2084bb 100644
--- a/src/Distribution/composer.json
+++ b/src/Distribution/composer.json
@@ -41,7 +41,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"scripts": {
diff --git a/src/Dumper/composer.json b/src/Dumper/composer.json
index 48fe7842f..9106c9bf6 100644
--- a/src/Dumper/composer.json
+++ b/src/Dumper/composer.json
@@ -37,7 +37,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Dumper/src/Dumper.php b/src/Dumper/src/Dumper.php
index 45ec57858..ba51d86f3 100644
--- a/src/Dumper/src/Dumper.php
+++ b/src/Dumper/src/Dumper.php
@@ -14,6 +14,8 @@
/**
* Renderer exports the content of the given variable, array or object into human friendly form.
+ *
+ * @deprecated since v2.13. Will be removed in v3.0
*/
class Dumper implements LoggerAwareInterface
{
diff --git a/src/Dumper/src/Exception/DumperException.php b/src/Dumper/src/Exception/DumperException.php
index 1e2ef9fe0..9ebe7a414 100644
--- a/src/Dumper/src/Exception/DumperException.php
+++ b/src/Dumper/src/Exception/DumperException.php
@@ -4,6 +4,9 @@
namespace Spiral\Debug\Exception;
+/**
+ * @deprecated since v2.13. Will be removed in v3.0
+ */
class DumperException extends \LogicException
{
}
diff --git a/src/Dumper/src/Renderer/AbstractRenderer.php b/src/Dumper/src/Renderer/AbstractRenderer.php
index 1ca38b06f..9a4313729 100644
--- a/src/Dumper/src/Renderer/AbstractRenderer.php
+++ b/src/Dumper/src/Renderer/AbstractRenderer.php
@@ -6,6 +6,9 @@
use Spiral\Debug\RendererInterface;
+/**
+ * @deprecated since v2.13. Will be removed in v3.0
+ */
abstract class AbstractRenderer implements RendererInterface
{
/**
diff --git a/src/Dumper/src/Renderer/ConsoleRenderer.php b/src/Dumper/src/Renderer/ConsoleRenderer.php
index 78b878cbf..5769e2179 100644
--- a/src/Dumper/src/Renderer/ConsoleRenderer.php
+++ b/src/Dumper/src/Renderer/ConsoleRenderer.php
@@ -8,6 +8,8 @@
/**
* Colorful styling for CLI dumps.
+ *
+ * @deprecated since v2.13. Will be removed in v3.0
*/
final class ConsoleRenderer extends AbstractRenderer
{
diff --git a/src/Dumper/src/Renderer/HtmlRenderer.php b/src/Dumper/src/Renderer/HtmlRenderer.php
index 8e9ee167d..ddc8c3853 100644
--- a/src/Dumper/src/Renderer/HtmlRenderer.php
+++ b/src/Dumper/src/Renderer/HtmlRenderer.php
@@ -8,6 +8,8 @@
/**
* HTML renderer with switchable color schemas.
+ *
+ * @deprecated since v2.13. Will be removed in v3.0
*/
final class HtmlRenderer implements RendererInterface
{
diff --git a/src/Dumper/src/Renderer/PlainRenderer.php b/src/Dumper/src/Renderer/PlainRenderer.php
index 578fe137f..2d4d2fbd2 100644
--- a/src/Dumper/src/Renderer/PlainRenderer.php
+++ b/src/Dumper/src/Renderer/PlainRenderer.php
@@ -6,6 +6,8 @@
/**
* No styles.
+ *
+ * @deprecated since v2.13. Will be removed in v3.0
*/
final class PlainRenderer extends AbstractRenderer
{
diff --git a/src/Dumper/src/RendererInterface.php b/src/Dumper/src/RendererInterface.php
index be2d55155..b54baa8ae 100644
--- a/src/Dumper/src/RendererInterface.php
+++ b/src/Dumper/src/RendererInterface.php
@@ -6,6 +6,8 @@
/**
* Applies set of styles to value dump.
+ *
+ * @deprecated since v2.13. Will be removed in v3.0
*/
interface RendererInterface
{
diff --git a/src/Dumper/src/System.php b/src/Dumper/src/System.php
index 7f815b811..fe0b4fe25 100644
--- a/src/Dumper/src/System.php
+++ b/src/Dumper/src/System.php
@@ -6,6 +6,8 @@
/**
* Describes the env PHP script is running within.
+ *
+ * @deprecated since v2.13. Will be removed in v3.0
*/
final class System
{
diff --git a/src/Encrypter/composer.json b/src/Encrypter/composer.json
index 47bf232e8..5b2b10fea 100644
--- a/src/Encrypter/composer.json
+++ b/src/Encrypter/composer.json
@@ -17,7 +17,7 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
- "spiral/core": "^2.13",
+ "spiral/core": "^2.14",
"defuse/php-encryption": "^2.2"
},
"require-dev": {
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Exceptions/composer.json b/src/Exceptions/composer.json
index 5fc15b0f0..10f4e4851 100644
--- a/src/Exceptions/composer.json
+++ b/src/Exceptions/composer.json
@@ -18,8 +18,8 @@
"php": ">=8.1",
"ext-json": "*",
"ext-mbstring": "*",
- "spiral/dumper": "^2.13",
- "spiral/debug": "^2.13"
+ "spiral/dumper": "^2.14",
+ "spiral/debug": "^2.14"
},
"require-dev": {
"mockery/mockery": "^1.5",
@@ -37,7 +37,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Files/composer.json b/src/Files/composer.json
index 55caabf32..b881254c7 100644
--- a/src/Files/composer.json
+++ b/src/Files/composer.json
@@ -33,7 +33,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Filters/composer.json b/src/Filters/composer.json
index b151cd1a0..1b9d33421 100644
--- a/src/Filters/composer.json
+++ b/src/Filters/composer.json
@@ -16,10 +16,10 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/models": "^2.13",
- "spiral/translator": "^2.13",
- "spiral/validation": "^2.13"
+ "spiral/core": "^2.14",
+ "spiral/models": "^2.14",
+ "spiral/translator": "^2.14",
+ "spiral/validation": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
@@ -37,7 +37,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Framework/Bootloader/Http/HttpBootloader.php b/src/Framework/Bootloader/Http/HttpBootloader.php
index 6cc956f98..4377ae04b 100644
--- a/src/Framework/Bootloader/Http/HttpBootloader.php
+++ b/src/Framework/Bootloader/Http/HttpBootloader.php
@@ -40,6 +40,7 @@ public function init(): void
'Content-Type' => 'text/html; charset=UTF-8',
],
'middleware' => [],
+ 'chunkSize' => null,
]
);
}
diff --git a/src/Hmvc/composer.json b/src/Hmvc/composer.json
index e8ab3fc5c..0d9b1e335 100644
--- a/src/Hmvc/composer.json
+++ b/src/Hmvc/composer.json
@@ -16,7 +16,7 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13"
+ "spiral/core": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5"
@@ -33,7 +33,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Http/composer.json b/src/Http/composer.json
index 418603a00..ede2293ab 100644
--- a/src/Http/composer.json
+++ b/src/Http/composer.json
@@ -18,9 +18,9 @@
"php": ">=8.1",
"ext-json": "*",
"ext-mbstring": "*",
- "spiral/core": "^2.13",
- "spiral/files": "^2.13",
- "spiral/streams": "^2.13",
+ "spiral/core": "^2.14",
+ "spiral/files": "^2.14",
+ "spiral/streams": "^2.14",
"psr/http-message": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-server-middleware": "^1.0",
@@ -44,7 +44,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Http/src/Config/HttpConfig.php b/src/Http/src/Config/HttpConfig.php
index 170da03e8..5165bd44a 100644
--- a/src/Http/src/Config/HttpConfig.php
+++ b/src/Http/src/Config/HttpConfig.php
@@ -20,6 +20,7 @@ final class HttpConfig extends InjectableConfig
'Content-Type' => 'text/html; charset=UTF-8',
],
'middleware' => [],
+ 'chunkSize' => null,
];
public function getBasePath(): string
@@ -44,4 +45,16 @@ public function getMiddleware(): array
{
return $this->config['middleware'] ?? $this->config['middlewares'];
}
+
+ /**
+ * If chunkSize isn't provided - using default values
+ */
+ public function getChunkSize(): ?int
+ {
+ if (\is_null($this->config['chunkSize']) || (int) $this->config['chunkSize'] < 0) {
+ return null;
+ }
+
+ return (int) $this->config['chunkSize'];
+ }
}
diff --git a/src/Logger/composer.json b/src/Logger/composer.json
index b0159bd14..ffeba7b46 100644
--- a/src/Logger/composer.json
+++ b/src/Logger/composer.json
@@ -17,7 +17,7 @@
"require": {
"php": ">=8.1",
"psr/log": "1 - 3",
- "spiral/core": "^2.13"
+ "spiral/core": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Mailer/composer.json b/src/Mailer/composer.json
index 767e46dcb..09692d27d 100644
--- a/src/Mailer/composer.json
+++ b/src/Mailer/composer.json
@@ -33,7 +33,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Models/composer.json b/src/Models/composer.json
index 8095df668..1092e4875 100644
--- a/src/Models/composer.json
+++ b/src/Models/composer.json
@@ -33,7 +33,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Pagination/composer.json b/src/Pagination/composer.json
index e45419ffe..04a65b0a7 100644
--- a/src/Pagination/composer.json
+++ b/src/Pagination/composer.json
@@ -33,7 +33,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Prototype/composer.json b/src/Prototype/composer.json
index 84730523b..c26d67297 100644
--- a/src/Prototype/composer.json
+++ b/src/Prototype/composer.json
@@ -19,8 +19,8 @@
"ext-json": "*",
"nikic/php-parser": "^4.1",
"doctrine/inflector": "^1.4|^2.0",
- "spiral/console": "^2.13",
- "spiral/attributes": "^2.13"
+ "spiral/console": "^2.14",
+ "spiral/annotations": "^2.14"
},
"autoload": {
"psr-4": {
@@ -31,7 +31,7 @@
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
"cycle/orm": "^1.8.1",
- "spiral/debug": "^2.13"
+ "spiral/debug": "^2.14"
},
"autoload-dev": {
"psr-4": {
@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Queue/composer.json b/src/Queue/composer.json
index 94bc2e620..dda5c9539 100644
--- a/src/Queue/composer.json
+++ b/src/Queue/composer.json
@@ -16,7 +16,8 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
- "spiral/snapshots": "^2.13",
+ "spiral/snapshots": "^2.14",
+ "spiral/attributes": "^2.14",
"doctrine/inflector": "^1.4|^2.0",
"opis/closure": "^3.6",
"ramsey/uuid": "^4.2.3"
@@ -32,13 +33,14 @@
}
},
"require-dev": {
+ "doctrine/annotations": "^1.12",
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
- "spiral/boot": "^2.13"
+ "spiral/boot": "^2.14"
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Queue/src/Attribute/Queueable.php b/src/Queue/src/Attribute/Queueable.php
new file mode 100644
index 000000000..7dd7e3c18
--- /dev/null
+++ b/src/Queue/src/Attribute/Queueable.php
@@ -0,0 +1,29 @@
+queue = $queue;
+ }
+}
diff --git a/src/Queue/src/QueueableDetector.php b/src/Queue/src/QueueableDetector.php
new file mode 100644
index 000000000..bbc1f587c
--- /dev/null
+++ b/src/Queue/src/QueueableDetector.php
@@ -0,0 +1,51 @@
+reader = $reader;
+ }
+
+ /**
+ * @psalm-param class-string|object $object
+ */
+ public function isQueueable($object): bool
+ {
+ $reflection = new \ReflectionClass($object);
+
+ if ($reflection->implementsInterface(QueueableInterface::class)) {
+ return true;
+ }
+
+ return $this->reader->firstClassMetadata($reflection, Queueable::class) !== null;
+ }
+
+ /**
+ * @psalm-param class-string|object $object
+ */
+ public function getQueue($object): ?string
+ {
+ $reflection = new \ReflectionClass($object);
+
+ $attribute = $this->reader->firstClassMetadata($reflection, Queueable::class);
+ if ($attribute !== null) {
+ return $attribute->queue;
+ }
+
+ if (\is_object($object) && $reflection->hasMethod('getQueue')) {
+ return $reflection->getMethod('getQueue')->invoke($object);
+ }
+
+ return null;
+ }
+}
diff --git a/src/Queue/tests/Attribute/Stub/NotQueueable.php b/src/Queue/tests/Attribute/Stub/NotQueueable.php
new file mode 100644
index 000000000..d7a8e019d
--- /dev/null
+++ b/src/Queue/tests/Attribute/Stub/NotQueueable.php
@@ -0,0 +1,9 @@
+create());
+
+ $this->assertSame($queueable, $detector->isQueueable($object));
+ $this->assertSame($queue, $detector->getQueue($object));
+ }
+
+ public function queueableProvider(): \Traversable
+ {
+ yield [Queueable::class, true, null];
+ yield [QueueableWithQueue::class, true, 'test'];
+ yield [QueueableWithInterface::class, true, null];
+ yield [new QueueableWithInterfaceAndQueue(), true, 'test'];
+ yield [new QueueableWithInterfaceAndStaticQueue(), true, 'test'];
+ yield [NotQueueable::class, false, null];
+ yield [new NotQueueable(), false, null];
+ yield [NotQueueableInterface::class, false, null];
+ }
+}
diff --git a/src/Reactor/composer.json b/src/Reactor/composer.json
index 48fecc7f3..c82c74e19 100644
--- a/src/Reactor/composer.json
+++ b/src/Reactor/composer.json
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Router/composer.json b/src/Router/composer.json
index c0f780b53..23a87c3f7 100644
--- a/src/Router/composer.json
+++ b/src/Router/composer.json
@@ -17,9 +17,9 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
- "spiral/core": "^2.13",
- "spiral/hmvc": "^2.13",
- "spiral/http": "^2.13",
+ "spiral/core": "^2.14",
+ "spiral/hmvc": "^2.14",
+ "spiral/http": "^2.14",
"cocur/slugify": "^3.2",
"doctrine/inflector": "^1.4|^2.0"
},
@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Scaffolder/composer.json b/src/Scaffolder/composer.json
index 854574893..3d4aded37 100644
--- a/src/Scaffolder/composer.json
+++ b/src/Scaffolder/composer.json
@@ -21,7 +21,7 @@
],
"require": {
"php": ">=8.1",
- "spiral/reactor": "^2.13",
+ "spiral/reactor": "^2.14",
"cocur/slugify": "^3.2",
"doctrine/inflector": "^1.4|^2.0"
},
@@ -29,14 +29,14 @@
"phpunit/phpunit": "^9.5.5",
"cycle/orm": "^1.8.1",
"cycle/annotated": "^2.0.6",
- "spiral/boot": "^2.13",
- "spiral/console": "^2.13",
- "spiral/core": "^2.13",
- "spiral/filters": "^2.13",
- "spiral/http": "^2.13",
+ "spiral/boot": "^2.14",
+ "spiral/console": "^2.14",
+ "spiral/core": "^2.14",
+ "spiral/filters": "^2.14",
+ "spiral/http": "^2.14",
"spiral/migrations": "^2.3",
- "spiral/prototype": "^2.13",
- "spiral/queue": "^2.13"
+ "spiral/prototype": "^2.14",
+ "spiral/queue": "^2.14"
},
"autoload": {
"files": [
@@ -53,7 +53,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Security/composer.json b/src/Security/composer.json
index 780ef5c3f..903ba244e 100644
--- a/src/Security/composer.json
+++ b/src/Security/composer.json
@@ -16,7 +16,7 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13"
+ "spiral/core": "^2.14"
},
"autoload": {
"psr-4": {
@@ -24,7 +24,7 @@
}
},
"require-dev": {
- "spiral/console": "^2.13",
+ "spiral/console": "^2.14",
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5"
},
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/SendIt/composer.json b/src/SendIt/composer.json
index 9629e1a54..92cc44d42 100644
--- a/src/SendIt/composer.json
+++ b/src/SendIt/composer.json
@@ -17,19 +17,19 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
- "spiral/core": "^2.13",
+ "spiral/core": "^2.14",
"spiral/jobs": "^2.2",
- "spiral/queue": "^2.13",
- "spiral/logger": "^2.13",
- "spiral/mailer": "^2.13",
- "spiral/views": "^2.13",
+ "spiral/queue": "^2.14",
+ "spiral/logger": "^2.14",
+ "spiral/mailer": "^2.14",
+ "spiral/views": "^2.14",
"symfony/mailer": "^5.1|^6.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
- "spiral/boot": "^2.13",
- "spiral/stempler-bridge": "^2.13"
+ "spiral/boot": "^2.14",
+ "spiral/stempler-bridge": "^2.14"
},
"autoload": {
"psr-4": {
@@ -43,7 +43,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Session/composer.json b/src/Session/composer.json
index ea7c3ab9c..e6226e69e 100644
--- a/src/Session/composer.json
+++ b/src/Session/composer.json
@@ -16,8 +16,8 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/files": "^2.13"
+ "spiral/core": "^2.14",
+ "spiral/files": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5"
@@ -34,7 +34,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Snapshots/composer.json b/src/Snapshots/composer.json
index 34f2501eb..6eb15e47f 100644
--- a/src/Snapshots/composer.json
+++ b/src/Snapshots/composer.json
@@ -32,7 +32,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Stempler/composer.json b/src/Stempler/composer.json
index d5e09a07d..3d1057f76 100644
--- a/src/Stempler/composer.json
+++ b/src/Stempler/composer.json
@@ -22,7 +22,7 @@
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
- "spiral/dumper": "^2.13"
+ "spiral/dumper": "^2.14"
},
"autoload": {
"files": [
@@ -39,7 +39,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Storage/composer.json b/src/Storage/composer.json
index 65d26d6f3..63cdfcf19 100644
--- a/src/Storage/composer.json
+++ b/src/Storage/composer.json
@@ -20,7 +20,7 @@
],
"require": {
"php": ">=8.1",
- "spiral/distribution": "^2.13",
+ "spiral/distribution": "^2.14",
"symfony/polyfill-php80": "^1.22",
"league/flysystem": "^2.3.1"
},
@@ -32,7 +32,7 @@
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"symfony/var-dumper": "^5.2|^6.0",
- "vimeo/psalm": "^4.22",
+ "vimeo/psalm": "^4.21",
"jetbrains/phpstorm-attributes": "^1.0"
},
"autoload-dev": {
@@ -52,7 +52,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Streams/composer.json b/src/Streams/composer.json
index 5903a0140..2dd425703 100644
--- a/src/Streams/composer.json
+++ b/src/Streams/composer.json
@@ -21,7 +21,7 @@
"require-dev": {
"phpunit/phpunit": "^9.5.5",
"mockery/mockery": "^1.5",
- "spiral/files": "^2.13",
+ "spiral/files": "^2.14",
"laminas/laminas-diactoros": "^2.8"
},
"autoload": {
@@ -36,7 +36,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Tokenizer/composer.json b/src/Tokenizer/composer.json
index 0661d2427..93d8200d0 100644
--- a/src/Tokenizer/composer.json
+++ b/src/Tokenizer/composer.json
@@ -16,8 +16,8 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/logger": "^2.13",
+ "spiral/core": "^2.14",
+ "spiral/logger": "^2.14",
"symfony/finder": "^5.3.7|^6.0"
},
"require-dev": {
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Translator/composer.json b/src/Translator/composer.json
index 441c5a646..05762d873 100644
--- a/src/Translator/composer.json
+++ b/src/Translator/composer.json
@@ -16,9 +16,9 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/logger": "^2.13",
- "spiral/tokenizer": "^2.13",
+ "spiral/core": "^2.14",
+ "spiral/logger": "^2.14",
+ "spiral/tokenizer": "^2.14",
"symfony/translation": "^5.1|^6.0"
},
"require-dev": {
@@ -40,7 +40,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Validation/composer.json b/src/Validation/composer.json
index 678ae71c1..e33e4889b 100644
--- a/src/Validation/composer.json
+++ b/src/Validation/composer.json
@@ -17,10 +17,10 @@
"require": {
"php": ">=8.1",
"ext-json": "*",
- "spiral/core": "^2.13",
- "spiral/files": "^2.13",
- "spiral/streams": "^2.13",
- "spiral/translator": "^2.13"
+ "spiral/core": "^2.14",
+ "spiral/files": "^2.14",
+ "spiral/streams": "^2.14",
+ "spiral/translator": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
@@ -39,7 +39,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/src/Views/composer.json b/src/Views/composer.json
index a69955831..9b4e6dc93 100644
--- a/src/Views/composer.json
+++ b/src/Views/composer.json
@@ -16,8 +16,8 @@
],
"require": {
"php": ">=8.1",
- "spiral/core": "^2.13",
- "spiral/files": "^2.13"
+ "spiral/core": "^2.14",
+ "spiral/files": "^2.14"
},
"require-dev": {
"phpunit/phpunit": "^9.5.5",
@@ -35,7 +35,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "2.13.x-dev"
+ "dev-master": "2.14.x-dev"
}
},
"config": {
diff --git a/tests/app/config/broadcasting.php b/tests/app/config/broadcasting.php
new file mode 100644
index 000000000..3df44bbd4
--- /dev/null
+++ b/tests/app/config/broadcasting.php
@@ -0,0 +1,28 @@
+ 'log',
+ 'aliases' => [
+ 'firebase' => 'null',
+ ],
+ 'connections' => [
+ 'null' => [
+ 'driver' => NullBroadcast::class,
+ ],
+ 'log' => [
+ 'driver' => LogBroadcast::class,
+ 'level' => \Psr\Log\LogLevel::DEBUG,
+ ],
+ 'nullable' => [
+ 'type' => 'null',
+ ],
+ ],
+ 'driverAliases' => [
+ 'null' => NullBroadcast::class,
+ ],
+];
diff --git a/tests/app/src/TestApp.php b/tests/app/src/TestApp.php
index ec8f1951f..0b77ebba5 100644
--- a/tests/app/src/TestApp.php
+++ b/tests/app/src/TestApp.php
@@ -53,6 +53,10 @@ class TestApp extends Kernel
// Cache
\Spiral\Cache\Bootloader\CacheBootloader::class,
+ // Broadcasting
+ \Spiral\Broadcasting\Bootloader\BroadcastingBootloader::class,
+ \Spiral\Broadcasting\Bootloader\WebsocketsBootloader::class,
+
// Auth
Bootloader\Auth\HttpAuthBootloader::class,