From a940620ffb0869209359c478a767ec29ea2f4983 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 14 Nov 2024 17:50:53 +0800 Subject: [PATCH 01/30] [WIP] --- composer.json | 3 + src/broadcasting/LICENSE.md | 23 + src/broadcasting/README.md | 2 + src/broadcasting/composer.json | 55 +++ .../src/Broadcasters/Broadcaster.php | 327 +++++++++++++ .../src/Contracts/Broadcaster.php | 27 ++ .../src/Contracts/HasBroadcastChannel.php | 18 + tests/Broadcasting/BroadcastEventTest.php | 15 + tests/Broadcasting/BroadcasterTest.php | 452 ++++++++++++++++++ .../UsePusherChannelsNamesTest.php | 108 +++++ 10 files changed, 1030 insertions(+) create mode 100644 src/broadcasting/LICENSE.md create mode 100644 src/broadcasting/README.md create mode 100644 src/broadcasting/composer.json create mode 100644 src/broadcasting/src/Broadcasters/Broadcaster.php create mode 100644 src/broadcasting/src/Contracts/Broadcaster.php create mode 100644 src/broadcasting/src/Contracts/HasBroadcastChannel.php create mode 100644 tests/Broadcasting/BroadcastEventTest.php create mode 100644 tests/Broadcasting/BroadcasterTest.php create mode 100644 tests/Broadcasting/UsePusherChannelsNamesTest.php diff --git a/composer.json b/composer.json index 7e19c271..054096fa 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "psr-4": { "SwooleTW\\Hyperf\\": "src/framework/src/", "SwooleTW\\Hyperf\\Auth\\": "src/auth/src/", + "SwooleTW\\Hyperf\\Broadcasting\\": "src/broadcasting/src/", "SwooleTW\\Hyperf\\Cache\\": "src/cache/src/", "SwooleTW\\Hyperf\\Config\\": "src/config/src/", "SwooleTW\\Hyperf\\Container\\": "src/container/src/", @@ -102,6 +103,7 @@ }, "replace": { "swooletw/hyperf-auth": "self.version", + "swooletw/hyperf-broadcasting": "self.version", "swooletw/hyperf-cache": "self.version", "swooletw/hyperf-config": "self.version", "swooletw/hyperf-container": "self.version", @@ -159,6 +161,7 @@ "config": [ "SwooleTW\\Hyperf\\ConfigProvider", "SwooleTW\\Hyperf\\Auth\\ConfigProvider", + "SwooleTW\\Hyperf\\Broadcasting\\ConfigProvider", "SwooleTW\\Hyperf\\Cache\\ConfigProvider", "SwooleTW\\Hyperf\\Cookie\\ConfigProvider", "SwooleTW\\Hyperf\\Config\\ConfigProvider", diff --git a/src/broadcasting/LICENSE.md b/src/broadcasting/LICENSE.md new file mode 100644 index 00000000..a8f5fd6a --- /dev/null +++ b/src/broadcasting/LICENSE.md @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) Taylor Otwell + +Copyright (c) Laravel Hyperf + +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 00000000..d41d68aa --- /dev/null +++ b/src/broadcasting/README.md @@ -0,0 +1,2 @@ +Broadcasting for Laravel Hyperf +=== diff --git a/src/broadcasting/composer.json b/src/broadcasting/composer.json new file mode 100644 index 00000000..46318e33 --- /dev/null +++ b/src/broadcasting/composer.json @@ -0,0 +1,55 @@ +{ + "name": "swooletw/hyperf-filesystem", + "type": "library", + "description": "The filesystem package for Hyperf.", + "license": "MIT", + "keywords": [ + "php", + "hyperf", + "swoole", + "broadcasting", + "laravel-hyperf" + ], + "authors": [ + { + "name": "Albert Chen", + "email": "albert@laravel-hyperf.com", + "homepage": "https://albert-chen.com" + } + ], + "support": { + "issues": "https://github.com/swooletw/hyperf-packages/issues", + "source": "https://github.com/swooletw/hyperf-packages" + }, + "autoload": { + "psr-4": { + "SwooleTW\\Hyperf\\Broadcasting": "src/" + } + }, + "require": { + "php": "^8.2", + "hyperf/collection": "~3.1.0", + "hyperf/context": "~3.1.0", + "hyperf/http-server": "~3.1.0", + "swooletw/hyperf-framework": "dev-master", + "swooletw/hyperf-router": "dev-master", + "swooletw/hyperf-support": "dev-master", + "swooletw/object-pool": "dev-master" + }, + "suggest": { + "ext-hash": "Required to use the Ably and Pusher broadcast drivers.", + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0)." + }, + "config": { + "sort-packages": true + }, + "extra": { + "hyperf": { + "config": "SwooleTW\\Hyperf\\Broadcasting\\ConfigProvider" + }, + "branch-alias": { + "dev-main": "3.1-dev" + } + } +} diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php new file mode 100644 index 00000000..f77e7184 --- /dev/null +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -0,0 +1,327 @@ +authenticatedUserCallback) { + return $this->authenticatedUserCallback->__invoke($request); + } + } + + /** + * Register the user retrieval callback used to authenticate connections. + * + * See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication. + */ + public function resolveAuthenticatedUserUsing(Closure $callback): void + { + $this->authenticatedUserCallback = $callback; + } + + /** + * Register a channel authenticator. + */ + public function channel(string|HasBroadcastChannel $channel, string|callable $callback, array $options = []): static + { + if ($channel instanceof HasBroadcastChannel) { + $channel = $channel->broadcastChannelRoute(); + } elseif (is_string($channel) && class_exists($channel) && is_a($channel, HasBroadcastChannel::class, true)) { + $channel = (new $channel)->broadcastChannelRoute(); + } + + $this->channels[$channel] = $callback; + + $this->channelOptions[$channel] = $options; + + return $this; + } + + /** + * Authenticate the incoming request for a given channel. + * + * @throws AccessDeniedHttpException + */ + protected function verifyUserCanAccessChannel(RequestInterface $request, string $channel): mixed + { + foreach ($this->channels as $pattern => $callback) { + if (! $this->channelNameMatchesPattern($channel, $pattern)) { + continue; + } + + $parameters = $this->extractAuthParameters($pattern, $channel, $callback); + + $handler = $this->normalizeChannelHandlerToCallable($callback); + + $result = $handler($this->retrieveUser($channel), ...$parameters); + + if ($result === false) { + throw new AccessDeniedHttpException(); + } elseif ($result) { + return $this->validAuthenticationResponse($request, $result); + } + } + + throw new AccessDeniedHttpException; + } + + /** + * Extract the parameters from the given pattern and channel. + */ + protected function extractAuthParameters(string $pattern, string $channel, callable|string $callback): array + { + $callbackParameters = $this->extractParameters($callback); + + return collect($this->extractChannelKeys($pattern, $channel))->reject(function ($value, $key) { + return is_numeric($key); + })->map(function ($value, $key) use ($callbackParameters) { + return $this->resolveBinding($key, $value, $callbackParameters); + })->values()->all(); + } + + /** + * Extracts the parameters out of what the user passed to handle the channel authentication. + * + * @return ReflectionParameter[] + * + * @throws Exception + */ + protected function extractParameters(callable|string $callback): array + { + if (is_callable($callback)) { + return (new ReflectionFunction($callback))->getParameters(); + } elseif (is_string($callback)) { + return $this->extractParametersFromClass($callback); + } + + throw new Exception('Given channel handler is an unknown type.'); + } + + /** + * Extracts the parameters out of a class channel's "join" method. + * + * @param string $callback + * @return ReflectionParameter[] + * + * @throws Exception + */ + protected function extractParametersFromClass(string $callback): array + { + $reflection = new ReflectionClass($callback); + + if (! $reflection->hasMethod('join')) { + throw new Exception('Class based channel must define a "join" method.'); + } + + return $reflection->getMethod('join')->getParameters(); + } + + /** + * Extract the channel keys from the incoming channel name. + */ + protected function extractChannelKeys(string $pattern, string $channel): array + { + preg_match('/^'.preg_replace('/\{(.*?)\}/', '(?<$1>[^\.]+)', $pattern).'/', $channel, $keys); + + return $keys; + } + + /** + * Resolve the given parameter binding. + */ + protected function resolveBinding(string $key, string $value, array $callbackParameters): mixed + { + $newValue = $this->resolveExplicitBindingIfPossible($key, $value); + + return $newValue === $value ? $this->resolveImplicitBindingIfPossible( + $key, $value, $callbackParameters + ) : $newValue; + } + + /** + * Resolve an explicit parameter binding if applicable. + */ + protected function resolveExplicitBindingIfPossible(string $key, string $value): mixed + { + $binder = $this->binder(); + + if ($binder && $binder->getBindingCallback($key)) { + return call_user_func($binder->getBindingCallback($key), $value); + } + + return $value; + } + + /** + * Resolve an implicit parameter binding if applicable. + * + * @throws AccessDeniedHttpException + */ + protected function resolveImplicitBindingIfPossible(string $key, string $value, array $callbackParameters): mixed + { + foreach ($callbackParameters as $parameter) { + if (! $this->isImplicitlyBindable($key, $parameter)) { + continue; + } + + $className = Reflector::getParameterClassName($parameter); + + if (is_null($model = (new $className)->resolveRouteBinding($value))) { + throw new AccessDeniedHttpException; + } + + return $model; + } + + return $value; + } + + /** + * Determine if a given key and parameter is implicitly bindable. + */ + protected function isImplicitlyBindable(string $key, ReflectionParameter $parameter): bool + { + return $parameter->getName() === $key + && Reflector::isParameterSubclassOf($parameter, UrlRoutable::class); + } + + /** + * Format the channel array into an array of strings. + */ + protected function formatChannels(array $channels): array + { + return array_map(function ($channel) { + return (string) $channel; + }, $channels); + } + + /** + * Get the model binding registrar instance. + * + * @return \Illuminate\Contracts\Routing\BindingRegistrar + */ + protected function binder() + { + // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar + // if (! $this->bindingRegistrar) { + // $this->bindingRegistrar = ApplicationContext::getContainer()->has(BindingRegistrar::class) + // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) : null; + // } + // + // return $this->bindingRegistrar; + return null; + } + + /** + * Normalize the given callback into a callable. + * + * @param mixed $callback + * @return callable + */ + protected function normalizeChannelHandlerToCallable($callback) + { + return is_callable($callback) ? $callback : function (...$args) use ($callback) { + return ApplicationContext::getContainer() + ->get($callback) + ->join(...$args); + }; + } + + /** + * Retrieve the authenticated user using the configured guard (if any). + */ + protected function retrieveUser(string $channel): mixed + { + $options = $this->retrieveChannelOptions($channel); + + $guards = $options['guards'] ?? null; + + if (is_null($guards)) { + return Auth::user(); + } + + foreach (Arr::wrap($guards) as $guard) { + if ($user = Auth::guard($guard)->user()) { + return $user; + } + } + } + + /** + * Retrieve options for a certain channel. + */ + protected function retrieveChannelOptions(string $channel): array + { + foreach ($this->channelOptions as $pattern => $options) { + if (! $this->channelNameMatchesPattern($channel, $pattern)) { + continue; + } + + return $options; + } + + return []; + } + + /** + * Check if the channel name from the request matches a pattern from registered channels. + */ + protected function channelNameMatchesPattern(string $channel, string $pattern): bool + { + return preg_match('/^'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel); + } + + /** + * Get all of the registered channels. + */ + public function getChannels(): Collection + { + return collect($this->channels); + } +} diff --git a/src/broadcasting/src/Contracts/Broadcaster.php b/src/broadcasting/src/Contracts/Broadcaster.php new file mode 100644 index 00000000..a1a55457 --- /dev/null +++ b/src/broadcasting/src/Contracts/Broadcaster.php @@ -0,0 +1,27 @@ +broadcaster = new FakeBroadcaster; + } + + protected function tearDown(): void + { + m::close(); + // + // Container::setInstance(null); + } + + public function testExtractingParametersWhileCheckingForUserAccess() + { + $container = m::mock(ContainerInterface::class); + ApplicationContext::setContainer($container); + + $callback = function ($user, BroadcasterTestEloquentModelStub $model, $nonModel) { + // + }; + $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', $callback); + $this->assertEquals(['model.1.instance', 'something'], $parameters); + + // $callback = function ($user, BroadcasterTestEloquentModelStub $model, BroadcasterTestEloquentModelStub $model2, $something) { + // // + // }; + // $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{model2}.{nonModel}', 'asd.1.uid.something', $callback); + // $this->assertEquals(['model.1.instance', 'model.uid.instance', 'something'], $parameters); + // + // $callback = function ($user) { + // // + // }; + // $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); + // $this->assertEquals([], $parameters); + // + // $callback = function ($user, $something) { + // // + // }; + // $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); + // $this->assertEquals([], $parameters); + // + // /* + // * Test Explicit Binding... + // */ + // $container = new Container; + // Container::setInstance($container); + // $binder = m::mock(BindingRegistrar::class); + // $binder->shouldReceive('getBindingCallback')->times(2)->with('model')->andReturn(function () { + // return 'bound'; + // }); + // $container->instance(BindingRegistrar::class, $binder); + // $callback = function ($user, $model) { + // // + // }; + // $parameters = $this->broadcaster->extractAuthParameters('something.{model}', 'something.1', $callback); + // $this->assertEquals(['bound'], $parameters); + // Container::setInstance(new Container); + } + + // public function testCanUseChannelClasses() + // { + // $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', DummyBroadcastingChannel::class); + // $this->assertEquals(['model.1.instance', 'something'], $parameters); + // } + // + // public function testModelRouteBinding() + // { + // $container = new Container; + // Container::setInstance($container); + // $binder = m::mock(BindingRegistrar::class); + // $callback = RouteBinding::forModel($container, BroadcasterTestEloquentModelStub::class); + // + // $binder->shouldReceive('getBindingCallback')->times(2)->with('model')->andReturn($callback); + // $container->instance(BindingRegistrar::class, $binder); + // $callback = function ($user, $model) { + // // + // }; + // $parameters = $this->broadcaster->extractAuthParameters('something.{model}', 'something.1', $callback); + // $this->assertEquals(['model.1.instance'], $parameters); + // Container::setInstance(new Container); + // } + // + // public function testUnknownChannelAuthHandlerTypeThrowsException() + // { + // $this->expectException(Exception::class); + // + // $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', 123); + // } + // + // public function testCanRegisterChannelsAsClasses() + // { + // $this->broadcaster->channel('something', function () { + // // + // }); + // + // $this->broadcaster->channel('somethingelse', DummyBroadcastingChannel::class); + // } + // + // public function testNotFoundThrowsHttpException() + // { + // $this->expectException(HttpException::class); + // + // $callback = function ($user, BroadcasterTestEloquentModelNotFoundStub $model) { + // // + // }; + // $this->broadcaster->extractAuthParameters('asd.{model}', 'asd.1', $callback); + // } + // + // public function testCanRegisterChannelsWithoutOptions() + // { + // $this->broadcaster->channel('somechannel', function () { + // // + // }); + // } + // + // public function testCanRegisterChannelsWithOptions() + // { + // $options = ['a' => ['b', 'c']]; + // $this->broadcaster->channel('somechannel', function () { + // // + // }, $options); + // } + // + // public function testCanRetrieveChannelsOptions() + // { + // $options = ['a' => ['b', 'c']]; + // $this->broadcaster->channel('somechannel', function () { + // // + // }, $options); + // + // $this->assertEquals( + // $options, + // $this->broadcaster->retrieveChannelOptions('somechannel') + // ); + // } + // + // public function testCanRetrieveChannelsOptionsUsingAChannelNameContainingArgs() + // { + // $options = ['a' => ['b', 'c']]; + // $this->broadcaster->channel('somechannel.{id}.test.{text}', function () { + // // + // }, $options); + // + // $this->assertEquals( + // $options, + // $this->broadcaster->retrieveChannelOptions('somechannel.23.test.mytext') + // ); + // } + // + // public function testCanRetrieveChannelsOptionsWhenMultipleChannelsAreRegistered() + // { + // $options = ['a' => ['b', 'c']]; + // $this->broadcaster->channel('somechannel', function () { + // // + // }); + // $this->broadcaster->channel('someotherchannel', function () { + // // + // }, $options); + // + // $this->assertEquals( + // $options, + // $this->broadcaster->retrieveChannelOptions('someotherchannel') + // ); + // } + // + // public function testDontRetrieveChannelsOptionsWhenChannelDoesntExists() + // { + // $options = ['a' => ['b', 'c']]; + // $this->broadcaster->channel('somechannel', function () { + // // + // }, $options); + // + // $this->assertEquals( + // [], + // $this->broadcaster->retrieveChannelOptions('someotherchannel') + // ); + // } + // + // public function testRetrieveUserWithoutGuard() + // { + // $this->broadcaster->channel('somechannel', function () { + // // + // }); + // + // $request = m::mock(Request::class); + // $request->shouldReceive('user') + // ->once() + // ->withNoArgs() + // ->andReturn(new DummyUser); + // + // $this->assertInstanceOf( + // DummyUser::class, + // $this->broadcaster->retrieveUser($request, 'somechannel') + // ); + // } + // + // public function testRetrieveUserWithOneGuardUsingAStringForSpecifyingGuard() + // { + // $this->broadcaster->channel('somechannel', function () { + // // + // }, ['guards' => 'myguard']); + // + // $request = m::mock(Request::class); + // $request->shouldReceive('user') + // ->once() + // ->with('myguard') + // ->andReturn(new DummyUser); + // + // $this->assertInstanceOf( + // DummyUser::class, + // $this->broadcaster->retrieveUser($request, 'somechannel') + // ); + // } + // + // public function testRetrieveUserWithMultipleGuardsAndRespectGuardsOrder() + // { + // $this->broadcaster->channel('somechannel', function () { + // // + // }, ['guards' => ['myguard1', 'myguard2']]); + // $this->broadcaster->channel('someotherchannel', function () { + // // + // }, ['guards' => ['myguard2', 'myguard1']]); + // + // $request = m::mock(Request::class); + // $request->shouldReceive('user') + // ->once() + // ->with('myguard1') + // ->andReturn(null); + // $request->shouldReceive('user') + // ->twice() + // ->with('myguard2') + // ->andReturn(new DummyUser) + // ->ordered('user'); + // + // $this->assertInstanceOf( + // DummyUser::class, + // $this->broadcaster->retrieveUser($request, 'somechannel') + // ); + // + // $this->assertInstanceOf( + // DummyUser::class, + // $this->broadcaster->retrieveUser($request, 'someotherchannel') + // ); + // } + // + // public function testRetrieveUserDontUseDefaultGuardWhenOneGuardSpecified() + // { + // $this->broadcaster->channel('somechannel', function () { + // // + // }, ['guards' => 'myguard']); + // + // $request = m::mock(Request::class); + // $request->shouldReceive('user') + // ->once() + // ->with('myguard') + // ->andReturn(null); + // $request->shouldNotReceive('user') + // ->withNoArgs(); + // + // $this->broadcaster->retrieveUser($request, 'somechannel'); + // } + // + // public function testRetrieveUserDontUseDefaultGuardWhenMultipleGuardsSpecified() + // { + // $this->broadcaster->channel('somechannel', function () { + // // + // }, ['guards' => ['myguard1', 'myguard2']]); + // + // $request = m::mock(Request::class); + // $request->shouldReceive('user') + // ->once() + // ->with('myguard1') + // ->andReturn(null); + // $request->shouldReceive('user') + // ->once() + // ->with('myguard2') + // ->andReturn(null); + // $request->shouldNotReceive('user') + // ->withNoArgs(); + // + // $this->broadcaster->retrieveUser($request, 'somechannel'); + // } + // + // public function testUserAuthenticationWithValidUser() + // { + // $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { + // return ['id' => '12345', 'socket' => $request->socket_id]; + // }); + // + // $user = $this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234'])); + // + // $this->assertSame([ + // 'id' => '12345', + // 'socket' => '1234.1234', + // ], $user); + // } + // + // public function testUserAuthenticationWithInvalidUser() + // { + // $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { + // return null; + // }); + // + // $user = $this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234'])); + // + // $this->assertNull($user); + // } + // + // public function testUserAuthenticationWithoutResolve() + // { + // $this->assertNull($this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234']))); + // } + // + // #[DataProvider('channelNameMatchPatternProvider')] + // public function testChannelNameMatchPattern($channel, $pattern, $shouldMatch) + // { + // $this->assertEquals($shouldMatch, $this->broadcaster->channelNameMatchesPattern($channel, $pattern)); + // } + // + // public static function channelNameMatchPatternProvider() + // { + // return [ + // ['something', 'something', true], + // ['something.23', 'something.{id}', true], + // ['something.23.test', 'something.{id}.test', true], + // ['something.23.test.42', 'something.{id}.test.{id2}', true], + // ['something-23:test-42', 'something-{id}:test-{id2}', true], + // ['something..test.42', 'something.{id}.test.{id2}', false], + // ['23:string:test', '{id}:string:{text}', true], + // ['something.23', 'something', false], + // ['something.23.test.42', 'something.test.{id}', false], + // ['something-23-test-42', 'something-{id}-test', false], + // ['23:test', '{id}:test:abcd', false], + // ['customer.order.1', 'order.{id}', false], + // ['customerorder.1', 'order.{id}', false], + // ]; + // } +} + +class FakeBroadcaster extends Broadcaster +{ + public function auth(RequestInterface $request): mixed + { + // + } + + public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed + { + // + } + + public function broadcast(array $channels, string $event, array $payload = []): void + { + // + } + + public function extractAuthParameters(string $pattern, string $channel, callable|string $callback): array + { + return parent::extractAuthParameters($pattern, $channel, $callback); + } + + public function retrieveChannelOptions(string $channel): array + { + return parent::retrieveChannelOptions($channel); + } + + public function retrieveUser(string $channel): mixed + { + return parent::retrieveUser($request, $channel); + } + + public function channelNameMatchesPattern(string $channel, string $pattern): bool + { + return parent::channelNameMatchesPattern($channel, $pattern); + } +} + +class BroadcasterTestEloquentModelStub extends Model +{ + public function getRouteKeyName() + { + return 'id'; + } + + public function where($key, $value) + { + $this->value = $value; + + return $this; + } + + public function firstOrFail() + { + return "model.{$this->value}.instance"; + } +} + +class BroadcasterTestEloquentModelNotFoundStub extends Model +{ + public function getRouteKeyName() + { + return 'id'; + } + + public function where($key, $value) + { + $this->value = $value; + + return $this; + } + + public function firstOrFail() + { + // + } +} + +class DummyBroadcastingChannel +{ + public function join($user, BroadcasterTestEloquentModelStub $model, $nonModel) + { + // + } +} + +class DummyUser +{ + // +} diff --git a/tests/Broadcasting/UsePusherChannelsNamesTest.php b/tests/Broadcasting/UsePusherChannelsNamesTest.php new file mode 100644 index 00000000..19003d39 --- /dev/null +++ b/tests/Broadcasting/UsePusherChannelsNamesTest.php @@ -0,0 +1,108 @@ +assertSame( + $normalizedName, + $broadcaster->normalizeChannelName($requestChannelName) + ); + } + + public function testChannelNameNormalizationSpecialCase() + { + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; + + $this->assertSame( + 'private-123', + $broadcaster->normalizeChannelName('private-encrypted-private-123') + ); + } + + #[DataProvider('channelsProvider')] + public function testIsGuardedChannel($requestChannelName, $_, $guarded) + { + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; + + $this->assertSame( + $guarded, + $broadcaster->isGuardedChannel($requestChannelName) + ); + } + + public static function channelsProvider() + { + $prefixesInfos = [ + ['prefix' => 'private-', 'guarded' => true], + ['prefix' => 'private-encrypted-', 'guarded' => true], + ['prefix' => 'presence-', 'guarded' => true], + ['prefix' => '', 'guarded' => false], + ]; + + $channels = [ + 'test', + 'test-channel', + 'test-private-channel', + 'test-presence-channel', + 'abcd.efgh', + 'abcd.efgh.ijkl', + 'test.{param}', + 'test-{param}', + '{a}.{b}', + '{a}-{b}', + '{a}-{b}.{c}', + ]; + + $tests = []; + foreach ($prefixesInfos as $prefixInfos) { + foreach ($channels as $channel) { + $tests[] = [ + $prefixInfos['prefix'].$channel, + $channel, + $prefixInfos['guarded'], + ]; + } + } + + $tests[] = ['private-private-test', 'private-test', true]; + $tests[] = ['private-presence-test', 'presence-test', true]; + $tests[] = ['presence-private-test', 'private-test', true]; + $tests[] = ['presence-presence-test', 'presence-test', true]; + $tests[] = ['public-test', 'public-test', false]; + + return $tests; + } +} + +class FakeBroadcasterUsingPusherChannelsNames extends Broadcaster +{ + use UsePusherChannelConventions; + + public function auth($request) + { + // + } + + public function validAuthenticationResponse($request, $result) + { + // + } + + public function broadcast(array $channels, $event, array $payload = []) + { + // + } +} From 65696e9e4e991cf0e7082652f27c2fc428f121c7 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 14 Nov 2024 18:21:41 +0800 Subject: [PATCH 02/30] [WIP] --- .../src/Broadcasters/Broadcaster.php | 4 - tests/Broadcasting/BroadcasterTest.php | 114 +++++++++--------- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index f77e7184..2b90049b 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -129,8 +129,6 @@ protected function extractAuthParameters(string $pattern, string $channel, calla * Extracts the parameters out of what the user passed to handle the channel authentication. * * @return ReflectionParameter[] - * - * @throws Exception */ protected function extractParameters(callable|string $callback): array { @@ -139,8 +137,6 @@ protected function extractParameters(callable|string $callback): array } elseif (is_string($callback)) { return $this->extractParametersFromClass($callback); } - - throw new Exception('Given channel handler is an unknown type.'); } /** diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index a7483cf8..14721a88 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -6,6 +6,7 @@ use Exception; use Hyperf\Context\ApplicationContext; +use Hyperf\Database\Model\Booted; use Hyperf\HttpServer\Contract\RequestInterface; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; @@ -35,8 +36,7 @@ protected function tearDown(): void public function testExtractingParametersWhileCheckingForUserAccess() { - $container = m::mock(ContainerInterface::class); - ApplicationContext::setContainer($container); + Booted::$container[BroadcasterTestEloquentModelStub::class] = true; $callback = function ($user, BroadcasterTestEloquentModelStub $model, $nonModel) { // @@ -44,27 +44,28 @@ public function testExtractingParametersWhileCheckingForUserAccess() $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', $callback); $this->assertEquals(['model.1.instance', 'something'], $parameters); - // $callback = function ($user, BroadcasterTestEloquentModelStub $model, BroadcasterTestEloquentModelStub $model2, $something) { - // // - // }; - // $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{model2}.{nonModel}', 'asd.1.uid.something', $callback); - // $this->assertEquals(['model.1.instance', 'model.uid.instance', 'something'], $parameters); - // - // $callback = function ($user) { - // // - // }; - // $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); - // $this->assertEquals([], $parameters); - // - // $callback = function ($user, $something) { - // // - // }; - // $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); - // $this->assertEquals([], $parameters); - // - // /* - // * Test Explicit Binding... - // */ + $callback = function ($user, BroadcasterTestEloquentModelStub $model, BroadcasterTestEloquentModelStub $model2, $something) { + // + }; + $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{model2}.{nonModel}', 'asd.1.uid.something', $callback); + $this->assertEquals(['model.1.instance', 'model.uid.instance', 'something'], $parameters); + + $callback = function ($user) { + // + }; + $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); + $this->assertEquals([], $parameters); + + $callback = function ($user, $something) { + // + }; + $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); + $this->assertEquals([], $parameters); + + /* + * Test Explicit Binding... + */ + // DOTO: 要等 binder 實作 // $container = new Container; // Container::setInstance($container); // $binder = m::mock(BindingRegistrar::class); @@ -80,12 +81,13 @@ public function testExtractingParametersWhileCheckingForUserAccess() // Container::setInstance(new Container); } - // public function testCanUseChannelClasses() - // { - // $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', DummyBroadcastingChannel::class); - // $this->assertEquals(['model.1.instance', 'something'], $parameters); - // } - // + public function testCanUseChannelClasses() + { + $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', DummyBroadcastingChannel::class); + $this->assertEquals(['model.1.instance', 'something'], $parameters); + } + + // DOTO: 要等 binder 實作 // public function testModelRouteBinding() // { // $container = new Container; @@ -102,33 +104,33 @@ public function testExtractingParametersWhileCheckingForUserAccess() // $this->assertEquals(['model.1.instance'], $parameters); // Container::setInstance(new Container); // } - // - // public function testUnknownChannelAuthHandlerTypeThrowsException() - // { - // $this->expectException(Exception::class); - // - // $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', 123); - // } - // - // public function testCanRegisterChannelsAsClasses() - // { - // $this->broadcaster->channel('something', function () { - // // - // }); - // - // $this->broadcaster->channel('somethingelse', DummyBroadcastingChannel::class); - // } - // - // public function testNotFoundThrowsHttpException() - // { - // $this->expectException(HttpException::class); - // - // $callback = function ($user, BroadcasterTestEloquentModelNotFoundStub $model) { - // // - // }; - // $this->broadcaster->extractAuthParameters('asd.{model}', 'asd.1', $callback); - // } - // + + public function testUnknownChannelAuthHandlerTypeThrowsException() + { + $this->expectException(Exception::class); + + $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', 'notClassString'); + } + + public function testCanRegisterChannelsAsClasses() + { + $this->broadcaster->channel('something', function () { + // + }); + + $this->broadcaster->channel('somethingelse', DummyBroadcastingChannel::class); + } + + public function testNotFoundThrowsHttpException() + { + $this->expectException(HttpException::class); + + $callback = function ($user, BroadcasterTestEloquentModelNotFoundStub $model) { + // + }; + $this->broadcaster->extractAuthParameters('asd.{model}', 'asd.1', $callback); + } + // public function testCanRegisterChannelsWithoutOptions() // { // $this->broadcaster->channel('somechannel', function () { From 76fa55117667fbaf687d38559c90838ccc3bf33a Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Fri, 15 Nov 2024 15:29:08 +0800 Subject: [PATCH 03/30] complete broadcaster --- .../src/Broadcasters/Broadcaster.php | 12 +- tests/Broadcasting/BroadcasterTest.php | 508 ++++++++++-------- 2 files changed, 283 insertions(+), 237 deletions(-) diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index 2b90049b..5f5e3908 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -53,6 +53,8 @@ public function resolveAuthenticatedUser(RequestInterface $request): ?array if ($this->authenticatedUserCallback) { return $this->authenticatedUserCallback->__invoke($request); } + + return null; } /** @@ -247,7 +249,8 @@ protected function binder() // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar // if (! $this->bindingRegistrar) { // $this->bindingRegistrar = ApplicationContext::getContainer()->has(BindingRegistrar::class) - // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) : null; + // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) + // : null; // } // // return $this->bindingRegistrar; @@ -283,10 +286,13 @@ protected function retrieveUser(string $channel): mixed } foreach (Arr::wrap($guards) as $guard) { - if ($user = Auth::guard($guard)->user()) { + $user = Auth::guard($guard)->user(); + if ($user) { return $user; } } + + return null; } /** @@ -310,7 +316,7 @@ protected function retrieveChannelOptions(string $channel): array */ protected function channelNameMatchesPattern(string $channel, string $pattern): bool { - return preg_match('/^'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel); + return (bool) preg_match('/^'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel); } /** diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 14721a88..aae2d0b4 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -6,18 +6,28 @@ use Exception; use Hyperf\Context\ApplicationContext; +use Hyperf\Context\RequestContext; use Hyperf\Database\Model\Booted; +use Hyperf\HttpMessage\Server\Request as ServerRequest; use Hyperf\HttpServer\Contract\RequestInterface; +use Hyperf\HttpServer\Request; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; +use stdClass; +use SwooleTW\Hyperf\Auth\Contracts\FactoryContract; use SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster; use SwooleTW\Hyperf\Database\Eloquent\Model; -use Symfony\Component\HttpKernel\Exception\HttpException; +use SwooleTW\Hyperf\HttpMessage\Exceptions\HttpException; +use SwooleTW\Hyperf\Support\Facades\Auth; +use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; class BroadcasterTest extends TestCase { + use HasMockedApplication; + + protected $container; + public FakeBroadcaster $broadcaster; protected function setUp(): void @@ -25,6 +35,11 @@ protected function setUp(): void parent::setUp(); $this->broadcaster = new FakeBroadcaster; + + $this->container = $this->getApplication([ + FactoryContract::class => fn () => new stdClass(), + ]); + ApplicationContext::setContainer($this->container); } protected function tearDown(): void @@ -123,6 +138,8 @@ public function testCanRegisterChannelsAsClasses() public function testNotFoundThrowsHttpException() { + Booted::$container[BroadcasterTestEloquentModelNotFoundStub::class] = true; + $this->expectException(HttpException::class); $callback = function ($user, BroadcasterTestEloquentModelNotFoundStub $model) { @@ -131,247 +148,270 @@ public function testNotFoundThrowsHttpException() $this->broadcaster->extractAuthParameters('asd.{model}', 'asd.1', $callback); } - // public function testCanRegisterChannelsWithoutOptions() - // { - // $this->broadcaster->channel('somechannel', function () { - // // - // }); - // } - // - // public function testCanRegisterChannelsWithOptions() - // { - // $options = ['a' => ['b', 'c']]; - // $this->broadcaster->channel('somechannel', function () { - // // - // }, $options); - // } - // - // public function testCanRetrieveChannelsOptions() - // { - // $options = ['a' => ['b', 'c']]; - // $this->broadcaster->channel('somechannel', function () { - // // - // }, $options); - // - // $this->assertEquals( - // $options, - // $this->broadcaster->retrieveChannelOptions('somechannel') - // ); - // } - // - // public function testCanRetrieveChannelsOptionsUsingAChannelNameContainingArgs() - // { - // $options = ['a' => ['b', 'c']]; - // $this->broadcaster->channel('somechannel.{id}.test.{text}', function () { - // // - // }, $options); - // - // $this->assertEquals( - // $options, - // $this->broadcaster->retrieveChannelOptions('somechannel.23.test.mytext') - // ); - // } - // - // public function testCanRetrieveChannelsOptionsWhenMultipleChannelsAreRegistered() - // { - // $options = ['a' => ['b', 'c']]; - // $this->broadcaster->channel('somechannel', function () { - // // - // }); - // $this->broadcaster->channel('someotherchannel', function () { - // // - // }, $options); - // - // $this->assertEquals( - // $options, - // $this->broadcaster->retrieveChannelOptions('someotherchannel') - // ); - // } - // - // public function testDontRetrieveChannelsOptionsWhenChannelDoesntExists() - // { - // $options = ['a' => ['b', 'c']]; - // $this->broadcaster->channel('somechannel', function () { - // // - // }, $options); - // - // $this->assertEquals( - // [], - // $this->broadcaster->retrieveChannelOptions('someotherchannel') - // ); - // } - // - // public function testRetrieveUserWithoutGuard() - // { - // $this->broadcaster->channel('somechannel', function () { - // // - // }); - // - // $request = m::mock(Request::class); - // $request->shouldReceive('user') - // ->once() - // ->withNoArgs() - // ->andReturn(new DummyUser); - // - // $this->assertInstanceOf( - // DummyUser::class, - // $this->broadcaster->retrieveUser($request, 'somechannel') - // ); - // } - // - // public function testRetrieveUserWithOneGuardUsingAStringForSpecifyingGuard() - // { - // $this->broadcaster->channel('somechannel', function () { - // // - // }, ['guards' => 'myguard']); - // - // $request = m::mock(Request::class); - // $request->shouldReceive('user') - // ->once() - // ->with('myguard') - // ->andReturn(new DummyUser); - // - // $this->assertInstanceOf( - // DummyUser::class, - // $this->broadcaster->retrieveUser($request, 'somechannel') - // ); - // } - // - // public function testRetrieveUserWithMultipleGuardsAndRespectGuardsOrder() - // { - // $this->broadcaster->channel('somechannel', function () { - // // - // }, ['guards' => ['myguard1', 'myguard2']]); - // $this->broadcaster->channel('someotherchannel', function () { - // // - // }, ['guards' => ['myguard2', 'myguard1']]); - // - // $request = m::mock(Request::class); - // $request->shouldReceive('user') - // ->once() - // ->with('myguard1') - // ->andReturn(null); - // $request->shouldReceive('user') - // ->twice() - // ->with('myguard2') - // ->andReturn(new DummyUser) - // ->ordered('user'); - // - // $this->assertInstanceOf( - // DummyUser::class, - // $this->broadcaster->retrieveUser($request, 'somechannel') - // ); - // - // $this->assertInstanceOf( - // DummyUser::class, - // $this->broadcaster->retrieveUser($request, 'someotherchannel') - // ); - // } - // - // public function testRetrieveUserDontUseDefaultGuardWhenOneGuardSpecified() - // { - // $this->broadcaster->channel('somechannel', function () { - // // - // }, ['guards' => 'myguard']); - // - // $request = m::mock(Request::class); - // $request->shouldReceive('user') - // ->once() - // ->with('myguard') - // ->andReturn(null); - // $request->shouldNotReceive('user') - // ->withNoArgs(); - // - // $this->broadcaster->retrieveUser($request, 'somechannel'); - // } - // - // public function testRetrieveUserDontUseDefaultGuardWhenMultipleGuardsSpecified() - // { - // $this->broadcaster->channel('somechannel', function () { - // // - // }, ['guards' => ['myguard1', 'myguard2']]); - // - // $request = m::mock(Request::class); - // $request->shouldReceive('user') - // ->once() - // ->with('myguard1') - // ->andReturn(null); - // $request->shouldReceive('user') - // ->once() - // ->with('myguard2') - // ->andReturn(null); - // $request->shouldNotReceive('user') - // ->withNoArgs(); - // - // $this->broadcaster->retrieveUser($request, 'somechannel'); - // } - // - // public function testUserAuthenticationWithValidUser() - // { - // $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { - // return ['id' => '12345', 'socket' => $request->socket_id]; - // }); - // - // $user = $this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234'])); - // - // $this->assertSame([ - // 'id' => '12345', - // 'socket' => '1234.1234', - // ], $user); - // } - // - // public function testUserAuthenticationWithInvalidUser() - // { - // $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { - // return null; - // }); - // - // $user = $this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234'])); - // - // $this->assertNull($user); - // } - // - // public function testUserAuthenticationWithoutResolve() - // { - // $this->assertNull($this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234']))); - // } - // - // #[DataProvider('channelNameMatchPatternProvider')] - // public function testChannelNameMatchPattern($channel, $pattern, $shouldMatch) - // { - // $this->assertEquals($shouldMatch, $this->broadcaster->channelNameMatchesPattern($channel, $pattern)); - // } - // - // public static function channelNameMatchPatternProvider() - // { - // return [ - // ['something', 'something', true], - // ['something.23', 'something.{id}', true], - // ['something.23.test', 'something.{id}.test', true], - // ['something.23.test.42', 'something.{id}.test.{id2}', true], - // ['something-23:test-42', 'something-{id}:test-{id2}', true], - // ['something..test.42', 'something.{id}.test.{id2}', false], - // ['23:string:test', '{id}:string:{text}', true], - // ['something.23', 'something', false], - // ['something.23.test.42', 'something.test.{id}', false], - // ['something-23-test-42', 'something-{id}-test', false], - // ['23:test', '{id}:test:abcd', false], - // ['customer.order.1', 'order.{id}', false], - // ['customerorder.1', 'order.{id}', false], - // ]; - // } + public function testCanRegisterChannelsWithoutOptions() + { + $this->broadcaster->channel('somechannel', function () { + // + }); + } + + public function testCanRegisterChannelsWithOptions() + { + $options = ['a' => ['b', 'c']]; + $this->broadcaster->channel('somechannel', function () { + // + }, $options); + } + + public function testCanRetrieveChannelsOptions() + { + $options = ['a' => ['b', 'c']]; + $this->broadcaster->channel('somechannel', function () { + // + }, $options); + + $this->assertEquals( + $options, + $this->broadcaster->retrieveChannelOptions('somechannel') + ); + } + + public function testCanRetrieveChannelsOptionsUsingAChannelNameContainingArgs() + { + $options = ['a' => ['b', 'c']]; + $this->broadcaster->channel('somechannel.{id}.test.{text}', function () { + // + }, $options); + + $this->assertEquals( + $options, + $this->broadcaster->retrieveChannelOptions('somechannel.23.test.mytext') + ); + } + + public function testCanRetrieveChannelsOptionsWhenMultipleChannelsAreRegistered() + { + $options = ['a' => ['b', 'c']]; + $this->broadcaster->channel('somechannel', function () { + // + }); + $this->broadcaster->channel('someotherchannel', function () { + // + }, $options); + + $this->assertEquals( + $options, + $this->broadcaster->retrieveChannelOptions('someotherchannel') + ); + } + + public function testDontRetrieveChannelsOptionsWhenChannelDoesntExists() + { + $options = ['a' => ['b', 'c']]; + $this->broadcaster->channel('somechannel', function () { + // + }, $options); + + $this->assertEquals( + [], + $this->broadcaster->retrieveChannelOptions('someotherchannel') + ); + } + + public function testRetrieveUserWithoutGuard() + { + $this->broadcaster->channel('somechannel', function () { + // + }); + + Auth::shouldReceive('user') + ->once() + ->withNoArgs() + ->andReturn(new DummyUser); + + $this->assertInstanceOf( + DummyUser::class, + $this->broadcaster->retrieveUser('somechannel') + ); + } + + public function testRetrieveUserWithOneGuardUsingAStringForSpecifyingGuard() + { + $this->broadcaster->channel('somechannel', function () { + // + }, ['guards' => 'myguard']); + + Auth::shouldReceive('guard') + ->once() + ->with('myguard') + ->andReturnSelf(); + Auth::shouldReceive('user') + ->once() + ->withNoArgs() + ->andReturn(new DummyUser); + + $this->assertInstanceOf( + DummyUser::class, + $this->broadcaster->retrieveUser('somechannel') + ); + } + + public function testRetrieveUserWithMultipleGuardsAndRespectGuardsOrder() + { + $this->broadcaster->channel('somechannel', function () { + // + }, ['guards' => ['myguard1', 'myguard2']]); + $this->broadcaster->channel('someotherchannel', function () { + // + }, ['guards' => ['myguard2', 'myguard1']]); + + Auth::shouldReceive('guard') + ->once() + ->with('myguard1') + ->andReturnSelf(); + Auth::shouldReceive('guard') + ->twice() + ->with('myguard2') + ->andReturnSelf(); + Auth::shouldReceive('user') + ->times(3) + ->withNoArgs() + ->andReturn(null, new DummyUser, new DummyUser); + + $this->assertInstanceOf( + DummyUser::class, + $this->broadcaster->retrieveUser('somechannel') + ); + + $this->assertInstanceOf( + DummyUser::class, + $this->broadcaster->retrieveUser('someotherchannel') + ); + } + + public function testRetrieveUserDontUseDefaultGuardWhenOneGuardSpecified() + { + $this->broadcaster->channel('somechannel', function () { + // + }, ['guards' => 'myguard']); + + Auth::shouldReceive('guard') + ->once() + ->with('myguard') + ->andReturnSelf(); + Auth::shouldReceive('user') + ->once() + ->withNoArgs() + ->andReturn(null); + Auth::shouldNotReceive('guard') + ->withNoArgs(); + + $this->broadcaster->retrieveUser('somechannel'); + } + + public function testRetrieveUserDontUseDefaultGuardWhenMultipleGuardsSpecified() + { + $this->broadcaster->channel('somechannel', function () { + // + }, ['guards' => ['myguard1', 'myguard2']]); + + Auth::shouldReceive('guard') + ->once() + ->with('myguard1') + ->andReturnSelf(); + Auth::shouldReceive('guard') + ->once() + ->with('myguard2') + ->andReturnSelf(); + Auth::shouldReceive('user') + ->twice() + ->withNoArgs() + ->andReturn(null); + Auth::shouldNotReceive('guard') + ->withNoArgs(); + + $this->broadcaster->retrieveUser('somechannel'); + } + + public function testUserAuthenticationWithValidUser() + { + $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { + return ['id' => '12345', 'socket' => $request->input('socket_id')]; + }); + + $this->mockRequest('http://exa.com/foo?socket_id=1234.1234#boom'); + $user = $this->broadcaster->resolveAuthenticatedUser(new Request()); + + $this->assertSame([ + 'id' => '12345', + 'socket' => '1234.1234', + ], $user); + } + + private function mockRequest(?string $uri = null): void + { + $request = new ServerRequest('GET', $uri ?: 'http://example.com/foo?bar=baz#boom'); + parse_str($request->getUri()->getQuery(), $result); + $request = $request->withQueryParams($result); + + RequestContext::set($request); + } + + public function testUserAuthenticationWithInvalidUser() + { + $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { + return null; + }); + + $user = $this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234'])); + + $this->assertNull($user); + } + + public function testUserAuthenticationWithoutResolve() + { + $this->mockRequest('http://exa.com/foo?socket_id=1234.1234#boom'); + $user = $this->broadcaster->resolveAuthenticatedUser(new Request()); + + $this->assertNull($this->broadcaster->resolveAuthenticatedUser(new Request())); + } + + #[DataProvider('channelNameMatchPatternProvider')] + public function testChannelNameMatchPattern($channel, $pattern, $shouldMatch) + { + $this->assertEquals($shouldMatch, $this->broadcaster->channelNameMatchesPattern($channel, $pattern)); + } + + public static function channelNameMatchPatternProvider() + { + return [ + ['something', 'something', true], + ['something.23', 'something.{id}', true], + ['something.23.test', 'something.{id}.test', true], + ['something.23.test.42', 'something.{id}.test.{id2}', true], + ['something-23:test-42', 'something-{id}:test-{id2}', true], + ['something..test.42', 'something.{id}.test.{id2}', false], + ['23:string:test', '{id}:string:{text}', true], + ['something.23', 'something', false], + ['something.23.test.42', 'something.test.{id}', false], + ['something-23-test-42', 'something-{id}-test', false], + ['23:test', '{id}:test:abcd', false], + ['customer.order.1', 'order.{id}', false], + ['customerorder.1', 'order.{id}', false], + ]; + } } class FakeBroadcaster extends Broadcaster { public function auth(RequestInterface $request): mixed { - // + return null; } public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed { - // + return null; } public function broadcast(array $channels, string $event, array $payload = []): void @@ -391,7 +431,7 @@ public function retrieveChannelOptions(string $channel): array public function retrieveUser(string $channel): mixed { - return parent::retrieveUser($request, $channel); + return parent::retrieveUser($channel); } public function channelNameMatchesPattern(string $channel, string $pattern): bool From b4cf9cefb984726035cd37b17e276125f2baff50 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Fri, 15 Nov 2024 15:33:33 +0800 Subject: [PATCH 04/30] fix cs --- .../src/Broadcasters/Broadcaster.php | 31 +++-- tests/Broadcasting/BroadcastEventTest.php | 15 --- tests/Broadcasting/BroadcasterTest.php | 123 ++++++++---------- .../UsePusherChannelsNamesTest.php | 108 --------------- 4 files changed, 69 insertions(+), 208 deletions(-) delete mode 100644 tests/Broadcasting/BroadcastEventTest.php delete mode 100644 tests/Broadcasting/UsePusherChannelsNamesTest.php diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index 5f5e3908..73b12f38 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -70,12 +70,12 @@ public function resolveAuthenticatedUserUsing(Closure $callback): void /** * Register a channel authenticator. */ - public function channel(string|HasBroadcastChannel $channel, string|callable $callback, array $options = []): static + public function channel(HasBroadcastChannel|string $channel, callable|string $callback, array $options = []): static { if ($channel instanceof HasBroadcastChannel) { $channel = $channel->broadcastChannelRoute(); } elseif (is_string($channel) && class_exists($channel) && is_a($channel, HasBroadcastChannel::class, true)) { - $channel = (new $channel)->broadcastChannelRoute(); + $channel = (new $channel())->broadcastChannelRoute(); } $this->channels[$channel] = $callback; @@ -105,12 +105,13 @@ protected function verifyUserCanAccessChannel(RequestInterface $request, string if ($result === false) { throw new AccessDeniedHttpException(); - } elseif ($result) { + } + if ($result) { return $this->validAuthenticationResponse($request, $result); } } - throw new AccessDeniedHttpException; + throw new AccessDeniedHttpException(); } /** @@ -136,7 +137,8 @@ protected function extractParameters(callable|string $callback): array { if (is_callable($callback)) { return (new ReflectionFunction($callback))->getParameters(); - } elseif (is_string($callback)) { + } + if (is_string($callback)) { return $this->extractParametersFromClass($callback); } } @@ -144,7 +146,6 @@ protected function extractParameters(callable|string $callback): array /** * Extracts the parameters out of a class channel's "join" method. * - * @param string $callback * @return ReflectionParameter[] * * @throws Exception @@ -165,7 +166,7 @@ protected function extractParametersFromClass(string $callback): array */ protected function extractChannelKeys(string $pattern, string $channel): array { - preg_match('/^'.preg_replace('/\{(.*?)\}/', '(?<$1>[^\.]+)', $pattern).'/', $channel, $keys); + preg_match('/^' . preg_replace('/\{(.*?)\}/', '(?<$1>[^\.]+)', $pattern) . '/', $channel, $keys); return $keys; } @@ -178,7 +179,9 @@ protected function resolveBinding(string $key, string $value, array $callbackPar $newValue = $this->resolveExplicitBindingIfPossible($key, $value); return $newValue === $value ? $this->resolveImplicitBindingIfPossible( - $key, $value, $callbackParameters + $key, + $value, + $callbackParameters ) : $newValue; } @@ -210,8 +213,8 @@ protected function resolveImplicitBindingIfPossible(string $key, string $value, $className = Reflector::getParameterClassName($parameter); - if (is_null($model = (new $className)->resolveRouteBinding($value))) { - throw new AccessDeniedHttpException; + if (is_null($model = (new $className())->resolveRouteBinding($value))) { + throw new AccessDeniedHttpException(); } return $model; @@ -249,7 +252,7 @@ protected function binder() // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar // if (! $this->bindingRegistrar) { // $this->bindingRegistrar = ApplicationContext::getContainer()->has(BindingRegistrar::class) - // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) + // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) // : null; // } // @@ -260,7 +263,7 @@ protected function binder() /** * Normalize the given callback into a callable. * - * @param mixed $callback + * @param mixed $callback * @return callable */ protected function normalizeChannelHandlerToCallable($callback) @@ -291,7 +294,7 @@ protected function retrieveUser(string $channel): mixed return $user; } } - + return null; } @@ -316,7 +319,7 @@ protected function retrieveChannelOptions(string $channel): array */ protected function channelNameMatchesPattern(string $channel, string $pattern): bool { - return (bool) preg_match('/^'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel); + return (bool) preg_match('/^' . preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern) . '$/', $channel); } /** diff --git a/tests/Broadcasting/BroadcastEventTest.php b/tests/Broadcasting/BroadcastEventTest.php deleted file mode 100644 index d2791cb0..00000000 --- a/tests/Broadcasting/BroadcastEventTest.php +++ /dev/null @@ -1,15 +0,0 @@ -broadcaster = new FakeBroadcaster; + $this->broadcaster = new FakeBroadcaster(); $this->container = $this->getApplication([ FactoryContract::class => fn () => new stdClass(), @@ -54,25 +58,21 @@ public function testExtractingParametersWhileCheckingForUserAccess() Booted::$container[BroadcasterTestEloquentModelStub::class] = true; $callback = function ($user, BroadcasterTestEloquentModelStub $model, $nonModel) { - // }; $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{nonModel}', 'asd.1.something', $callback); $this->assertEquals(['model.1.instance', 'something'], $parameters); $callback = function ($user, BroadcasterTestEloquentModelStub $model, BroadcasterTestEloquentModelStub $model2, $something) { - // }; $parameters = $this->broadcaster->extractAuthParameters('asd.{model}.{model2}.{nonModel}', 'asd.1.uid.something', $callback); $this->assertEquals(['model.1.instance', 'model.uid.instance', 'something'], $parameters); $callback = function ($user) { - // }; $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); $this->assertEquals([], $parameters); $callback = function ($user, $something) { - // }; $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); $this->assertEquals([], $parameters); @@ -130,7 +130,6 @@ public function testUnknownChannelAuthHandlerTypeThrowsException() public function testCanRegisterChannelsAsClasses() { $this->broadcaster->channel('something', function () { - // }); $this->broadcaster->channel('somethingelse', DummyBroadcastingChannel::class); @@ -143,7 +142,6 @@ public function testNotFoundThrowsHttpException() $this->expectException(HttpException::class); $callback = function ($user, BroadcasterTestEloquentModelNotFoundStub $model) { - // }; $this->broadcaster->extractAuthParameters('asd.{model}', 'asd.1', $callback); } @@ -151,96 +149,87 @@ public function testNotFoundThrowsHttpException() public function testCanRegisterChannelsWithoutOptions() { $this->broadcaster->channel('somechannel', function () { - // }); } - + public function testCanRegisterChannelsWithOptions() { $options = ['a' => ['b', 'c']]; $this->broadcaster->channel('somechannel', function () { - // }, $options); } - - public function testCanRetrieveChannelsOptions() - { - $options = ['a' => ['b', 'c']]; - $this->broadcaster->channel('somechannel', function () { - // - }, $options); - - $this->assertEquals( - $options, - $this->broadcaster->retrieveChannelOptions('somechannel') - ); - } - + + public function testCanRetrieveChannelsOptions() + { + $options = ['a' => ['b', 'c']]; + $this->broadcaster->channel('somechannel', function () { + }, $options); + + $this->assertEquals( + $options, + $this->broadcaster->retrieveChannelOptions('somechannel') + ); + } + public function testCanRetrieveChannelsOptionsUsingAChannelNameContainingArgs() { $options = ['a' => ['b', 'c']]; $this->broadcaster->channel('somechannel.{id}.test.{text}', function () { - // }, $options); - + $this->assertEquals( $options, $this->broadcaster->retrieveChannelOptions('somechannel.23.test.mytext') ); } - + public function testCanRetrieveChannelsOptionsWhenMultipleChannelsAreRegistered() { $options = ['a' => ['b', 'c']]; $this->broadcaster->channel('somechannel', function () { - // }); $this->broadcaster->channel('someotherchannel', function () { - // }, $options); - + $this->assertEquals( $options, $this->broadcaster->retrieveChannelOptions('someotherchannel') ); } - + public function testDontRetrieveChannelsOptionsWhenChannelDoesntExists() { $options = ['a' => ['b', 'c']]; $this->broadcaster->channel('somechannel', function () { - // }, $options); - + $this->assertEquals( [], $this->broadcaster->retrieveChannelOptions('someotherchannel') ); } - + public function testRetrieveUserWithoutGuard() { $this->broadcaster->channel('somechannel', function () { - // }); - + Auth::shouldReceive('user') ->once() ->withNoArgs() - ->andReturn(new DummyUser); - + ->andReturn(new DummyUser()); + $this->assertInstanceOf( DummyUser::class, $this->broadcaster->retrieveUser('somechannel') ); } - + public function testRetrieveUserWithOneGuardUsingAStringForSpecifyingGuard() { $this->broadcaster->channel('somechannel', function () { - // }, ['guards' => 'myguard']); - + Auth::shouldReceive('guard') ->once() ->with('myguard') @@ -248,23 +237,21 @@ public function testRetrieveUserWithOneGuardUsingAStringForSpecifyingGuard() Auth::shouldReceive('user') ->once() ->withNoArgs() - ->andReturn(new DummyUser); - + ->andReturn(new DummyUser()); + $this->assertInstanceOf( DummyUser::class, $this->broadcaster->retrieveUser('somechannel') ); } - + public function testRetrieveUserWithMultipleGuardsAndRespectGuardsOrder() { $this->broadcaster->channel('somechannel', function () { - // }, ['guards' => ['myguard1', 'myguard2']]); $this->broadcaster->channel('someotherchannel', function () { - // }, ['guards' => ['myguard2', 'myguard1']]); - + Auth::shouldReceive('guard') ->once() ->with('myguard1') @@ -276,25 +263,24 @@ public function testRetrieveUserWithMultipleGuardsAndRespectGuardsOrder() Auth::shouldReceive('user') ->times(3) ->withNoArgs() - ->andReturn(null, new DummyUser, new DummyUser); - + ->andReturn(null, new DummyUser(), new DummyUser()); + $this->assertInstanceOf( DummyUser::class, $this->broadcaster->retrieveUser('somechannel') ); - + $this->assertInstanceOf( DummyUser::class, $this->broadcaster->retrieveUser('someotherchannel') ); } - + public function testRetrieveUserDontUseDefaultGuardWhenOneGuardSpecified() { $this->broadcaster->channel('somechannel', function () { - // }, ['guards' => 'myguard']); - + Auth::shouldReceive('guard') ->once() ->with('myguard') @@ -305,16 +291,15 @@ public function testRetrieveUserDontUseDefaultGuardWhenOneGuardSpecified() ->andReturn(null); Auth::shouldNotReceive('guard') ->withNoArgs(); - + $this->broadcaster->retrieveUser('somechannel'); } - + public function testRetrieveUserDontUseDefaultGuardWhenMultipleGuardsSpecified() { $this->broadcaster->channel('somechannel', function () { - // }, ['guards' => ['myguard1', 'myguard2']]); - + Auth::shouldReceive('guard') ->once() ->with('myguard1') @@ -329,19 +314,19 @@ public function testRetrieveUserDontUseDefaultGuardWhenMultipleGuardsSpecified() ->andReturn(null); Auth::shouldNotReceive('guard') ->withNoArgs(); - + $this->broadcaster->retrieveUser('somechannel'); } - + public function testUserAuthenticationWithValidUser() { $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { return ['id' => '12345', 'socket' => $request->input('socket_id')]; }); - + $this->mockRequest('http://exa.com/foo?socket_id=1234.1234#boom'); $user = $this->broadcaster->resolveAuthenticatedUser(new Request()); - + $this->assertSame([ 'id' => '12345', 'socket' => '1234.1234', @@ -356,18 +341,18 @@ private function mockRequest(?string $uri = null): void RequestContext::set($request); } - + public function testUserAuthenticationWithInvalidUser() { $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { return null; }); - + $user = $this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234'])); - + $this->assertNull($user); } - + public function testUserAuthenticationWithoutResolve() { $this->mockRequest('http://exa.com/foo?socket_id=1234.1234#boom'); @@ -375,13 +360,13 @@ public function testUserAuthenticationWithoutResolve() $this->assertNull($this->broadcaster->resolveAuthenticatedUser(new Request())); } - + #[DataProvider('channelNameMatchPatternProvider')] public function testChannelNameMatchPattern($channel, $pattern, $shouldMatch) { $this->assertEquals($shouldMatch, $this->broadcaster->channelNameMatchesPattern($channel, $pattern)); } - + public static function channelNameMatchPatternProvider() { return [ @@ -416,7 +401,6 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re public function broadcast(array $channels, string $event, array $payload = []): void { - // } public function extractAuthParameters(string $pattern, string $channel, callable|string $callback): array @@ -476,7 +460,6 @@ public function where($key, $value) public function firstOrFail() { - // } } @@ -484,11 +467,9 @@ class DummyBroadcastingChannel { public function join($user, BroadcasterTestEloquentModelStub $model, $nonModel) { - // } } class DummyUser { - // } diff --git a/tests/Broadcasting/UsePusherChannelsNamesTest.php b/tests/Broadcasting/UsePusherChannelsNamesTest.php deleted file mode 100644 index 19003d39..00000000 --- a/tests/Broadcasting/UsePusherChannelsNamesTest.php +++ /dev/null @@ -1,108 +0,0 @@ -assertSame( - $normalizedName, - $broadcaster->normalizeChannelName($requestChannelName) - ); - } - - public function testChannelNameNormalizationSpecialCase() - { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; - - $this->assertSame( - 'private-123', - $broadcaster->normalizeChannelName('private-encrypted-private-123') - ); - } - - #[DataProvider('channelsProvider')] - public function testIsGuardedChannel($requestChannelName, $_, $guarded) - { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; - - $this->assertSame( - $guarded, - $broadcaster->isGuardedChannel($requestChannelName) - ); - } - - public static function channelsProvider() - { - $prefixesInfos = [ - ['prefix' => 'private-', 'guarded' => true], - ['prefix' => 'private-encrypted-', 'guarded' => true], - ['prefix' => 'presence-', 'guarded' => true], - ['prefix' => '', 'guarded' => false], - ]; - - $channels = [ - 'test', - 'test-channel', - 'test-private-channel', - 'test-presence-channel', - 'abcd.efgh', - 'abcd.efgh.ijkl', - 'test.{param}', - 'test-{param}', - '{a}.{b}', - '{a}-{b}', - '{a}-{b}.{c}', - ]; - - $tests = []; - foreach ($prefixesInfos as $prefixInfos) { - foreach ($channels as $channel) { - $tests[] = [ - $prefixInfos['prefix'].$channel, - $channel, - $prefixInfos['guarded'], - ]; - } - } - - $tests[] = ['private-private-test', 'private-test', true]; - $tests[] = ['private-presence-test', 'presence-test', true]; - $tests[] = ['presence-private-test', 'private-test', true]; - $tests[] = ['presence-presence-test', 'presence-test', true]; - $tests[] = ['public-test', 'public-test', false]; - - return $tests; - } -} - -class FakeBroadcasterUsingPusherChannelsNames extends Broadcaster -{ - use UsePusherChannelConventions; - - public function auth($request) - { - // - } - - public function validAuthenticationResponse($request, $result) - { - // - } - - public function broadcast(array $channels, $event, array $payload = []) - { - // - } -} From 330e75e3aac9b13a4e9c527436d3e41ad3564b56 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Fri, 15 Nov 2024 15:55:09 +0800 Subject: [PATCH 05/30] phpstan fix --- .../src/Broadcasters/Broadcaster.php | 33 ++++++++++--------- .../src/Contracts/Broadcaster.php | 3 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index 73b12f38..d8d73a54 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -190,11 +190,12 @@ protected function resolveBinding(string $key, string $value, array $callbackPar */ protected function resolveExplicitBindingIfPossible(string $key, string $value): mixed { - $binder = $this->binder(); + // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar + // $binder = $this->binder(); - if ($binder && $binder->getBindingCallback($key)) { - return call_user_func($binder->getBindingCallback($key), $value); - } + // if ($binder && $binder->getBindingCallback($key)) { + // return call_user_func($binder->getBindingCallback($key), $value); + // } return $value; } @@ -242,23 +243,23 @@ protected function formatChannels(array $channels): array }, $channels); } + // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar /** * Get the model binding registrar instance. * * @return \Illuminate\Contracts\Routing\BindingRegistrar */ - protected function binder() - { - // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar - // if (! $this->bindingRegistrar) { - // $this->bindingRegistrar = ApplicationContext::getContainer()->has(BindingRegistrar::class) - // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) - // : null; - // } - // - // return $this->bindingRegistrar; - return null; - } + // protected function binder() + // { + // if (! $this->bindingRegistrar) { + // $this->bindingRegistrar = ApplicationContext::getContainer()->has(BindingRegistrar::class) + // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) + // : null; + // } + // + // return $this->bindingRegistrar; + // return null; + // } /** * Normalize the given callback into a callable. diff --git a/src/broadcasting/src/Contracts/Broadcaster.php b/src/broadcasting/src/Contracts/Broadcaster.php index a1a55457..6c9cfb46 100644 --- a/src/broadcasting/src/Contracts/Broadcaster.php +++ b/src/broadcasting/src/Contracts/Broadcaster.php @@ -5,6 +5,7 @@ namespace SwooleTW\Hyperf\Broadcasting\Contracts; use Hyperf\HttpServer\Contract\RequestInterface; +use SwooleTW\Hyperf\Broadcasting\BroadcastException; interface Broadcaster { @@ -20,8 +21,6 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re /** * Broadcast the given event. - * - * @throws \Illuminate\Broadcasting\BroadcastException */ public function broadcast(array $channels, string $event, array $payload = []): void; } From bb905f69c06b5177d8d0b64b9671a07e846b7a3d Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Fri, 15 Nov 2024 16:04:26 +0800 Subject: [PATCH 06/30] fix cs --- src/broadcasting/src/Broadcasters/Broadcaster.php | 3 ++- src/broadcasting/src/Contracts/Broadcaster.php | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index d8d73a54..93ae6bae 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -247,6 +247,7 @@ protected function formatChannels(array $channels): array /** * Get the model binding registrar instance. * + * @param mixed $callback * @return \Illuminate\Contracts\Routing\BindingRegistrar */ // protected function binder() @@ -256,7 +257,7 @@ protected function formatChannels(array $channels): array // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) // : null; // } - // + // // return $this->bindingRegistrar; // return null; // } diff --git a/src/broadcasting/src/Contracts/Broadcaster.php b/src/broadcasting/src/Contracts/Broadcaster.php index 6c9cfb46..4d5462dc 100644 --- a/src/broadcasting/src/Contracts/Broadcaster.php +++ b/src/broadcasting/src/Contracts/Broadcaster.php @@ -5,7 +5,6 @@ namespace SwooleTW\Hyperf\Broadcasting\Contracts; use Hyperf\HttpServer\Contract\RequestInterface; -use SwooleTW\Hyperf\Broadcasting\BroadcastException; interface Broadcaster { From d7a09b65a49c2465ee93e23af7623bea581f67c5 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Sat, 16 Nov 2024 10:22:32 +0800 Subject: [PATCH 07/30] [WIP] --- composer.json | 5 +- src/broadcasting/src/BroadcastException.php | 10 + .../src/Broadcasters/AblyBroadcaster.php | 207 ++++++++++++++++++ tests/Broadcasting/AblyBroadcasterTest.php | 159 ++++++++++++++ tests/Broadcasting/BroadcasterTest.php | 11 +- 5 files changed, 382 insertions(+), 10 deletions(-) create mode 100644 src/broadcasting/src/BroadcastException.php create mode 100644 src/broadcasting/src/Broadcasters/AblyBroadcaster.php create mode 100644 tests/Broadcasting/AblyBroadcasterTest.php diff --git a/composer.json b/composer.json index 054096fa..6e3752a0 100644 --- a/composer.json +++ b/composer.json @@ -133,9 +133,12 @@ "aws/aws-sdk-php": "Required to use the SES mail driver (^3.235.5).", "symfony/http-client": "Required to use the Symfony API mail transports (^6.2).", "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.2).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2)." + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2).", + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0)." }, "require-dev": { + "ably/ably-php": "^1.0", "fakerphp/faker": "^2.0", "friendsofphp/php-cs-fixer": "^3.57.2", "hyperf/devtool": "~3.1.0", diff --git a/src/broadcasting/src/BroadcastException.php b/src/broadcasting/src/BroadcastException.php new file mode 100644 index 00000000..290f5b20 --- /dev/null +++ b/src/broadcasting/src/BroadcastException.php @@ -0,0 +1,10 @@ +ably = $ably; + } + + /** + * Authenticate the incoming request for a given channel. + * + * @throws AccessDeniedHttpException + */ + public function auth(RequestInterface $request): mixed + { + $originalChannelName = $request->input('channel_name'); + $channelName = $this->normalizeChannelName($originalChannelName); + + if (empty($originalChannelName) + || ($this->isGuardedChannel($originalChannelName) && ! $this->retrieveUser($channelName)) + ) { + throw new AccessDeniedHttpException; + } + + return parent::verifyUserCanAccessChannel( + $request, $channelName + ); + } + + /** + * Return the valid authentication response. + */ + public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed + { + $originalChannelName = $request->input('channel_name'); + $socketId = $request->input('socket_id'); + + if (str_starts_with($originalChannelName, 'private')) { + $signature = $this->generateAblySignature($originalChannelName, $socketId); + + return ['auth' => $this->getPublicToken().':'.$signature]; + } + + $channelName = $this->normalizeChannelName($originalChannelName); + + $user = $this->retrieveUser($channelName); + + $broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting') + ? $user->getAuthIdentifierForBroadcasting() + : $user->getAuthIdentifier(); + + $signature = $this->generateAblySignature( + $originalChannelName, + $socketId, + $userData = array_filter([ + 'user_id' => (string) $broadcastIdentifier, + 'user_info' => $result, + ]) + ); + + return [ + 'auth' => $this->getPublicToken().':'.$signature, + 'channel_data' => json_encode($userData), + ]; + } + + /** + * Generate the signature needed for Ably authentication headers. + */ + public function generateAblySignature(string $channelName, string $socketId, array|null $userData = null): string + { + return hash_hmac( + 'sha256', + sprintf('%s:%s%s', $socketId, $channelName, $userData ? ':'.json_encode($userData) : ''), + $this->getPrivateToken(), + ); + } + + /** + * Broadcast the given event. + * + * @throws BroadcastException + */ + public function broadcast(array $channels, string $event, array $payload = []): void + { + try { + foreach ($this->formatChannels($channels) as $channel) { + $this->ably->channels->get($channel)->publish( + $this->buildAblyMessage($event, $payload) + ); + } + } catch (AblyException $e) { + throw new BroadcastException( + sprintf('Ably error: %s', $e->getMessage()) + ); + } + } + + /** + * Build an Ably message object for broadcasting. + */ + protected function buildAblyMessage(string $event, array $payload = []): AblyMessage + { + return tap(new AblyMessage, function ($message) use ($event, $payload) { + $message->name = $event; + $message->data = $payload; + $message->connectionKey = data_get($payload, 'socket'); + }); + } + + /** + * Return true if the channel is protected by authentication. + */ + public function isGuardedChannel(string $channel): bool + { + return Str::startsWith($channel, ['private-', 'presence-']); + } + + /** + * Remove prefix from channel name. + */ + public function normalizeChannelName(string $channel): string + { + if ($this->isGuardedChannel($channel)) { + return str_starts_with($channel, 'private-') + ? Str::replaceFirst('private-', '', $channel) + : Str::replaceFirst('presence-', '', $channel); + } + + return $channel; + } + + /** + * Format the channel array into an array of strings. + */ + protected function formatChannels(array $channels): array + { + return array_map(function ($channel) { + $channel = (string) $channel; + + if (Str::startsWith($channel, ['private-', 'presence-'])) { + return str_starts_with($channel, 'private-') + ? Str::replaceFirst('private-', 'private:', $channel) + : Str::replaceFirst('presence-', 'presence:', $channel); + } + + return 'public:'.$channel; + }, $channels); + } + + /** + * Get the public token value from the Ably key. + */ + protected function getPublicToken(): string + { + return Str::before($this->ably->options->key, ':'); + } + + /** + * Get the private token value from the Ably key. + */ + protected function getPrivateToken(): string + { + return Str::after($this->ably->options->key, ':'); + } + + /** + * Get the underlying Ably SDK instance. + */ + public function getAbly(): AblyRest + { + return $this->ably; + } + + /** + * Set the underlying Ably SDK instance. + */ + public function setAbly(AblyRest $ably): void + { + $this->ably = $ably; + } +} diff --git a/tests/Broadcasting/AblyBroadcasterTest.php b/tests/Broadcasting/AblyBroadcasterTest.php new file mode 100644 index 00000000..70ca5b3a --- /dev/null +++ b/tests/Broadcasting/AblyBroadcasterTest.php @@ -0,0 +1,159 @@ +ably = m::mock(AblyRest::class, ['abcd:efgh']); + + $this->broadcaster = m::mock(AblyBroadcaster::class, [$this->ably])->makePartial(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + m::close(); + } + + public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue() + { + $this->broadcaster->channel('test', function () { + return true; + }); + + $this->broadcaster->shouldReceive('validAuthenticationResponse') + ->once(); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('private-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenCallbackReturnFalse() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return false; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('private-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequestUserNotFound() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return true; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithoutUserForChannel('private-test') + ); + } + + public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCallbackReturnAnArray() + { + $returnData = [1, 2, 3, 4]; + $this->broadcaster->channel('test', function () use ($returnData) { + return $returnData; + }); + + $this->broadcaster->shouldReceive('validAuthenticationResponse') + ->once(); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('presence-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCallbackReturnNull() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + // + }); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('presence-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenRequestUserNotFound() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return [1, 2, 3, 4]; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithoutUserForChannel('presence-test') + ); + } + + /** + * @param string $channel + * @return \Illuminate\Http\Request + */ + protected function getMockRequestWithUserForChannel($channel) + { + $request = m::mock(Request::class); + $request->shouldReceive('all')->andReturn(['channel_name' => $channel, 'socket_id' => 'abcd.1234']); + + $request->shouldReceive('input') + ->with('callback', false) + ->andReturn(false); + + $user = m::mock('User'); + $user->shouldReceive('getAuthIdentifierForBroadcasting') + ->andReturn(42); + $user->shouldReceive('getAuthIdentifier') + ->andReturn(42); + + $request->shouldReceive('user') + ->andReturn($user); + + return $request; + } + + /** + * @param string $channel + * @return \Illuminate\Http\Request + */ + protected function getMockRequestWithoutUserForChannel($channel) + { + $request = m::mock(Request::class); + $request->shouldReceive('all')->andReturn(['channel_name' => $channel]); + + $request->shouldReceive('user') + ->andReturn(null); + + return $request; + } +} diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 49c195af..f1af75c5 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -40,17 +40,10 @@ protected function setUp(): void $this->broadcaster = new FakeBroadcaster(); - $this->container = $this->getApplication([ + $container = $this->getApplication([ FactoryContract::class => fn () => new stdClass(), ]); - ApplicationContext::setContainer($this->container); - } - - protected function tearDown(): void - { - m::close(); - // - // Container::setInstance(null); + ApplicationContext::setContainer($container); } public function testExtractingParametersWhileCheckingForUserAccess() From 69a1cb14f635caaecefea4e0c7763b3244b97d27 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 12:55:00 +0800 Subject: [PATCH 08/30] add AblyBroadcaster --- src/auth/src/Authenticatable.php | 8 +++ .../src/Broadcasters/AblyBroadcaster.php | 12 ++-- tests/Broadcasting/AblyBroadcasterTest.php | 60 +++++++++---------- tests/Broadcasting/BroadcasterTest.php | 15 ++++- 4 files changed, 51 insertions(+), 44 deletions(-) diff --git a/src/auth/src/Authenticatable.php b/src/auth/src/Authenticatable.php index 230b18c9..363b45f1 100644 --- a/src/auth/src/Authenticatable.php +++ b/src/auth/src/Authenticatable.php @@ -22,6 +22,14 @@ public function getAuthIdentifier(): mixed return $this->{$this->getAuthIdentifierName()}; } + /** + * Get the unique broadcast identifier for the user. + */ + public function getAuthIdentifierForBroadcasting(): mixed + { + return $this->getAuthIdentifier(); + } + /** * Get the password for the user. */ diff --git a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php index ebad5c72..ef29331b 100644 --- a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php @@ -12,10 +12,6 @@ use SwooleTW\Hyperf\Broadcasting\BroadcastException; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; -/** - * @author Matthew Hall (matthall28@gmail.com) - * @author Taylor Otwell (taylor@laravel.com) - */ class AblyBroadcaster extends Broadcaster { /** @@ -26,7 +22,7 @@ class AblyBroadcaster extends Broadcaster /** * Create a new broadcaster instance. */ - public function __construct(AblyRest $ably): void + public function __construct(AblyRest $ably) { $this->ably = $ably; } @@ -41,7 +37,7 @@ public function auth(RequestInterface $request): mixed $originalChannelName = $request->input('channel_name'); $channelName = $this->normalizeChannelName($originalChannelName); - if (empty($originalChannelName) + if (empty($originalChannelName) || ($this->isGuardedChannel($originalChannelName) && ! $this->retrieveUser($channelName)) ) { throw new AccessDeniedHttpException; @@ -71,8 +67,8 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re $user = $this->retrieveUser($channelName); $broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting') - ? $user->getAuthIdentifierForBroadcasting() - : $user->getAuthIdentifier(); + ? $user->getAuthIdentifierForBroadcasting() + : $user->getAuthIdentifier(); $signature = $this->generateAblySignature( $originalChannelName, diff --git a/tests/Broadcasting/AblyBroadcasterTest.php b/tests/Broadcasting/AblyBroadcasterTest.php index 70ca5b3a..0156ae91 100644 --- a/tests/Broadcasting/AblyBroadcasterTest.php +++ b/tests/Broadcasting/AblyBroadcasterTest.php @@ -5,20 +5,24 @@ namespace SwooleTW\Hyperf\Tests\Broadcasting; use Ably\AblyRest; -use Illuminate\Broadcasting\Broadcasters\AblyBroadcaster; -use Illuminate\Http\Request; +use Hyperf\HttpServer\Request; use Mockery as m; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use stdClass; +use SwooleTW\Hyperf\Auth\Contracts\FactoryContract; +use SwooleTW\Hyperf\Broadcasting\Broadcasters\AblyBroadcaster; +use SwooleTW\Hyperf\Foundation\ApplicationContext; +use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; +use SwooleTW\Hyperf\Support\Facades\Auth; +use SwooleTW\Hyperf\Support\Facades\Facade; +use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; class AblyBroadcasterTest extends TestCase { - /** - * @var \Illuminate\Broadcasting\Broadcasters\AblyBroadcaster - */ - public $broadcaster; + use HasMockedApplication; - public $ably; + public AblyBroadcaster $broadcaster; + public AblyRest $ably; protected function setUp(): void { @@ -27,6 +31,11 @@ protected function setUp(): void $this->ably = m::mock(AblyRest::class, ['abcd:efgh']); $this->broadcaster = m::mock(AblyBroadcaster::class, [$this->ably])->makePartial(); + + $container = $this->getApplication([ + FactoryContract::class => fn () => new stdClass(), + ]); + ApplicationContext::setContainer($container); } protected function tearDown(): void @@ -34,6 +43,8 @@ protected function tearDown(): void parent::tearDown(); m::close(); + + Facade::clearResolvedInstances(); } public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue() @@ -42,8 +53,7 @@ public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCal return true; }); - $this->broadcaster->shouldReceive('validAuthenticationResponse') - ->once(); + $this->broadcaster->shouldReceive('validAuthenticationResponse')->once(); $this->broadcaster->auth( $this->getMockRequestWithUserForChannel('private-test') @@ -83,8 +93,7 @@ public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCa return $returnData; }); - $this->broadcaster->shouldReceive('validAuthenticationResponse') - ->once(); + $this->broadcaster->shouldReceive('validAuthenticationResponse')->once(); $this->broadcaster->auth( $this->getMockRequestWithUserForChannel('presence-test') @@ -96,7 +105,6 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCal $this->expectException(AccessDeniedHttpException::class); $this->broadcaster->channel('test', function () { - // }); $this->broadcaster->auth( @@ -117,18 +125,10 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenReq ); } - /** - * @param string $channel - * @return \Illuminate\Http\Request - */ - protected function getMockRequestWithUserForChannel($channel) + protected function getMockRequestWithUserForChannel(string $channel): Request { $request = m::mock(Request::class); - $request->shouldReceive('all')->andReturn(['channel_name' => $channel, 'socket_id' => 'abcd.1234']); - - $request->shouldReceive('input') - ->with('callback', false) - ->andReturn(false); + $request->shouldReceive('input')->with('channel_name')->andReturn($channel); $user = m::mock('User'); $user->shouldReceive('getAuthIdentifierForBroadcasting') @@ -136,23 +136,17 @@ protected function getMockRequestWithUserForChannel($channel) $user->shouldReceive('getAuthIdentifier') ->andReturn(42); - $request->shouldReceive('user') - ->andReturn($user); + Auth::shouldReceive('user')->andReturn($user); return $request; } - /** - * @param string $channel - * @return \Illuminate\Http\Request - */ - protected function getMockRequestWithoutUserForChannel($channel) + protected function getMockRequestWithoutUserForChannel(string $channel): Request { $request = m::mock(Request::class); - $request->shouldReceive('all')->andReturn(['channel_name' => $channel]); + $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - $request->shouldReceive('user') - ->andReturn(null); + Auth::shouldReceive('user')->andReturn(null); return $request; } diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index f1af75c5..554651b9 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -20,6 +20,7 @@ use SwooleTW\Hyperf\Database\Eloquent\Model; use SwooleTW\Hyperf\HttpMessage\Exceptions\HttpException; use SwooleTW\Hyperf\Support\Facades\Auth; +use SwooleTW\Hyperf\Support\Facades\Facade; use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; /** @@ -46,6 +47,15 @@ protected function setUp(): void ApplicationContext::setContainer($container); } + protected function tearDown(): void + { + parent::tearDown(); + + m::close(); + + Facade::clearResolvedInstances(); + } + public function testExtractingParametersWhileCheckingForUserAccess() { Booted::$container[BroadcasterTestEloquentModelStub::class] = true; @@ -341,7 +351,8 @@ public function testUserAuthenticationWithInvalidUser() return null; }); - $user = $this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234'])); + $this->mockRequest('http://exa.com/foo?socket_id=1234.1234#boom'); + $user = $this->broadcaster->resolveAuthenticatedUser(new Request()); $this->assertNull($user); } @@ -349,8 +360,6 @@ public function testUserAuthenticationWithInvalidUser() public function testUserAuthenticationWithoutResolve() { $this->mockRequest('http://exa.com/foo?socket_id=1234.1234#boom'); - $user = $this->broadcaster->resolveAuthenticatedUser(new Request()); - $this->assertNull($this->broadcaster->resolveAuthenticatedUser(new Request())); } From 5613734ec18f12d06fe5f55437b717ab1cd0f359 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 12:57:52 +0800 Subject: [PATCH 09/30] fix cs --- src/broadcasting/src/BroadcastException.php | 3 ++- .../src/Broadcasters/AblyBroadcaster.php | 17 +++++++++-------- tests/Broadcasting/AblyBroadcasterTest.php | 9 +++++++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/broadcasting/src/BroadcastException.php b/src/broadcasting/src/BroadcastException.php index 290f5b20..83261605 100644 --- a/src/broadcasting/src/BroadcastException.php +++ b/src/broadcasting/src/BroadcastException.php @@ -1,10 +1,11 @@ isGuardedChannel($originalChannelName) && ! $this->retrieveUser($channelName)) ) { - throw new AccessDeniedHttpException; + throw new AccessDeniedHttpException(); } return parent::verifyUserCanAccessChannel( - $request, $channelName + $request, + $channelName ); } @@ -59,7 +60,7 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re if (str_starts_with($originalChannelName, 'private')) { $signature = $this->generateAblySignature($originalChannelName, $socketId); - return ['auth' => $this->getPublicToken().':'.$signature]; + return ['auth' => $this->getPublicToken() . ':' . $signature]; } $channelName = $this->normalizeChannelName($originalChannelName); @@ -80,7 +81,7 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re ); return [ - 'auth' => $this->getPublicToken().':'.$signature, + 'auth' => $this->getPublicToken() . ':' . $signature, 'channel_data' => json_encode($userData), ]; } @@ -88,11 +89,11 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re /** * Generate the signature needed for Ably authentication headers. */ - public function generateAblySignature(string $channelName, string $socketId, array|null $userData = null): string + public function generateAblySignature(string $channelName, string $socketId, ?array $userData = null): string { return hash_hmac( 'sha256', - sprintf('%s:%s%s', $socketId, $channelName, $userData ? ':'.json_encode($userData) : ''), + sprintf('%s:%s%s', $socketId, $channelName, $userData ? ':' . json_encode($userData) : ''), $this->getPrivateToken(), ); } @@ -122,7 +123,7 @@ public function broadcast(array $channels, string $event, array $payload = []): */ protected function buildAblyMessage(string $event, array $payload = []): AblyMessage { - return tap(new AblyMessage, function ($message) use ($event, $payload) { + return tap(new AblyMessage(), function ($message) use ($event, $payload) { $message->name = $event; $message->data = $payload; $message->connectionKey = data_get($payload, 'socket'); @@ -165,7 +166,7 @@ protected function formatChannels(array $channels): array : Str::replaceFirst('presence-', 'presence:', $channel); } - return 'public:'.$channel; + return 'public:' . $channel; }, $channels); } diff --git a/tests/Broadcasting/AblyBroadcasterTest.php b/tests/Broadcasting/AblyBroadcasterTest.php index 0156ae91..ff97a079 100644 --- a/tests/Broadcasting/AblyBroadcasterTest.php +++ b/tests/Broadcasting/AblyBroadcasterTest.php @@ -17,11 +17,16 @@ use SwooleTW\Hyperf\Support\Facades\Facade; use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; +/** + * @internal + * @coversNothing + */ class AblyBroadcasterTest extends TestCase { use HasMockedApplication; public AblyBroadcaster $broadcaster; + public AblyRest $ably; protected function setUp(): void @@ -132,9 +137,9 @@ protected function getMockRequestWithUserForChannel(string $channel): Request $user = m::mock('User'); $user->shouldReceive('getAuthIdentifierForBroadcasting') - ->andReturn(42); + ->andReturn(42); $user->shouldReceive('getAuthIdentifier') - ->andReturn(42); + ->andReturn(42); Auth::shouldReceive('user')->andReturn($user); From 825da0260bddc527472fb871390d5821bb33648f Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 13:48:15 +0800 Subject: [PATCH 10/30] phpstan fix --- phpstan.neon.dist | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index e2e5e13c..fea14a3d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -34,4 +34,6 @@ parameters: - '#Call to an undefined static method SwooleTW\\Hyperf\\Support\\Facades\\#' - '#Call to an undefined method Psr\\Container\\ContainerInterface::make\(\)#' - message: '#Call to an undefined method SwooleTW\\Hyperf\\Foundation\\Testing\\TestCase::#' - path: src/foundation/src/Testing/TestCase.php \ No newline at end of file + path: src/foundation/src/Testing/TestCase.php + - message: '#Method Ably\\Channel::publish\(\) invoked with 1 parameter, 2 required.#' + path: src/broadcasting/src/Broadcasters/AblyBroadcaster.php From a9429cba1aaf72dfec9e96c2dc5a3d9957a9794f Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 14:50:07 +0800 Subject: [PATCH 11/30] add PusherBroadcaster --- .../src/Broadcasters/AblyBroadcaster.php | 24 +- .../src/Broadcasters/PusherBroadcaster.php | 165 +++++++++++++ .../UsePusherChannelConventions.php | 30 +++ tests/Broadcasting/AblyBroadcasterTest.php | 5 +- tests/Broadcasting/PusherBroadcasterTest.php | 216 ++++++++++++++++++ 5 files changed, 424 insertions(+), 16 deletions(-) create mode 100644 src/broadcasting/src/Broadcasters/PusherBroadcaster.php create mode 100644 src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php create mode 100644 tests/Broadcasting/PusherBroadcasterTest.php diff --git a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php index bb4088c5..ce53bc7a 100644 --- a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php @@ -34,18 +34,18 @@ public function __construct(AblyRest $ably) */ public function auth(RequestInterface $request): mixed { - $originalChannelName = $request->input('channel_name'); - $channelName = $this->normalizeChannelName($originalChannelName); + $channelName = $request->input('channel_name'); + $normalizeChannelName = $this->normalizeChannelName($channelName); - if (empty($originalChannelName) - || ($this->isGuardedChannel($originalChannelName) && ! $this->retrieveUser($channelName)) + if (empty($channelName) + || ($this->isGuardedChannel($channelName) && ! $this->retrieveUser($normalizeChannelName)) ) { throw new AccessDeniedHttpException(); } return parent::verifyUserCanAccessChannel( $request, - $channelName + $normalizeChannelName ); } @@ -54,25 +54,25 @@ public function auth(RequestInterface $request): mixed */ public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed { - $originalChannelName = $request->input('channel_name'); + $channelName = $request->input('channel_name'); $socketId = $request->input('socket_id'); - if (str_starts_with($originalChannelName, 'private')) { - $signature = $this->generateAblySignature($originalChannelName, $socketId); + if (str_starts_with($channelName, 'private')) { + $signature = $this->generateAblySignature($channelName, $socketId); return ['auth' => $this->getPublicToken() . ':' . $signature]; } - $channelName = $this->normalizeChannelName($originalChannelName); - - $user = $this->retrieveUser($channelName); + $user = $this->retrieveUser( + $this->normalizeChannelName($channelName) + ); $broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting') ? $user->getAuthIdentifierForBroadcasting() : $user->getAuthIdentifier(); $signature = $this->generateAblySignature( - $originalChannelName, + $channelName, $socketId, $userData = array_filter([ 'user_id' => (string) $broadcastIdentifier, diff --git a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php new file mode 100644 index 00000000..a6838cc9 --- /dev/null +++ b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php @@ -0,0 +1,165 @@ +pusher = $pusher; + } + + /** + * Resolve the authenticated user payload for an incoming connection request. + * + * See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication + * See: https://pusher.com/docs/channels/server_api/authenticating-users/#response + */ + public function resolveAuthenticatedUser(RequestInterface $request): ?array + { + if (! $user = parent::resolveAuthenticatedUser($request)) { + return null; + } + + if (method_exists($this->pusher, 'authenticateUser')) { + return $this->pusher->authenticateUser($request->input('socket_id'), $user); + } + + $settings = $this->pusher->getSettings(); + $encodedUser = json_encode($user); + $decodedString = "{$request->input('socket_id')}::user::{$encodedUser}"; + + $auth = $settings['auth_key'].':'.hash_hmac( + 'sha256', $decodedString, $settings['secret'] + ); + + return [ + 'auth' => $auth, + 'user_data' => $encodedUser, + ]; + } + + /** + * Authenticate the incoming request for a given channel. + * + * @throws AccessDeniedHttpException + */ + public function auth(RequestInterface $request): mixed + { + $channelName = $request->input('channel_name'); + $normalizeChannelName = $this->normalizeChannelName($channelName); + + if (empty($channelName) + || ($this->isGuardedChannel($channelName) && ! $this->retrieveUser($normalizeChannelName)) + ) { + throw new AccessDeniedHttpException; + } + + return parent::verifyUserCanAccessChannel( + $request, $normalizeChannelName + ); + } + + /** + * Return the valid authentication response. + */ + public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed + { + $channelName = $request->input('channel_name'); + $socketId = $request->input('socket_id'); + + if (str_starts_with($channelName, 'private')) { + return $this->decodePusherResponse( + $request, + method_exists($this->pusher, 'authorizeChannel') + ? $this->pusher->authorizeChannel($channelName, $socketId) + : $this->pusher->socket_auth($channelName, $socketId) + ); + } + + $user = $this->retrieveUser( + $this->normalizeChannelName($channelName) + ); + + $broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting') + ? $user->getAuthIdentifierForBroadcasting() + : $user->getAuthIdentifier(); + + return $this->decodePusherResponse( + $request, + method_exists($this->pusher, 'authorizePresenceChannel') + ? $this->pusher->authorizePresenceChannel($channelName, $socketId, $broadcastIdentifier, $result) + : $this->pusher->presence_auth($channelName, $socketId, $broadcastIdentifier, $result) + ); + } + + /** + * Decode the given Pusher response. + */ + protected function decodePusherResponse(RequestInterface $request, mixed $response): array + { + if (! $request->input('callback', false)) { + return json_decode($response, true); + } + + return response()->json(json_decode($response, true))->withCallback($request->input('callback')); + } + + /** + * Broadcast the given event. + */ + public function broadcast(array $channels, string $event, array $payload = []): void + { + $socket = Arr::pull($payload, 'socket'); + + $parameters = $socket !== null ? ['socket_id' => $socket] : []; + + $channels = Collection::make($this->formatChannels($channels)); + + try { + $channels->chunk(100)->each(function ($channels) use ($event, $payload, $parameters) { + $this->pusher->trigger($channels->toArray(), $event, $payload, $parameters); + }); + } catch (ApiErrorException $e) { + throw new BroadcastException( + sprintf('Pusher error: %s.', $e->getMessage()) + ); + } + } + + /** + * Get the Pusher SDK instance. + */ + public function getPusher(): Pusher + { + return $this->pusher; + } + + /** + * Set the Pusher SDK instance. + */ + public function setPusher(Pusher $pusher): void + { + $this->pusher = $pusher; + } +} diff --git a/src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php b/src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php new file mode 100644 index 00000000..49cbc73d --- /dev/null +++ b/src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php @@ -0,0 +1,30 @@ +shouldReceive('input')->with('channel_name')->andReturn($channel); $user = m::mock('User'); - $user->shouldReceive('getAuthIdentifierForBroadcasting') - ->andReturn(42); - $user->shouldReceive('getAuthIdentifier') - ->andReturn(42); + $user->shouldReceive('getAuthIdentifier')->andReturn(42); Auth::shouldReceive('user')->andReturn($user); diff --git a/tests/Broadcasting/PusherBroadcasterTest.php b/tests/Broadcasting/PusherBroadcasterTest.php new file mode 100644 index 00000000..66afc020 --- /dev/null +++ b/tests/Broadcasting/PusherBroadcasterTest.php @@ -0,0 +1,216 @@ +pusher = m::mock(Pusher::class); + $this->broadcaster = m::mock(PusherBroadcaster::class, [$this->pusher])->makePartial(); + + $container = $this->getApplication([ + FactoryContract::class => fn () => new stdClass(), + ]); + ApplicationContext::setContainer($container); + } + + protected function tearDown(): void + { + parent::tearDown(); + + m::close(); + + Facade::clearResolvedInstances(); + } + + public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue() + { + $this->broadcaster->channel('test', function () { + return true; + }); + + $this->broadcaster->shouldReceive('validAuthenticationResponse')->once(); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('private-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenCallbackReturnFalse() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return false; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('private-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequestUserNotFound() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return true; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithoutUserForChannel('private-test') + ); + } + + public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCallbackReturnAnArray() + { + $returnData = [1, 2, 3, 4]; + $this->broadcaster->channel('test', function () use ($returnData) { + return $returnData; + }); + + $this->broadcaster->shouldReceive('validAuthenticationResponse')->once(); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('presence-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCallbackReturnNull() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + }); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('presence-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenRequestUserNotFound() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return [1, 2, 3, 4]; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithoutUserForChannel('presence-test') + ); + } + + public function testValidAuthenticationResponseCallPusherSocketAuthMethodWithPrivateChannel() + { + $request = $this->getMockRequestWithUserForChannel('private-test'); + + $data = [ + 'auth' => 'abcd:efgh', + ]; + + $this->pusher->shouldReceive('socket_auth') + ->once() + ->andReturn(json_encode($data)); + + $this->assertEquals( + $data, + $this->broadcaster->validAuthenticationResponse($request, true) + ); + } + + public function testValidAuthenticationResponseCallPusherPresenceAuthMethodWithPresenceChannel() + { + $request = $this->getMockRequestWithUserForChannel('presence-test'); + + $data = [ + 'auth' => 'abcd:efgh', + 'channel_data' => [ + 'user_id' => 42, + 'user_info' => [1, 2, 3, 4], + ], + ]; + + $this->pusher->shouldReceive('presence_auth') + ->once() + ->andReturn(json_encode($data)); + + $this->assertEquals( + $data, + $this->broadcaster->validAuthenticationResponse($request, true) + ); + } + + public function testUserAuthenticationForPusher() + { + $this->pusher + ->shouldReceive('getSettings') + ->andReturn([ + 'auth_key' => '278d425bdf160c739803', + 'secret' => '7ad3773142a6692b25b8', + ]); + + $this->broadcaster = new PusherBroadcaster($this->pusher); + + $this->broadcaster->resolveAuthenticatedUserUsing(function () { + return ['id' => '12345']; + }); + + $response = $this->broadcaster->resolveAuthenticatedUser( + $this->getMockRequestWithUserForChannel('presence-test') + ); + + // The result is hard-coded from the Pusher docs + // See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication + $this->assertSame([ + 'auth' => '278d425bdf160c739803:4708d583dada6a56435fb8bc611c77c359a31eebde13337c16ab43aa6de336ba', + 'user_data' => json_encode(['id' => '12345']), + ], $response); + } + + protected function getMockRequestWithUserForChannel(string $channel): Request + { + $request = m::mock(Request::class); + $request->shouldReceive('input')->with('channel_name')->andReturn($channel); + $request->shouldReceive('input')->with('socket_id')->andReturn('1234.1234'); + $request->shouldReceive('input')->with('callback', false)->andReturn(false); + + $user = m::mock('User'); + $user->shouldReceive('getAuthIdentifier')->andReturn(42); + + Auth::shouldReceive('user')->andReturn($user); + + return $request; + } + + protected function getMockRequestWithoutUserForChannel(string $channel): Request + { + $request = m::mock(Request::class); + $request->shouldReceive('input')->with('channel_name')->andReturn($channel); + + Auth::shouldReceive('user')->andReturn(null); + + return $request; + } +} From 746ee43001649baba5ea74bf59e4967de042fd39 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 16:03:49 +0800 Subject: [PATCH 12/30] phpstan fix --- composer.json | 7 ++-- .../src/Broadcasters/PusherBroadcaster.php | 34 +++++++--------- .../UsePusherChannelConventions.php | 2 + tests/Broadcasting/PusherBroadcasterTest.php | 39 ++++++++++--------- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/composer.json b/composer.json index 6e3752a0..dd2f60be 100644 --- a/composer.json +++ b/composer.json @@ -140,20 +140,21 @@ "require-dev": { "ably/ably-php": "^1.0", "fakerphp/faker": "^2.0", + "filp/whoops": "^2.15", "friendsofphp/php-cs-fixer": "^3.57.2", "hyperf/devtool": "~3.1.0", "hyperf/redis": "~3.1.0", "hyperf/testing": "~3.1.0", "hyperf/view-engine": "~3.1.0", "league/flysystem": "^3.0", - "league/flysystem-path-prefixing": "^3.3", - "league/flysystem-read-only": "^3.3", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-google-cloud-storage": "^3.0", - "filp/whoops": "^2.15", + "league/flysystem-path-prefixing": "^3.3", + "league/flysystem-read-only": "^3.3", "mockery/mockery": "^1.5.1", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.0.7", + "pusher/pusher-php-server": "^7.2", "swoole/ide-helper": "~5.1.0" }, "config": { diff --git a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php index a6838cc9..a8823d44 100644 --- a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php @@ -42,15 +42,20 @@ public function resolveAuthenticatedUser(RequestInterface $request): ?array } if (method_exists($this->pusher, 'authenticateUser')) { - return $this->pusher->authenticateUser($request->input('socket_id'), $user); + return json_decode( + $this->pusher->authenticateUser($request->input('socket_id'), $user), + true, + ); } $settings = $this->pusher->getSettings(); $encodedUser = json_encode($user); $decodedString = "{$request->input('socket_id')}::user::{$encodedUser}"; - $auth = $settings['auth_key'].':'.hash_hmac( - 'sha256', $decodedString, $settings['secret'] + $auth = $settings['auth_key'] . ':' . hash_hmac( + 'sha256', + $decodedString, + $settings['secret'] ); return [ @@ -72,11 +77,12 @@ public function auth(RequestInterface $request): mixed if (empty($channelName) || ($this->isGuardedChannel($channelName) && ! $this->retrieveUser($normalizeChannelName)) ) { - throw new AccessDeniedHttpException; + throw new AccessDeniedHttpException(); } return parent::verifyUserCanAccessChannel( - $request, $normalizeChannelName + $request, + $normalizeChannelName ); } @@ -90,10 +96,7 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re if (str_starts_with($channelName, 'private')) { return $this->decodePusherResponse( - $request, - method_exists($this->pusher, 'authorizeChannel') - ? $this->pusher->authorizeChannel($channelName, $socketId) - : $this->pusher->socket_auth($channelName, $socketId) + $this->pusher->authorizeChannel($channelName, $socketId), ); } @@ -106,23 +109,16 @@ public function validAuthenticationResponse(RequestInterface $request, mixed $re : $user->getAuthIdentifier(); return $this->decodePusherResponse( - $request, - method_exists($this->pusher, 'authorizePresenceChannel') - ? $this->pusher->authorizePresenceChannel($channelName, $socketId, $broadcastIdentifier, $result) - : $this->pusher->presence_auth($channelName, $socketId, $broadcastIdentifier, $result) + $this->pusher->authorizePresenceChannel($channelName, $socketId, (string) $broadcastIdentifier, $result) ); } /** * Decode the given Pusher response. */ - protected function decodePusherResponse(RequestInterface $request, mixed $response): array + protected function decodePusherResponse(mixed $response): array { - if (! $request->input('callback', false)) { - return json_decode($response, true); - } - - return response()->json(json_decode($response, true))->withCallback($request->input('callback')); + return json_decode($response, true); } /** diff --git a/src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php b/src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php index 49cbc73d..54ae47fe 100644 --- a/src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php +++ b/src/broadcasting/src/Broadcasters/UsePusherChannelConventions.php @@ -1,5 +1,7 @@ 'abcd:efgh', ]; - $this->pusher->shouldReceive('socket_auth') - ->once() - ->andReturn(json_encode($data)); + $this->pusher->shouldReceive('authorizeChannel') + ->once() + ->andReturn(json_encode($data)); $this->assertEquals( $data, @@ -152,9 +159,9 @@ public function testValidAuthenticationResponseCallPusherPresenceAuthMethodWithP ], ]; - $this->pusher->shouldReceive('presence_auth') - ->once() - ->andReturn(json_encode($data)); + $this->pusher->shouldReceive('authorizePresenceChannel') + ->once() + ->andReturn(json_encode($data)); $this->assertEquals( $data, @@ -164,12 +171,14 @@ public function testValidAuthenticationResponseCallPusherPresenceAuthMethodWithP public function testUserAuthenticationForPusher() { + $authenticateUser = [ + 'auth' => '278d425bdf160c739803:4708d583dada6a56435fb8bc611c77c359a31eebde13337c16ab43aa6de336ba', + 'user_data' => json_encode(['id' => '12345']), + ]; + $this->pusher - ->shouldReceive('getSettings') - ->andReturn([ - 'auth_key' => '278d425bdf160c739803', - 'secret' => '7ad3773142a6692b25b8', - ]); + ->shouldReceive('authenticateUser') + ->andReturn(json_encode($authenticateUser)); $this->broadcaster = new PusherBroadcaster($this->pusher); @@ -181,12 +190,7 @@ public function testUserAuthenticationForPusher() $this->getMockRequestWithUserForChannel('presence-test') ); - // The result is hard-coded from the Pusher docs - // See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication - $this->assertSame([ - 'auth' => '278d425bdf160c739803:4708d583dada6a56435fb8bc611c77c359a31eebde13337c16ab43aa6de336ba', - 'user_data' => json_encode(['id' => '12345']), - ], $response); + $this->assertSame($authenticateUser, $response); } protected function getMockRequestWithUserForChannel(string $channel): Request @@ -194,7 +198,6 @@ protected function getMockRequestWithUserForChannel(string $channel): Request $request = m::mock(Request::class); $request->shouldReceive('input')->with('channel_name')->andReturn($channel); $request->shouldReceive('input')->with('socket_id')->andReturn('1234.1234'); - $request->shouldReceive('input')->with('callback', false)->andReturn(false); $user = m::mock('User'); $user->shouldReceive('getAuthIdentifier')->andReturn(42); From 613874301170d52729d468f734438dd1281a823f Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 18:09:21 +0800 Subject: [PATCH 13/30] add RedisBroadcaster --- src/broadcasting/composer.json | 2 + .../src/Broadcasters/RedisBroadcaster.php | 143 +++++++++++++ tests/Broadcasting/RedisBroadcasterTest.php | 193 ++++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 src/broadcasting/src/Broadcasters/RedisBroadcaster.php create mode 100644 tests/Broadcasting/RedisBroadcasterTest.php diff --git a/src/broadcasting/composer.json b/src/broadcasting/composer.json index 46318e33..49a5814c 100644 --- a/src/broadcasting/composer.json +++ b/src/broadcasting/composer.json @@ -31,6 +31,7 @@ "hyperf/collection": "~3.1.0", "hyperf/context": "~3.1.0", "hyperf/http-server": "~3.1.0", + "hyperf/redis": "~3.1.0", "swooletw/hyperf-framework": "dev-master", "swooletw/hyperf-router": "dev-master", "swooletw/hyperf-support": "dev-master", @@ -38,6 +39,7 @@ }, "suggest": { "ext-hash": "Required to use the Ably and Pusher broadcast drivers.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0)." }, diff --git a/src/broadcasting/src/Broadcasters/RedisBroadcaster.php b/src/broadcasting/src/Broadcasters/RedisBroadcaster.php new file mode 100644 index 00000000..9e906c9f --- /dev/null +++ b/src/broadcasting/src/Broadcasters/RedisBroadcaster.php @@ -0,0 +1,143 @@ +factory = $factory; + $this->prefix = $prefix; + $this->connection = $connection; + } + + /** + * Authenticate the incoming request for a given channel. + * + * @throws AccessDeniedHttpException + */ + public function auth(RequestInterface $request): mixed + { + $channelName = $request->input('channel_name'); + $normalizeChannelName = $this->normalizeChannelName( + str_replace($this->prefix, '', $channelName) + ); + + if (empty($channelName) + || ($this->isGuardedChannel($channelName) && ! $this->retrieveUser($normalizeChannelName) + )) { + throw new AccessDeniedHttpException(); + } + + return parent::verifyUserCanAccessChannel( + $request, $normalizeChannelName + ); + } + + /** + * Return the valid authentication response. + */ + public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed + { + if (is_bool($result)) { + return json_encode($result); + } + + $channelName = $this->normalizeChannelName($request->input('channel_name')); + + $user = $this->retrieveUser($channelName); + + $broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting') + ? $user->getAuthIdentifierForBroadcasting() + : $user->getAuthIdentifier(); + + return json_encode(['channel_data' => [ + 'user_id' => $broadcastIdentifier, + 'user_info' => $result, + ]]); + } + + /** + * Broadcast the given event. + * + * @throws BroadcastException + */ + public function broadcast(array $channels, string $event, array $payload = []): void + { + if (empty($channels)) { + return; + } + + $connection = $this->factory->get($this->connection); + + $payload = json_encode([ + 'event' => $event, + 'data' => $payload, + 'socket' => Arr::pull($payload, 'socket'), + ]); + + try { + $connection->eval( + $this->broadcastMultipleChannelsScript(), + [$payload, ...$this->formatChannels($channels)], + 0, + ); + } catch (ConnectionException|RedisException $e) { + throw new BroadcastException( + sprintf('Redis error: %s.', $e->getMessage()) + ); + } + } + + /** + * Get the Lua script for broadcasting to multiple channels. + * + * ARGV[1] - The payload + * ARGV[2...] - The channels + */ + protected function broadcastMultipleChannelsScript(): string + { + return <<<'LUA' + for i = 2, #ARGV do + redis.call('publish', ARGV[i], ARGV[1]) + end + LUA; + } + + /** + * Format the channel array into an array of strings. + */ + protected function formatChannels(array $channels): array + { + return array_map(function ($channel) { + return $this->prefix.$channel; + }, parent::formatChannels($channels)); + } +} diff --git a/tests/Broadcasting/RedisBroadcasterTest.php b/tests/Broadcasting/RedisBroadcasterTest.php new file mode 100644 index 00000000..80438f54 --- /dev/null +++ b/tests/Broadcasting/RedisBroadcasterTest.php @@ -0,0 +1,193 @@ +broadcaster = m::mock(RedisBroadcaster::class)->makePartial(); + + $container = $this->getApplication([ + FactoryContract::class => fn () => new stdClass(), + ]); + ApplicationContext::setContainer($container); + } + + protected function tearDown(): void + { + parent::tearDown(); + + m::close(); + + Facade::clearResolvedInstances(); + } + + public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue() + { + $this->broadcaster->channel('test', function () { + return true; + }); + + $this->broadcaster->shouldReceive('validAuthenticationResponse')->once(); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('private-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenCallbackReturnFalse() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return false; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('private-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPrivateChannelWhenRequestUserNotFound() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return true; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithoutUserForChannel('private-test') + ); + } + + public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCallbackReturnAnArray() + { + $returnData = [1, 2, 3, 4]; + $this->broadcaster->channel('test', function () use ($returnData) { + return $returnData; + }); + + $this->broadcaster->shouldReceive('validAuthenticationResponse') + ->once(); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('presence-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCallbackReturnNull() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + // + }); + + $this->broadcaster->auth( + $this->getMockRequestWithUserForChannel('presence-test') + ); + } + + public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenRequestUserNotFound() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->broadcaster->channel('test', function () { + return [1, 2, 3, 4]; + }); + + $this->broadcaster->auth( + $this->getMockRequestWithoutUserForChannel('presence-test') + ); + } + + public function testValidAuthenticationResponseWithPrivateChannel() + { + $request = $this->getMockRequestWithUserForChannel('private-test'); + + $this->assertEquals( + json_encode(true), + $this->broadcaster->validAuthenticationResponse($request, true) + ); + } + + public function testValidAuthenticationResponseWithPresenceChannel() + { + $request = $this->getMockRequestWithUserForChannel('presence-test'); + + $this->assertEquals( + json_encode([ + 'channel_data' => [ + 'user_id' => 42, + 'user_info' => [ + 'a' => 'b', + 'c' => 'd', + ], + ], + ]), + $this->broadcaster->validAuthenticationResponse($request, [ + 'a' => 'b', + 'c' => 'd', + ]) + ); + } + + /** + * Create a new config repository instance. + * + * @return \Illuminate\Config\Repository + */ + protected function createConfig() + { + return new Config([ + 'redis' => [ + 'options' => ['prefix' => 'laravel_database_'], + ], + ]); + } + + protected function getMockRequestWithUserForChannel(string $channel): Request + { + $request = m::mock(Request::class); + $request->shouldReceive('input')->with('channel_name')->andReturn($channel); + + $user = m::mock('User'); + $user->shouldReceive('getAuthIdentifier')->andReturn(42); + + Auth::shouldReceive('user')->andReturn($user); + + return $request; + } + + protected function getMockRequestWithoutUserForChannel(string $channel): Request + { + $request = m::mock(Request::class); + $request->shouldReceive('input')->with('channel_name')->andReturn($channel); + + Auth::shouldReceive('user')->andReturn(null); + + return $request; + } +} From f05425c96b8e741642893df938e63015e68ca6f0 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 18:10:58 +0800 Subject: [PATCH 14/30] fix cs --- .../src/Broadcasters/RedisBroadcaster.php | 13 +++++++------ tests/Broadcasting/RedisBroadcasterTest.php | 7 +++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/broadcasting/src/Broadcasters/RedisBroadcaster.php b/src/broadcasting/src/Broadcasters/RedisBroadcaster.php index 9e906c9f..561cc1f7 100644 --- a/src/broadcasting/src/Broadcasters/RedisBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/RedisBroadcaster.php @@ -6,8 +6,8 @@ use Hyperf\Collection\Arr; use Hyperf\HttpServer\Contract\RequestInterface; -use Hyperf\Redis\RedisFactory; use Hyperf\Pool\Exception\ConnectionException; +use Hyperf\Redis\RedisFactory; use RedisException; use SwooleTW\Hyperf\Broadcasting\BroadcastException; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; @@ -51,13 +51,14 @@ public function auth(RequestInterface $request): mixed ); if (empty($channelName) - || ($this->isGuardedChannel($channelName) && ! $this->retrieveUser($normalizeChannelName) - )) { + || ($this->isGuardedChannel($channelName) && ! $this->retrieveUser($normalizeChannelName)) + ) { throw new AccessDeniedHttpException(); } return parent::verifyUserCanAccessChannel( - $request, $normalizeChannelName + $request, + $normalizeChannelName ); } @@ -95,7 +96,7 @@ public function broadcast(array $channels, string $event, array $payload = []): return; } - $connection = $this->factory->get($this->connection); + $connection = $this->factory->get($this->connection); $payload = json_encode([ 'event' => $event, @@ -137,7 +138,7 @@ protected function broadcastMultipleChannelsScript(): string protected function formatChannels(array $channels): array { return array_map(function ($channel) { - return $this->prefix.$channel; + return $this->prefix . $channel; }, parent::formatChannels($channels)); } } diff --git a/tests/Broadcasting/RedisBroadcasterTest.php b/tests/Broadcasting/RedisBroadcasterTest.php index 80438f54..542603c3 100644 --- a/tests/Broadcasting/RedisBroadcasterTest.php +++ b/tests/Broadcasting/RedisBroadcasterTest.php @@ -16,6 +16,10 @@ use SwooleTW\Hyperf\Support\Facades\Facade; use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; +/** + * @internal + * @coversNothing + */ class RedisBroadcasterTest extends TestCase { use HasMockedApplication; @@ -90,7 +94,7 @@ public function testAuthCallValidAuthenticationResponseWithPresenceChannelWhenCa }); $this->broadcaster->shouldReceive('validAuthenticationResponse') - ->once(); + ->once(); $this->broadcaster->auth( $this->getMockRequestWithUserForChannel('presence-test') @@ -102,7 +106,6 @@ public function testAuthThrowAccessDeniedHttpExceptionWithPresenceChannelWhenCal $this->expectException(AccessDeniedHttpException::class); $this->broadcaster->channel('test', function () { - // }); $this->broadcaster->auth( From c6830a1ff5f4cd365bee9ef532c13a7aa4f88af4 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Mon, 18 Nov 2024 18:14:16 +0800 Subject: [PATCH 15/30] remove useless code --- tests/Broadcasting/RedisBroadcasterTest.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/Broadcasting/RedisBroadcasterTest.php b/tests/Broadcasting/RedisBroadcasterTest.php index 542603c3..23d7567e 100644 --- a/tests/Broadcasting/RedisBroadcasterTest.php +++ b/tests/Broadcasting/RedisBroadcasterTest.php @@ -157,20 +157,6 @@ public function testValidAuthenticationResponseWithPresenceChannel() ); } - /** - * Create a new config repository instance. - * - * @return \Illuminate\Config\Repository - */ - protected function createConfig() - { - return new Config([ - 'redis' => [ - 'options' => ['prefix' => 'laravel_database_'], - ], - ]); - } - protected function getMockRequestWithUserForChannel(string $channel): Request { $request = m::mock(Request::class); From fadff95fe5eaf3a03f2be5eaff9d68cdae64bce1 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Tue, 19 Nov 2024 11:43:27 +0800 Subject: [PATCH 16/30] add Channels and BroadcastEvent --- src/broadcasting/src/BroadcastEvent.php | 140 ++++++++++++++++++ .../src/Broadcasters/LogBroadcaster.php | 52 +++++++ .../src/Broadcasters/NullBroadcaster.php | 33 +++++ src/broadcasting/src/Channel.php | 32 ++++ src/broadcasting/src/Contracts/Factory.php | 15 ++ .../src/EncryptedPrivateChannel.php | 16 ++ .../src/InteractsWithBroadcasting.php | 35 +++++ src/broadcasting/src/LICENSE.md | 25 ++++ src/broadcasting/src/PresenceChannel.php | 16 ++ src/broadcasting/src/PrivateChannel.php | 20 +++ tests/Broadcasting/BroadcastEventTest.php | 105 +++++++++++++ 11 files changed, 489 insertions(+) create mode 100644 src/broadcasting/src/BroadcastEvent.php create mode 100644 src/broadcasting/src/Broadcasters/LogBroadcaster.php create mode 100644 src/broadcasting/src/Broadcasters/NullBroadcaster.php create mode 100644 src/broadcasting/src/Channel.php create mode 100644 src/broadcasting/src/Contracts/Factory.php create mode 100644 src/broadcasting/src/EncryptedPrivateChannel.php create mode 100644 src/broadcasting/src/InteractsWithBroadcasting.php create mode 100644 src/broadcasting/src/LICENSE.md create mode 100644 src/broadcasting/src/PresenceChannel.php create mode 100644 src/broadcasting/src/PrivateChannel.php create mode 100644 tests/Broadcasting/BroadcastEventTest.php diff --git a/src/broadcasting/src/BroadcastEvent.php b/src/broadcasting/src/BroadcastEvent.php new file mode 100644 index 00000000..5f4303b3 --- /dev/null +++ b/src/broadcasting/src/BroadcastEvent.php @@ -0,0 +1,140 @@ +event = $event; + $this->tries = property_exists($event, 'tries') ? $event->tries : null; + $this->timeout = property_exists($event, 'timeout') ? $event->timeout : null; + $this->backoff = property_exists($event, 'backoff') ? $event->backoff : null; + $this->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null; + $this->maxExceptions = property_exists($event, 'maxExceptions') ? $event->maxExceptions : null; + } + + /** + * Handle the queued job. + */ + public function handle(BroadcastingFactory $manager): void + { + $channels = Arr::wrap($this->event->broadcastOn()); + + if (empty($channels)) { + return; + } + + $name = method_exists($this->event, 'broadcastAs') + ? $this->event->broadcastAs() + : get_class($this->event); + + $connections = method_exists($this->event, 'broadcastConnections') + ? $this->event->broadcastConnections() + : [null]; + + $payload = $this->getPayloadFromEvent($this->event); + + foreach ($connections as $connection) { + $manager->connection($connection)->broadcast( + $channels, $name, $payload + ); + } + } + + /** + * Get the payload for the given event. + */ + protected function getPayloadFromEvent(mixed $event): array + { + if (method_exists($event, 'broadcastWith') + && ! is_null($payload = $event->broadcastWith()) + ) { + return array_merge($payload, ['socket' => data_get($event, 'socket')]); + } + + $payload = []; + + foreach ((new ReflectionClass($event))->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { + $payload[$property->getName()] = $this->formatProperty($property->getValue($event)); + } + + unset($payload['broadcastQueue']); + + return $payload; + } + + /** + * Format the given value for a property. + */ + protected function formatProperty(mixed $value): mixed + { + if ($value instanceof Arrayable) { + return $value->toArray(); + } + + return $value; + } + + /** + * Get the display name for the queued job. + */ + public function displayName(): string + { + return get_class($this->event); + } + + /** + * Prepare the instance for cloning. + */ + public function __clone() + { + $this->event = clone $this->event; + } +} diff --git a/src/broadcasting/src/Broadcasters/LogBroadcaster.php b/src/broadcasting/src/Broadcasters/LogBroadcaster.php new file mode 100644 index 00000000..6079a7ac --- /dev/null +++ b/src/broadcasting/src/Broadcasters/LogBroadcaster.php @@ -0,0 +1,52 @@ +logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function auth(RequestInterface $request): mixed + { + return null; + } + + /** + * {@inheritdoc} + */ + public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed + { + return null; + } + + /** + * {@inheritdoc} + */ + public function broadcast(array $channels, string $event, array $payload = []): void + { + $channels = implode(', ', $this->formatChannels($channels)); + + $payload = json_encode($payload, JSON_PRETTY_PRINT); + + $this->logger->info('Broadcasting ['.$event.'] on channels ['.$channels.'] with payload:'.PHP_EOL.$payload); + } +} diff --git a/src/broadcasting/src/Broadcasters/NullBroadcaster.php b/src/broadcasting/src/Broadcasters/NullBroadcaster.php new file mode 100644 index 00000000..e2344378 --- /dev/null +++ b/src/broadcasting/src/Broadcasters/NullBroadcaster.php @@ -0,0 +1,33 @@ +name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name; + } + + /** + * Convert the channel instance to a string. + */ + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/broadcasting/src/Contracts/Factory.php b/src/broadcasting/src/Contracts/Factory.php new file mode 100644 index 00000000..116df256 --- /dev/null +++ b/src/broadcasting/src/Contracts/Factory.php @@ -0,0 +1,15 @@ +broadcastConnection = is_null($connection) + ? [null] + : Arr::wrap($connection); + + return $this; + } + + /** + * Get the broadcaster connections the event should be broadcast on. + */ + public function broadcastConnections(): array + { + return $this->broadcastConnection; + } +} diff --git a/src/broadcasting/src/LICENSE.md b/src/broadcasting/src/LICENSE.md new file mode 100644 index 00000000..193eba9f --- /dev/null +++ b/src/broadcasting/src/LICENSE.md @@ -0,0 +1,25 @@ +The MIT License (MIT) + +Copyright (c) Taylor Otwell + +Copyright (c) Hyperf + +Copyright (c) Laravel Hyperf + +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/src/PresenceChannel.php b/src/broadcasting/src/PresenceChannel.php new file mode 100644 index 00000000..071514bb --- /dev/null +++ b/src/broadcasting/src/PresenceChannel.php @@ -0,0 +1,16 @@ +broadcastChannel() : $name; + + parent::__construct('private-'.$name); + } +} diff --git a/tests/Broadcasting/BroadcastEventTest.php b/tests/Broadcasting/BroadcastEventTest.php new file mode 100644 index 00000000..f1abac92 --- /dev/null +++ b/tests/Broadcasting/BroadcastEventTest.php @@ -0,0 +1,105 @@ +shouldReceive('broadcast')->once()->with( + ['test-channel'], TestBroadcastEvent::class, ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] + ); + + $manager = m::mock(BroadcastingFactory::class); + + $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + + $event = new TestBroadcastEvent; + + (new BroadcastEvent($event))->handle($manager); + } + + public function testManualParameterSpecification() + { + $broadcaster = m::mock(Broadcaster::class); + + $broadcaster->shouldReceive('broadcast')->once()->with( + ['test-channel'], TestBroadcastEventWithManualData::class, ['name' => 'Taylor', 'socket' => null] + ); + + $manager = m::mock(BroadcastingFactory::class); + + $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); + + $event = new TestBroadcastEventWithManualData; + + (new BroadcastEvent($event))->handle($manager); + } + + public function testSpecificBroadcasterGiven() + { + $broadcaster = m::mock(Broadcaster::class); + + $broadcaster->shouldReceive('broadcast')->once(); + + $manager = m::mock(BroadcastingFactory::class); + + $manager->shouldReceive('connection')->once()->with('log')->andReturn($broadcaster); + + $event = new TestBroadcastEventWithSpecificBroadcaster; + + (new BroadcastEvent($event))->handle($manager); + } +} + +class TestBroadcastEvent +{ + public $firstName = 'Taylor'; + public $lastName = 'Otwell'; + public $collection; + private $title = 'Developer'; + + public function __construct() + { + $this->collection = collect(['foo' => 'bar']); + } + + public function broadcastOn() + { + return ['test-channel']; + } +} + +class TestBroadcastEventWithManualData extends TestBroadcastEvent +{ + public function broadcastWith() + { + return ['name' => 'Taylor']; + } +} + +class TestBroadcastEventWithSpecificBroadcaster extends TestBroadcastEvent +{ + use InteractsWithBroadcasting; + + public function __construct() + { + $this->broadcastVia('log'); + } +} From ba83fe334e6329434b6c7fc791e6ce127447b0c0 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Tue, 19 Nov 2024 17:38:46 +0800 Subject: [PATCH 17/30] add contracts --- .../src/Contracts/ShouldBeUnique.php | 10 ++++++++++ .../src/Contracts/ShouldBroadcast.php | 17 +++++++++++++++++ .../src/Contracts/ShouldBroadcastNow.php | 9 +++++++++ 3 files changed, 36 insertions(+) create mode 100644 src/broadcasting/src/Contracts/ShouldBeUnique.php create mode 100644 src/broadcasting/src/Contracts/ShouldBroadcast.php create mode 100644 src/broadcasting/src/Contracts/ShouldBroadcastNow.php diff --git a/src/broadcasting/src/Contracts/ShouldBeUnique.php b/src/broadcasting/src/Contracts/ShouldBeUnique.php new file mode 100644 index 00000000..99628cd2 --- /dev/null +++ b/src/broadcasting/src/Contracts/ShouldBeUnique.php @@ -0,0 +1,10 @@ + Date: Tue, 19 Nov 2024 17:43:19 +0800 Subject: [PATCH 18/30] add InteractsWithSockets, UniqueBroadcastEven --- .../src/Broadcasters/Broadcaster.php | 4 +- src/broadcasting/src/InteractsWithSockets.php | 35 ++++++ src/broadcasting/src/UniqueBroadcastEvent.php | 59 ++++++++++ tests/Broadcasting/BroadcasterTest.php | 4 +- .../UsePusherChannelsNamesTest.php | 109 ++++++++++++++++++ 5 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 src/broadcasting/src/InteractsWithSockets.php create mode 100644 src/broadcasting/src/UniqueBroadcastEvent.php create mode 100644 tests/Broadcasting/UsePusherChannelsNamesTest.php diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index 93ae6bae..f3d1832f 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -190,7 +190,7 @@ protected function resolveBinding(string $key, string $value, array $callbackPar */ protected function resolveExplicitBindingIfPossible(string $key, string $value): mixed { - // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar + // TODO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar // $binder = $this->binder(); // if ($binder && $binder->getBindingCallback($key)) { @@ -243,7 +243,7 @@ protected function formatChannels(array $channels): array }, $channels); } - // DOTO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar + // TODO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar /** * Get the model binding registrar instance. * diff --git a/src/broadcasting/src/InteractsWithSockets.php b/src/broadcasting/src/InteractsWithSockets.php new file mode 100644 index 00000000..94da9666 --- /dev/null +++ b/src/broadcasting/src/InteractsWithSockets.php @@ -0,0 +1,35 @@ +socket = Broadcast::socket(); + + return $this; + } + + /** + * Broadcast the event to everyone. + */ + public function broadcastToEveryone(): static + { + $this->socket = null; + + return $this; + } +} diff --git a/src/broadcasting/src/UniqueBroadcastEvent.php b/src/broadcasting/src/UniqueBroadcastEvent.php new file mode 100644 index 00000000..dd92f909 --- /dev/null +++ b/src/broadcasting/src/UniqueBroadcastEvent.php @@ -0,0 +1,59 @@ +uniqueId = get_class($event); + + if (method_exists($event, 'uniqueId')) { + $this->uniqueId .= $event->uniqueId(); + } elseif (property_exists($event, 'uniqueId')) { + $this->uniqueId .= $event->uniqueId; + } + + if (method_exists($event, 'uniqueFor')) { + $this->uniqueFor = $event->uniqueFor(); + } elseif (property_exists($event, 'uniqueFor')) { + $this->uniqueFor = $event->uniqueFor; + } + + parent::__construct($event); + } + + /** + * Resolve the cache implementation that should manage the event's uniqueness. + */ + public function uniqueVia(): Repository + { + // TODO: Repository 好像沒有註冊在 SwooleTW\Hyperf\Foundation\Application@registerCoreContainerAliases + return method_exists($this->event, 'uniqueVia') + ? $this->event->uniqueVia() + : ApplicationContext::getContainer()->get(Repository::class); + } +} diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 554651b9..30d40f4d 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -83,7 +83,7 @@ public function testExtractingParametersWhileCheckingForUserAccess() /* * Test Explicit Binding... */ - // DOTO: 要等 binder 實作 + // TODO: 要等 binder 實作 // $container = new Container; // Container::setInstance($container); // $binder = m::mock(BindingRegistrar::class); @@ -105,7 +105,7 @@ public function testCanUseChannelClasses() $this->assertEquals(['model.1.instance', 'something'], $parameters); } - // DOTO: 要等 binder 實作 + // TODO: 要等 binder 實作 // public function testModelRouteBinding() // { // $container = new Container; diff --git a/tests/Broadcasting/UsePusherChannelsNamesTest.php b/tests/Broadcasting/UsePusherChannelsNamesTest.php new file mode 100644 index 00000000..70b5bda3 --- /dev/null +++ b/tests/Broadcasting/UsePusherChannelsNamesTest.php @@ -0,0 +1,109 @@ +assertSame( + $normalizedName, + $broadcaster->normalizeChannelName($requestChannelName) + ); + } + + public function testChannelNameNormalizationSpecialCase() + { + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; + + $this->assertSame( + 'private-123', + $broadcaster->normalizeChannelName('private-encrypted-private-123') + ); + } + + #[DataProvider('channelsProvider')] + public function testIsGuardedChannel($requestChannelName, $_, $guarded) + { + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; + + $this->assertSame( + $guarded, + $broadcaster->isGuardedChannel($requestChannelName) + ); + } + + public static function channelsProvider() + { + $prefixesInfos = [ + ['prefix' => 'private-', 'guarded' => true], + ['prefix' => 'private-encrypted-', 'guarded' => true], + ['prefix' => 'presence-', 'guarded' => true], + ['prefix' => '', 'guarded' => false], + ]; + + $channels = [ + 'test', + 'test-channel', + 'test-private-channel', + 'test-presence-channel', + 'abcd.efgh', + 'abcd.efgh.ijkl', + 'test.{param}', + 'test-{param}', + '{a}.{b}', + '{a}-{b}', + '{a}-{b}.{c}', + ]; + + $tests = []; + foreach ($prefixesInfos as $prefixInfos) { + foreach ($channels as $channel) { + $tests[] = [ + $prefixInfos['prefix'].$channel, + $channel, + $prefixInfos['guarded'], + ]; + } + } + + $tests[] = ['private-private-test', 'private-test', true]; + $tests[] = ['private-presence-test', 'presence-test', true]; + $tests[] = ['presence-private-test', 'private-test', true]; + $tests[] = ['presence-presence-test', 'presence-test', true]; + $tests[] = ['public-test', 'public-test', false]; + dd($tests); + + return $tests; + } +} + +class FakeBroadcasterUsingPusherChannelsNames extends Broadcaster +{ + use UsePusherChannelConventions; + + public function auth(RequestInterface $request): mixed + { + return null; + } + + public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed + { + return null; + } + + public function broadcast(array $channels, string $event, array $payload = []): void + { + } +} From eafb31d1f374eaa67d5bc248817ec7a8fc8fd7d6 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Tue, 19 Nov 2024 19:58:41 +0800 Subject: [PATCH 19/30] don't depend on container of SwoolTW, instead depend on Psr7 container --- .../src/Broadcasters/AblyBroadcaster.php | 13 +- .../src/Broadcasters/Broadcaster.php | 18 +-- .../src/Broadcasters/LogBroadcaster.php | 13 +- .../src/Broadcasters/PusherBroadcaster.php | 14 +- .../src/Broadcasters/RedisBroadcaster.php | 24 +-- tests/Broadcasting/AblyBroadcasterTest.php | 43 +++--- tests/Broadcasting/BroadcasterTest.php | 139 ++++++++++-------- tests/Broadcasting/PusherBroadcasterTest.php | 42 +++--- tests/Broadcasting/RedisBroadcasterTest.php | 29 ++-- 9 files changed, 167 insertions(+), 168 deletions(-) diff --git a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php index ce53bc7a..e786b491 100644 --- a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php @@ -9,22 +9,19 @@ use Ably\Models\Message as AblyMessage; use Hyperf\HttpServer\Contract\RequestInterface; use Hyperf\Stringable\Str; +use Psr\Container\ContainerInterface; use SwooleTW\Hyperf\Broadcasting\BroadcastException; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; class AblyBroadcaster extends Broadcaster { - /** - * The AblyRest SDK instance. - */ - protected AblyRest $ably; - /** * Create a new broadcaster instance. */ - public function __construct(AblyRest $ably) - { - $this->ably = $ably; + public function __construct( + protected ContainerInterface $container, + protected AblyRest $ably, + ) { } /** diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index f3d1832f..306f43a2 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -8,17 +8,16 @@ use Exception; use Hyperf\Collection\Arr; use Hyperf\Collection\Collection; -use Hyperf\Context\ApplicationContext; use Hyperf\HttpServer\Contract\RequestInterface; use ReflectionClass; use ReflectionFunction; use ReflectionParameter; +use SwooleTW\Hyperf\Auth\AuthManager; use SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster as BroadcasterContract; use SwooleTW\Hyperf\Broadcasting\Contracts\HasBroadcastChannel; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; use SwooleTW\Hyperf\Router\Contracts\UrlRoutable; use SwooleTW\Hyperf\Router\Router; -use SwooleTW\Hyperf\Support\Facades\Auth; use SwooleTW\Hyperf\Support\Reflector; abstract class Broadcaster implements BroadcasterContract @@ -253,8 +252,8 @@ protected function formatChannels(array $channels): array // protected function binder() // { // if (! $this->bindingRegistrar) { - // $this->bindingRegistrar = ApplicationContext::getContainer()->has(BindingRegistrar::class) - // ? ApplicationContext::getContainer()->get(BindingRegistrar::class) + // $this->bindingRegistrar = $this->container->has(BindingRegistrar::class) + // ? $this->container->get(BindingRegistrar::class) // : null; // } // @@ -271,9 +270,7 @@ protected function formatChannels(array $channels): array protected function normalizeChannelHandlerToCallable($callback) { return is_callable($callback) ? $callback : function (...$args) use ($callback) { - return ApplicationContext::getContainer() - ->get($callback) - ->join(...$args); + return $this->container->get($callback)->join(...$args); }; } @@ -283,15 +280,16 @@ protected function normalizeChannelHandlerToCallable($callback) protected function retrieveUser(string $channel): mixed { $options = $this->retrieveChannelOptions($channel); - $guards = $options['guards'] ?? null; + $auth = $this->container->get(AuthManager::class); + if (is_null($guards)) { - return Auth::user(); + return $auth->user(); } foreach (Arr::wrap($guards) as $guard) { - $user = Auth::guard($guard)->user(); + $user = $auth->guard($guard)->user(); if ($user) { return $user; } diff --git a/src/broadcasting/src/Broadcasters/LogBroadcaster.php b/src/broadcasting/src/Broadcasters/LogBroadcaster.php index 6079a7ac..04d4b90d 100644 --- a/src/broadcasting/src/Broadcasters/LogBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/LogBroadcaster.php @@ -5,21 +5,18 @@ namespace SwooleTW\Hyperf\Broadcasting\Broadcasters; use Hyperf\HttpServer\Contract\RequestInterface; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; class LogBroadcaster extends Broadcaster { - /** - * The logger implementation. - */ - protected LoggerInterface $logger; - /** * Create a new broadcaster instance. */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + protected ContainerInterface $container, + protected LoggerInterface $logger + ) { } /** diff --git a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php index a8823d44..bc33cc76 100644 --- a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php @@ -7,6 +7,7 @@ use Hyperf\Collection\Arr; use Hyperf\Collection\Collection; use Hyperf\HttpServer\Contract\RequestInterface; +use Psr\Container\ContainerInterface; use Pusher\ApiErrorException; use Pusher\Pusher; use SwooleTW\Hyperf\Broadcasting\BroadcastException; @@ -15,18 +16,13 @@ class PusherBroadcaster extends Broadcaster { use UsePusherChannelConventions; - - /** - * The Pusher SDK instance. - */ - protected Pusher $pusher; - /** * Create a new broadcaster instance. */ - public function __construct(Pusher $pusher) - { - $this->pusher = $pusher; + public function __construct( + protected ContainerInterface $container, + protected Pusher $pusher + ) { } /** diff --git a/src/broadcasting/src/Broadcasters/RedisBroadcaster.php b/src/broadcasting/src/Broadcasters/RedisBroadcaster.php index 561cc1f7..9e2731e2 100644 --- a/src/broadcasting/src/Broadcasters/RedisBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/RedisBroadcaster.php @@ -8,6 +8,7 @@ use Hyperf\HttpServer\Contract\RequestInterface; use Hyperf\Pool\Exception\ConnectionException; use Hyperf\Redis\RedisFactory; +use Psr\Container\ContainerInterface; use RedisException; use SwooleTW\Hyperf\Broadcasting\BroadcastException; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; @@ -16,26 +17,15 @@ class RedisBroadcaster extends Broadcaster { use UsePusherChannelConventions; - protected RedisFactory $factory; - - /** - * The Redis connection to use for broadcasting. - */ - protected ?string $connection = null; - - /** - * The Redis key prefix. - */ - protected string $prefix = ''; - /** * Create a new broadcaster instance. */ - public function __construct(RedisFactory $factory, ?string $connection = null, string $prefix = '') - { - $this->factory = $factory; - $this->prefix = $prefix; - $this->connection = $connection; + public function __construct( + protected ContainerInterface $container, + protected RedisFactory $factory, + protected ?string $connection = null, + protected string $prefix = '' + ) { } /** diff --git a/tests/Broadcasting/AblyBroadcasterTest.php b/tests/Broadcasting/AblyBroadcasterTest.php index 8343c20e..9b1bc52d 100644 --- a/tests/Broadcasting/AblyBroadcasterTest.php +++ b/tests/Broadcasting/AblyBroadcasterTest.php @@ -8,14 +8,10 @@ use Hyperf\HttpServer\Request; use Mockery as m; use PHPUnit\Framework\TestCase; -use stdClass; -use SwooleTW\Hyperf\Auth\Contracts\FactoryContract; +use Psr\Container\ContainerInterface; +use SwooleTW\Hyperf\Auth\AuthManager; use SwooleTW\Hyperf\Broadcasting\Broadcasters\AblyBroadcaster; -use SwooleTW\Hyperf\Foundation\ApplicationContext; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; -use SwooleTW\Hyperf\Support\Facades\Auth; -use SwooleTW\Hyperf\Support\Facades\Facade; -use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; /** * @internal @@ -23,24 +19,17 @@ */ class AblyBroadcasterTest extends TestCase { - use HasMockedApplication; - - public AblyBroadcaster $broadcaster; - - public AblyRest $ably; + protected AblyBroadcaster $broadcaster; + protected AblyRest $ably; + protected ContainerInterface $container; protected function setUp(): void { parent::setUp(); - $this->ably = m::mock(AblyRest::class, ['abcd:efgh']); - - $this->broadcaster = m::mock(AblyBroadcaster::class, [$this->ably])->makePartial(); - - $container = $this->getApplication([ - FactoryContract::class => fn () => new stdClass(), - ]); - ApplicationContext::setContainer($container); + $this->container = m::mock(ContainerInterface::class); + $this->ably = m::mock(AblyRest::class, ['abcd:efg']); + $this->broadcaster = m::mock(AblyBroadcaster::class, [$this->container, $this->ably])->makePartial(); } protected function tearDown(): void @@ -48,8 +37,6 @@ protected function tearDown(): void parent::tearDown(); m::close(); - - Facade::clearResolvedInstances(); } public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue() @@ -138,7 +125,12 @@ protected function getMockRequestWithUserForChannel(string $channel): Request $user = m::mock('User'); $user->shouldReceive('getAuthIdentifier')->andReturn(42); - Auth::shouldReceive('user')->andReturn($user); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('user')->andReturn($user); + + $this->container->shouldReceive('get') + ->with(AuthManager::class) + ->andReturn($authManager); return $request; } @@ -148,7 +140,12 @@ protected function getMockRequestWithoutUserForChannel(string $channel): Request $request = m::mock(Request::class); $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - Auth::shouldReceive('user')->andReturn(null); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('user')->andReturn(null); + + $this->container->shouldReceive('get') + ->with(AuthManager::class) + ->andReturn($authManager); return $request; } diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 30d40f4d..ffae49fe 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -5,7 +5,6 @@ namespace SwooleTW\Hyperf\Tests\Broadcasting; use Exception; -use Hyperf\Context\ApplicationContext; use Hyperf\Context\RequestContext; use Hyperf\Database\Model\Booted; use Hyperf\HttpMessage\Server\Request as ServerRequest; @@ -14,14 +13,13 @@ use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use stdClass; -use SwooleTW\Hyperf\Auth\Contracts\FactoryContract; +use Psr\Container\ContainerInterface; +use SwooleTW\Hyperf\Auth\AuthManager; +use SwooleTW\Hyperf\Auth\Contracts\Authenticatable; +use SwooleTW\Hyperf\Auth\Contracts\Guard; use SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster; use SwooleTW\Hyperf\Database\Eloquent\Model; use SwooleTW\Hyperf\HttpMessage\Exceptions\HttpException; -use SwooleTW\Hyperf\Support\Facades\Auth; -use SwooleTW\Hyperf\Support\Facades\Facade; -use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; /** * @internal @@ -29,22 +27,16 @@ */ class BroadcasterTest extends TestCase { - use HasMockedApplication; - - protected $container; - - public FakeBroadcaster $broadcaster; + protected ContainerInterface $container; + protected FakeBroadcaster $broadcaster; protected function setUp(): void { parent::setUp(); - $this->broadcaster = new FakeBroadcaster(); + $this->container = m::mock(ContainerInterface::class); - $container = $this->getApplication([ - FactoryContract::class => fn () => new stdClass(), - ]); - ApplicationContext::setContainer($container); + $this->broadcaster = new FakeBroadcaster($this->container); } protected function tearDown(): void @@ -52,8 +44,6 @@ protected function tearDown(): void parent::tearDown(); m::close(); - - Facade::clearResolvedInstances(); } public function testExtractingParametersWhileCheckingForUserAccess() @@ -217,11 +207,17 @@ public function testRetrieveUserWithoutGuard() $this->broadcaster->channel('somechannel', function () { }); - Auth::shouldReceive('user') + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('user') ->once() ->withNoArgs() ->andReturn(new DummyUser()); + $this->container->shouldReceive('get') + ->once() + ->with(AuthManager::class) + ->andReturn($authManager); + $this->assertInstanceOf( DummyUser::class, $this->broadcaster->retrieveUser('somechannel') @@ -233,14 +229,21 @@ public function testRetrieveUserWithOneGuardUsingAStringForSpecifyingGuard() $this->broadcaster->channel('somechannel', function () { }, ['guards' => 'myguard']); - Auth::shouldReceive('guard') - ->once() - ->with('myguard') - ->andReturnSelf(); - Auth::shouldReceive('user') + $guard = m::mock(Guard::class); + $guard->shouldReceive('user') ->once() ->withNoArgs() ->andReturn(new DummyUser()); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('guard') + ->once() + ->with('myguard') + ->andReturn($guard); + + $this->container->shouldReceive('get') + ->once() + ->with(AuthManager::class) + ->andReturn($authManager); $this->assertInstanceOf( DummyUser::class, @@ -255,18 +258,30 @@ public function testRetrieveUserWithMultipleGuardsAndRespectGuardsOrder() $this->broadcaster->channel('someotherchannel', function () { }, ['guards' => ['myguard2', 'myguard1']]); - Auth::shouldReceive('guard') + $guard1 = m::mock(Guard::class); + $guard1->shouldReceive('user') + ->once() + ->andReturn(null); + $guard2 = m::mock(Guard::class); + $guard2->shouldReceive('user') + ->twice() + ->andReturn(new DummyUser()); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('guard') ->once() ->with('myguard1') - ->andReturnSelf(); - Auth::shouldReceive('guard') + ->andReturn($guard1); + $authManager->shouldReceive('guard') ->twice() ->with('myguard2') - ->andReturnSelf(); - Auth::shouldReceive('user') - ->times(3) - ->withNoArgs() - ->andReturn(null, new DummyUser(), new DummyUser()); + ->andReturn($guard2); + $authManager->shouldNotReceive('guard') + ->withNoArgs(); + + $this->container->shouldReceive('get') + ->twice() + ->with(AuthManager::class) + ->andReturn($authManager); $this->assertInstanceOf( DummyUser::class, @@ -284,39 +299,22 @@ public function testRetrieveUserDontUseDefaultGuardWhenOneGuardSpecified() $this->broadcaster->channel('somechannel', function () { }, ['guards' => 'myguard']); - Auth::shouldReceive('guard') + $guard = m::mock(Guard::class); + $guard->shouldReceive('user') ->once() - ->with('myguard') - ->andReturnSelf(); - Auth::shouldReceive('user') + ->andReturn(new DummyUser()); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('guard') ->once() - ->withNoArgs() - ->andReturn(null); - Auth::shouldNotReceive('guard') + ->with('myguard') + ->andReturn($guard); + $authManager->shouldNotReceive('guard') ->withNoArgs(); - $this->broadcaster->retrieveUser('somechannel'); - } - - public function testRetrieveUserDontUseDefaultGuardWhenMultipleGuardsSpecified() - { - $this->broadcaster->channel('somechannel', function () { - }, ['guards' => ['myguard1', 'myguard2']]); - - Auth::shouldReceive('guard') - ->once() - ->with('myguard1') - ->andReturnSelf(); - Auth::shouldReceive('guard') + $this->container->shouldReceive('get') ->once() - ->with('myguard2') - ->andReturnSelf(); - Auth::shouldReceive('user') - ->twice() - ->withNoArgs() - ->andReturn(null); - Auth::shouldNotReceive('guard') - ->withNoArgs(); + ->with(AuthManager::class) + ->andReturn($authManager); $this->broadcaster->retrieveUser('somechannel'); } @@ -391,6 +389,11 @@ public static function channelNameMatchPatternProvider() class FakeBroadcaster extends Broadcaster { + public function __construct( + protected ContainerInterface $container + ) { + } + public function auth(RequestInterface $request): mixed { return null; @@ -472,6 +475,20 @@ public function join($user, BroadcasterTestEloquentModelStub $model, $nonModel) } } -class DummyUser +class DummyUser implements Authenticatable { + public function getAuthIdentifierName(): string + { + return 'dummy_user'; + } + + public function getAuthIdentifier(): mixed + { + return 'dummy_user'; + } + + public function getAuthPassword(): string + { + return 'dummy_password'; + } } diff --git a/tests/Broadcasting/PusherBroadcasterTest.php b/tests/Broadcasting/PusherBroadcasterTest.php index 434a1d83..2540d7bd 100644 --- a/tests/Broadcasting/PusherBroadcasterTest.php +++ b/tests/Broadcasting/PusherBroadcasterTest.php @@ -7,15 +7,11 @@ use Hyperf\HttpServer\Request; use Mockery as m; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Pusher\Pusher; -use stdClass; -use SwooleTW\Hyperf\Auth\Contracts\FactoryContract; +use SwooleTW\Hyperf\Auth\AuthManager; use SwooleTW\Hyperf\Broadcasting\Broadcasters\PusherBroadcaster; -use SwooleTW\Hyperf\Foundation\ApplicationContext; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; -use SwooleTW\Hyperf\Support\Facades\Auth; -use SwooleTW\Hyperf\Support\Facades\Facade; -use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; /** * @internal @@ -23,23 +19,17 @@ */ class PusherBroadcasterTest extends TestCase { - use HasMockedApplication; - - public PusherBroadcaster $broadcaster; - - public Pusher $pusher; + protected ContainerInterface $container; + protected PusherBroadcaster $broadcaster; + protected Pusher $pusher; protected function setUp(): void { parent::setUp(); + $this->container = m::mock(ContainerInterface::class); $this->pusher = m::mock(Pusher::class); - $this->broadcaster = m::mock(PusherBroadcaster::class, [$this->pusher])->makePartial(); - - $container = $this->getApplication([ - FactoryContract::class => fn () => new stdClass(), - ]); - ApplicationContext::setContainer($container); + $this->broadcaster = m::mock(PusherBroadcaster::class, [$this->container, $this->pusher])->makePartial(); } protected function tearDown(): void @@ -47,8 +37,6 @@ protected function tearDown(): void parent::tearDown(); m::close(); - - Facade::clearResolvedInstances(); } public function testAuthCallValidAuthenticationResponseWithPrivateChannelWhenCallbackReturnTrue() @@ -180,8 +168,6 @@ public function testUserAuthenticationForPusher() ->shouldReceive('authenticateUser') ->andReturn(json_encode($authenticateUser)); - $this->broadcaster = new PusherBroadcaster($this->pusher); - $this->broadcaster->resolveAuthenticatedUserUsing(function () { return ['id' => '12345']; }); @@ -202,7 +188,12 @@ protected function getMockRequestWithUserForChannel(string $channel): Request $user = m::mock('User'); $user->shouldReceive('getAuthIdentifier')->andReturn(42); - Auth::shouldReceive('user')->andReturn($user); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('user')->andReturn($user); + + $this->container->shouldReceive('get') + ->with(AuthManager::class) + ->andReturn($authManager); return $request; } @@ -212,7 +203,12 @@ protected function getMockRequestWithoutUserForChannel(string $channel): Request $request = m::mock(Request::class); $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - Auth::shouldReceive('user')->andReturn(null); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('user')->andReturn(null); + + $this->container->shouldReceive('get') + ->with(AuthManager::class) + ->andReturn($authManager); return $request; } diff --git a/tests/Broadcasting/RedisBroadcasterTest.php b/tests/Broadcasting/RedisBroadcasterTest.php index 23d7567e..a9d5e616 100644 --- a/tests/Broadcasting/RedisBroadcasterTest.php +++ b/tests/Broadcasting/RedisBroadcasterTest.php @@ -5,9 +5,12 @@ namespace SwooleTW\Hyperf\Tests\Broadcasting; use Hyperf\HttpServer\Request; +use Hyperf\Redis\RedisFactory; use Mockery as m; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use stdClass; +use SwooleTW\Hyperf\Auth\AuthManager; use SwooleTW\Hyperf\Auth\Contracts\FactoryContract; use SwooleTW\Hyperf\Broadcasting\Broadcasters\RedisBroadcaster; use SwooleTW\Hyperf\Foundation\ApplicationContext; @@ -24,18 +27,16 @@ class RedisBroadcasterTest extends TestCase { use HasMockedApplication; - public RedisBroadcaster $broadcaster; + protected RedisBroadcaster $broadcaster; + protected ContainerInterface $container; protected function setUp(): void { parent::setUp(); - $this->broadcaster = m::mock(RedisBroadcaster::class)->makePartial(); - - $container = $this->getApplication([ - FactoryContract::class => fn () => new stdClass(), - ]); - ApplicationContext::setContainer($container); + $this->container = m::mock(ContainerInterface::class); + $factory = m::mock(RedisFactory::class); + $this->broadcaster = m::mock(RedisBroadcaster::class, [$this->container, $factory])->makePartial(); } protected function tearDown(): void @@ -165,7 +166,12 @@ protected function getMockRequestWithUserForChannel(string $channel): Request $user = m::mock('User'); $user->shouldReceive('getAuthIdentifier')->andReturn(42); - Auth::shouldReceive('user')->andReturn($user); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('user')->andReturn($user); + + $this->container->shouldReceive('get') + ->with(AuthManager::class) + ->andReturn($authManager); return $request; } @@ -175,7 +181,12 @@ protected function getMockRequestWithoutUserForChannel(string $channel): Request $request = m::mock(Request::class); $request->shouldReceive('input')->with('channel_name')->andReturn($channel); - Auth::shouldReceive('user')->andReturn(null); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('user')->andReturn(null); + + $this->container->shouldReceive('get') + ->with(AuthManager::class) + ->andReturn($authManager); return $request; } From 370df40be5cad0985a294dcefa17203d09f882ea Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Tue, 19 Nov 2024 20:00:42 +0800 Subject: [PATCH 20/30] [WIP] --- src/broadcasting/src/AnonymousEvent.php | 151 +++++++++++++++++++++ src/broadcasting/src/PendingBroadcast.php | 61 +++++++++ src/foundation/src/Events/Dispatchable.php | 10 ++ src/foundation/src/helpers.php | 12 ++ src/support/src/Facades/Broadcast.php | 51 +++++++ 5 files changed, 285 insertions(+) create mode 100644 src/broadcasting/src/AnonymousEvent.php create mode 100644 src/broadcasting/src/PendingBroadcast.php create mode 100644 src/support/src/Facades/Broadcast.php diff --git a/src/broadcasting/src/AnonymousEvent.php b/src/broadcasting/src/AnonymousEvent.php new file mode 100644 index 00000000..662a2834 --- /dev/null +++ b/src/broadcasting/src/AnonymousEvent.php @@ -0,0 +1,151 @@ +channels = Arr::wrap($channels); + } + + /** + * Set the connection the event should be broadcast on. + */ + public function via(string $connection): static + { + $this->connection = $connection; + + return $this; + } + + /** + * Set the name the event should be broadcast as. + */ + public function as(string $name): static + { + $this->name = $name; + + return $this; + } + + /** + * Set the payload the event should be broadcast with. + */ + public function with(Arrayable|array $payload): static + { + $this->payload = $payload instanceof Arrayable + ? $payload->toArray() + : collect($payload)->map( + fn ($p) => $p instanceof Arrayable ? $p->toArray() : $p + )->all(); + + return $this; + } + + /** + * Broadcast the event to everyone except the current user. + */ + public function toOthers(): static + { + $this->includeCurrentUser = false; + + return $this; + } + + /** + * Broadcast the event. + */ + public function sendNow(): void + { + $this->shouldBroadcastNow = true; + + $this->send(); + } + + /** + * Broadcast the event. + */ + public function send(): void + { + $broadcast = broadcast($this)->via($this->connection); + + if (! $this->includeCurrentUser) { + $broadcast->toOthers(); + } + } + + /** + * Get the name the event should broadcast as. + */ + public function broadcastAs(): string + { + return $this->name ?: class_basename($this); + } + + /** + * Get the payload the event should broadcast with. + * + * @return array + */ + public function broadcastWith(): array + { + return $this->payload; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|\Illuminate\Broadcasting\Channel[]|string[]|string + */ + public function broadcastOn(): Channel|array + { + return $this->channels; + } + + /** + * Determine if the event should be broadcast synchronously. + */ + public function shouldBroadcastNow(): bool + { + return $this->shouldBroadcastNow; + } +} diff --git a/src/broadcasting/src/PendingBroadcast.php b/src/broadcasting/src/PendingBroadcast.php new file mode 100644 index 00000000..2458a0c6 --- /dev/null +++ b/src/broadcasting/src/PendingBroadcast.php @@ -0,0 +1,61 @@ +event = $event; + $this->events = $events; + } + + /** + * Broadcast the event using a specific broadcaster. + */ + public function via(?string $connection = null): static + { + if (method_exists($this->event, 'broadcastVia')) { + $this->event->broadcastVia($connection); + } + + return $this; + } + + /** + * Broadcast the event to everyone except the current user. + */ + public function toOthers(): static + { + if (method_exists($this->event, 'dontBroadcastToCurrentUser')) { + $this->event->dontBroadcastToCurrentUser(); + } + + return $this; + } + + /** + * Handle the object's destruction. + */ + public function __destruct() + { + $this->events->dispatch($this->event); + } +} diff --git a/src/foundation/src/Events/Dispatchable.php b/src/foundation/src/Events/Dispatchable.php index a2c4fe6e..dc22072f 100644 --- a/src/foundation/src/Events/Dispatchable.php +++ b/src/foundation/src/Events/Dispatchable.php @@ -4,6 +4,8 @@ namespace SwooleTW\Hyperf\Foundation\Events; +use SwooleTW\Hyperf\Broadcasting\PendingBroadcast; + trait Dispatchable { /** @@ -33,4 +35,12 @@ public static function dispatchUnless(bool $boolean, mixed ...$arguments): mixed return event(new static(...$arguments)); } } + + /** + * Broadcast the event with the given arguments. + */ + public static function broadcast(): PendingBroadcast + { + return broadcast(new static(...func_get_args())); + } } diff --git a/src/foundation/src/helpers.php b/src/foundation/src/helpers.php index 9f9c4832..b9eff681 100644 --- a/src/foundation/src/helpers.php +++ b/src/foundation/src/helpers.php @@ -22,6 +22,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Log\LoggerInterface; use SwooleTW\Hyperf\Auth\Contracts\Gate; +use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastFactory; +use SwooleTW\Hyperf\Broadcasting\PendingBroadcast; use SwooleTW\Hyperf\Cookie\Contracts\Cookie as CookieContract; use SwooleTW\Hyperf\Foundation\Exceptions\Contracts\ExceptionHandler as ExceptionHandlerContract; use SwooleTW\Hyperf\Http\Contracts\RequestContract; @@ -112,6 +114,16 @@ function app_path(string $path = ''): string } } +if (! function_exists('broadcast')) { + /** + * Begin broadcasting an event. + */ + function broadcast(mixed $event = null): PendingBroadcast + { + return app(BroadcastFactory::class)->event($event); + } +} + if (! function_exists('database_path')) { /** * Get the path to the database folder. diff --git a/src/support/src/Facades/Broadcast.php b/src/support/src/Facades/Broadcast.php new file mode 100644 index 00000000..92e38752 --- /dev/null +++ b/src/support/src/Facades/Broadcast.php @@ -0,0 +1,51 @@ + Date: Wed, 20 Nov 2024 18:23:05 +0800 Subject: [PATCH 21/30] [WIP] --- phpstan.neon.dist | 2 - src/broadcasting/LICENSE.md | 4 +- src/broadcasting/src/AnonymousEvent.php | 4 +- src/broadcasting/src/BroadcastController.php | 32 ++ src/broadcasting/src/BroadcastManager.php | 451 ++++++++++++++++++ src/broadcasting/src/BroadcastPoolProxy.php | 33 ++ .../src/Broadcasters/AblyBroadcaster.php | 1 + .../src/Broadcasters/Broadcaster.php | 6 + src/broadcasting/src/ConfigProvider.php | 20 + src/broadcasting/src/LICENSE.md | 25 - src/broadcasting/src/UniqueBroadcastEvent.php | 14 +- src/support/src/Facades/Broadcast.php | 36 +- 12 files changed, 577 insertions(+), 51 deletions(-) create mode 100644 src/broadcasting/src/BroadcastController.php create mode 100644 src/broadcasting/src/BroadcastManager.php create mode 100644 src/broadcasting/src/BroadcastPoolProxy.php create mode 100644 src/broadcasting/src/ConfigProvider.php delete mode 100644 src/broadcasting/src/LICENSE.md diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fea14a3d..1f19e819 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -35,5 +35,3 @@ parameters: - '#Call to an undefined method Psr\\Container\\ContainerInterface::make\(\)#' - message: '#Call to an undefined method SwooleTW\\Hyperf\\Foundation\\Testing\\TestCase::#' path: src/foundation/src/Testing/TestCase.php - - message: '#Method Ably\\Channel::publish\(\) invoked with 1 parameter, 2 required.#' - path: src/broadcasting/src/Broadcasters/AblyBroadcaster.php diff --git a/src/broadcasting/LICENSE.md b/src/broadcasting/LICENSE.md index a8f5fd6a..68781284 100644 --- a/src/broadcasting/LICENSE.md +++ b/src/broadcasting/LICENSE.md @@ -2,6 +2,8 @@ The MIT License (MIT) Copyright (c) Taylor Otwell +Copyright (c) Hyperf + Copyright (c) Laravel Hyperf Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,4 +22,4 @@ 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 +THE SOFTWARE. diff --git a/src/broadcasting/src/AnonymousEvent.php b/src/broadcasting/src/AnonymousEvent.php index 662a2834..0cfeb142 100644 --- a/src/broadcasting/src/AnonymousEvent.php +++ b/src/broadcasting/src/AnonymousEvent.php @@ -4,6 +4,8 @@ namespace SwooleTW\Hyperf\Broadcasting; +use Hyperf\Collection\Arr; +use Hyperf\Contract\Arrayable; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcast; use SwooleTW\Hyperf\Broadcasting\InteractsWithBroadcasting; use SwooleTW\Hyperf\Foundation\Events\Dispatchable; @@ -39,8 +41,6 @@ class AnonymousEvent implements ShouldBroadcast /** * Create a new anonymous broadcastable event instance. - * - * @return void */ public function __construct(protected Channel|array|string $channels) { diff --git a/src/broadcasting/src/BroadcastController.php b/src/broadcasting/src/BroadcastController.php new file mode 100644 index 00000000..be6e21d8 --- /dev/null +++ b/src/broadcasting/src/BroadcastController.php @@ -0,0 +1,32 @@ + ['web']]; + + $this->app->get(DispatcherFactory::class)->getRouter() + ->group($attributes, function ($router) { + $router->get('/broadcasting/auth', '\\'.BroadcastController::class.'@authenticate'); + $router->post('/broadcasting/auth', '\\'.BroadcastController::class.'@authenticate'); + }); + } + + /** + * Register the routes for handling broadcast user authentication. + */ + public function userRoutes(?array $attributes = null): void + { + $attributes = $attributes ?: ['middleware' => ['web']]; + + $this->app->get(DispatcherFactory::class)->getRouter() + ->group($attributes, function ($router) { + $router->get('/broadcasting/user-auth', '\\'.BroadcastController::class.'@authenticateUser'); + $router->post('/broadcasting/user-auth', '\\'.BroadcastController::class.'@authenticateUser'); + }); + } + + /** + * Register the routes for handling broadcast authentication and sockets. + * + * Alias of "routes" method. + */ + public function channelRoutes(?array $attributes = null): void + { + $this->routes($attributes); + } + + /** + * Get the socket ID for the given request. + */ + public function socket(?RequestInterface $request = null): ?string + { + $request ??= $this->app->get(RequestInterface::class); + + return $request?->header('X-Socket-ID'); + } + + /** + * Begin sending an anonymous broadcast to the given channels. + */ + public function on(Channel|string|array $channels): AnonymousEvent + { + return new AnonymousEvent($channels); + } + + /** + * Begin sending an anonymous broadcast to the given private channels. + */ + public function private(string $channel): AnonymousEvent + { + return $this->on(new PrivateChannel($channel)); + } + + /** + * Begin sending an anonymous broadcast to the given presence channels. + */ + public function presence(string $channel): AnonymousEvent + { + return $this->on(new PresenceChannel($channel)); + } + + /** + * Begin broadcasting an event. + */ + public function event(mixed $event = null): PendingBroadcast + { + return new PendingBroadcast( + $this->app->get(EventDispatcherInterface::class), + $event, + ); + } + + /** + * Queue the given event for broadcast. + */ + public function queue(mixed $event): void + { + // TODO: wait bus package + // if ($event instanceof ShouldBroadcastNow + // || (is_object($event) && method_exists($event, 'shouldBroadcastNow') && $event->shouldBroadcastNow()) + // ) { + // return $this->app->get(BusDispatcherContract::class)->dispatchNow(new BroadcastEvent(clone $event)); + // } + + $queue = match (true) { + method_exists($event, 'broadcastQueue') => $event->broadcastQueue(), + isset($event->broadcastQueue) => $event->broadcastQueue, + isset($event->queue) => $event->queue, + default => null, + }; + + $broadcastEvent = new BroadcastEvent(clone $event); + + if ($event instanceof ShouldBeUnique) { + $broadcastEvent = new UniqueBroadcastEvent($this->app, clone $event); + + if ($this->mustBeUniqueAndCannotAcquireLock($broadcastEvent)) { + return; + } + } + + // TODO: wait queue package + // $this->app->get('queue') + // ->connection($event->connection ?? null) + // ->pushOn($queue, $broadcastEvent); + } + + /** + * Determine if the broadcastable event must be unique and determine if we can acquire the necessary lock. + */ + protected function mustBeUniqueAndCannotAcquireLock(UniqueBroadcastEvent $event): bool + { + return false; + // TODO: wait bus package + // return ! (new UniqueLock($event->uniqueVia()))->acquire($event); + } + + /** + * Get a driver instance. + */ + public function connection(?string $driver = null): Broadcaster + { + return $this->driver($driver); + } + + /** + * Get a driver instance. + */ + public function driver(?string $name = null): Broadcaster + { + $name = $name ?: $this->getDefaultDriver(); + + return $this->drivers[$name] = $this->get($name); + } + + /** + * Attempt to get the connection from the local cache. + */ + protected function get(string $name): Broadcaster + { + return $this->drivers[$name] ?? $this->resolve($name); + } + + /** + * Resolve the given broadcaster with Pool Proxy if need. + * + * @throws InvalidArgumentException + */ + protected function resolve(string $name): Broadcaster + { + $config = $this->getConfig($name); + + if (is_null($config)) { + throw new InvalidArgumentException("Broadcast connection [{$name}] is not defined."); + } + + return in_array($config['driver'], $this->poolables) + ? $this->createPoolProxy( + $name, + fn () => $this->doResolve($config), + $config['pool'] ?? [] + ) + : $this->doResolve($config); + + } + + /** + * Resolve the given broadcaster + * + * @throws InvalidArgumentException + */ + protected function doResolve(array $config): Broadcaster + { + if (isset($this->customCreators[$config['driver']])) { + return $this->callCustomCreator($config); + } + + $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; + + if (! method_exists($this, $driverMethod)) { + throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported."); + } + + return $this->{$driverMethod}($config); + } + + /** + * Call a custom driver creator. + */ + protected function callCustomCreator(array $config): Broadcaster + { + return $this->customCreators[$config['driver']]($this->app, $config); + } + + /** + * Create an instance of the driver. + */ + protected function createReverbDriver(array $config): Broadcaster + { + return $this->createPusherDriver($config); + } + + /** + * Create an instance of the driver. + */ + protected function createPusherDriver(array $config): Broadcaster + { + return new PusherBroadcaster($this->app, $this->pusher($config)); + } + + /** + * Get a Pusher instance for the given configuration. + */ + public function pusher(array $config): Pusher + { + $guzzleClient = new GuzzleClient( + array_merge( + [ + 'connect_timeout' => 10, + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + 'timeout' => 30, + ], + $config['client_options'] ?? [], + ), + ); + + $pusher = new Pusher( + $config['key'], + $config['secret'], + $config['app_id'], + $config['options'] ?? [], + $guzzleClient, + ); + + if ($config['log'] ?? false) { + $pusher->setLogger($this->app->get(LoggerInterface::class)); + } + + return $pusher; + } + + /** + * Create an instance of the driver. + */ + protected function createAblyDriver(array $config): Broadcaster + { + return new AblyBroadcaster($this->app, $this->ably($config)); + } + + /** + * Get an Ably instance for the given configuration. + */ + public function ably(array $config): AblyRest + { + return new AblyRest($config); + } + + /** + * Create an instance of the driver. + */ + protected function createRedisDriver(array $config): Broadcaster + { + return new RedisBroadcaster( + $this->app, + $this->app->get(RedisFactory::class), + $config['connection'] ?? null, + $this->app->get(ConfigInterface::class)->set('database.redis.options.prefix', ''), + ); + } + + /** + * Create an instance of the driver. + */ + protected function createLogDriver(array $config): Broadcaster + { + return new LogBroadcaster( + $this->app, + $this->app->get(LoggerInterface::class) + ); + } + + /** + * Create an instance of the driver. + */ + protected function createNullDriver(array $config): Broadcaster + { + return new NullBroadcaster; + } + + /** + * Get the connection configuration. + * + * @param string $name + * @return array + */ + protected function getConfig(string $name): array + { + if (! is_null($name) && $name !== 'null') { + return $this->app->get(ConfigInterface::class)->get("broadcasting.connections.{$name}"); + } + + return ['driver' => 'null']; + } + + /** + * Get the default driver name. + */ + public function getDefaultDriver(): string + { + return $this->app->get(ConfigInterface::class)->get('broadcasting.default'); + } + + /** + * Set the default driver name. + */ + public function setDefaultDriver(string $name): void + { + $this->app->get(ConfigInterface::class)->set('broadcasting.default', $name); + } + + /** + * Disconnect the given disk and remove from local cache. + */ + public function purge(?string $name = null): void + { + $name ??= $this->getDefaultDriver(); + + unset($this->drivers[$name]); + } + + /** + * Register a custom driver creator Closure. + */ + public function extend(string $driver, Closure $callback): static + { + $this->customCreators[$driver] = $callback; + + return $this; + } + + /** + * Get the application instance used by the manager. + */ + public function getApplication(): ContainerInterface + { + return $this->app; + } + + /** + * Set the application instance used by the manager. + */ + public function setApplication(ContainerInterface $app): static + { + $this->app = $app; + + return $this; + } + + /** + * Forget all of the resolved driver instances. + */ + public function forgetDrivers(): static + { + $this->drivers = []; + + return $this; + } + + /** + * Dynamically call the default driver instance. + */ + public function __call(string $method, array $parameters): mixed + { + return $this->driver()->$method(...$parameters); + } +} diff --git a/src/broadcasting/src/BroadcastPoolProxy.php b/src/broadcasting/src/BroadcastPoolProxy.php new file mode 100644 index 00000000..e21b7db3 --- /dev/null +++ b/src/broadcasting/src/BroadcastPoolProxy.php @@ -0,0 +1,33 @@ +__call(__FUNCTION__, func_get_args()); + } + + /** + * Return the valid authentication response. + */ + public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed + { + return $this->__call(__FUNCTION__, func_get_args()); + } + + /** + * Broadcast the given event. + */ + public function broadcast(array $channels, string $event, array $payload = []): void + { + $this->__call(__FUNCTION__, func_get_args()); + } +} diff --git a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php index e786b491..db0ab0bf 100644 --- a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php @@ -104,6 +104,7 @@ public function broadcast(array $channels, string $event, array $payload = []): { try { foreach ($this->formatChannels($channels) as $channel) { + // @phpstan-ignore-line $this->ably->channels->get($channel)->publish( $this->buildAblyMessage($event, $payload) ); diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index 306f43a2..a566b447 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -9,6 +9,7 @@ use Hyperf\Collection\Arr; use Hyperf\Collection\Collection; use Hyperf\HttpServer\Contract\RequestInterface; +use Psr\Container\ContainerInterface; use ReflectionClass; use ReflectionFunction; use ReflectionParameter; @@ -22,6 +23,11 @@ abstract class Broadcaster implements BroadcasterContract { + /** + * The container instance. + */ + protected ContainerInterface $container; + /** * The callback to resolve the authenticated user information. */ diff --git a/src/broadcasting/src/ConfigProvider.php b/src/broadcasting/src/ConfigProvider.php new file mode 100644 index 00000000..ecaeab3c --- /dev/null +++ b/src/broadcasting/src/ConfigProvider.php @@ -0,0 +1,20 @@ + [ + Factory::class => fn (ContainerInterface $container) => new BroadcastManager($container), + ], + ]; + } +} diff --git a/src/broadcasting/src/LICENSE.md b/src/broadcasting/src/LICENSE.md deleted file mode 100644 index 193eba9f..00000000 --- a/src/broadcasting/src/LICENSE.md +++ /dev/null @@ -1,25 +0,0 @@ -The MIT License (MIT) - -Copyright (c) Taylor Otwell - -Copyright (c) Hyperf - -Copyright (c) Laravel Hyperf - -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/src/UniqueBroadcastEvent.php b/src/broadcasting/src/UniqueBroadcastEvent.php index dd92f909..595e3cc2 100644 --- a/src/broadcasting/src/UniqueBroadcastEvent.php +++ b/src/broadcasting/src/UniqueBroadcastEvent.php @@ -4,8 +4,9 @@ namespace SwooleTW\Hyperf\Broadcasting; +use Psr\Container\ContainerInterface; +use SwooleTW\Hyperf\Cache\Contracts\Factory as Cache; use SwooleTW\Hyperf\Cache\Contracts\Repository; -use SwooleTW\Hyperf\Foundation\ApplicationContext; // TODO: wait queue // use Illuminate\Contracts\Queue\ShouldBeUnique; @@ -14,6 +15,11 @@ // class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique class UniqueBroadcastEvent extends BroadcastEvent { + /** + * The container instance. + */ + public ContainerInterface $container; + /** * The unique lock identifier. */ @@ -27,8 +33,10 @@ class UniqueBroadcastEvent extends BroadcastEvent /** * Create a new event instance. */ - public function __construct(mixed $event) + public function __construct(ContainerInterface $container, mixed $event) { + $this->container = $container; + $this->uniqueId = get_class($event); if (method_exists($event, 'uniqueId')) { @@ -54,6 +62,6 @@ public function uniqueVia(): Repository // TODO: Repository 好像沒有註冊在 SwooleTW\Hyperf\Foundation\Application@registerCoreContainerAliases return method_exists($this->event, 'uniqueVia') ? $this->event->uniqueVia() - : ApplicationContext::getContainer()->get(Repository::class); + : $this->container->get(Cache::class); } } diff --git a/src/support/src/Facades/Broadcast.php b/src/support/src/Facades/Broadcast.php index 92e38752..2bab627f 100644 --- a/src/support/src/Facades/Broadcast.php +++ b/src/support/src/Facades/Broadcast.php @@ -10,33 +10,33 @@ * @method static void routes(array|null $attributes = null) * @method static void userRoutes(array|null $attributes = null) * @method static void channelRoutes(array|null $attributes = null) -* @method static string|null socket(\Illuminate\Http\Request|null $request = null) -* @method static \Illuminate\Broadcasting\AnonymousEvent on(\Illuminate\Broadcasting\Channel|array|string $channels) -* @method static \Illuminate\Broadcasting\AnonymousEvent private(string $channel) -* @method static \Illuminate\Broadcasting\AnonymousEvent presence(string $channel) -* @method static \Illuminate\Broadcasting\PendingBroadcast event(mixed|null $event = null) +* @method static string|null socket(\Hyperf\HttpServer\Contract\RequestInterface|null $request = null) +* @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent on(\SwooleTW\Hyperf\Broadcasting\Channel|array|string $channels) +* @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent private(string $channel) +* @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent presence(string $channel) +* @method static \SwooleTW\Hyperf\Broadcasting\PendingBroadcast event(mixed|null $event = null) * @method static void queue(mixed $event) -* @method static mixed connection(string|null $driver = null) -* @method static mixed driver(string|null $name = null) +* @method static \SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster connection(string|null $driver = null) +* @method static \SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster driver(string|null $name = null) * @method static \Pusher\Pusher pusher(array $config) * @method static \Ably\AblyRest ably(array $config) * @method static string getDefaultDriver() * @method static void setDefaultDriver(string $name) * @method static void purge(string|null $name = null) -* @method static \Illuminate\Broadcasting\BroadcastManager extend(string $driver, \Closure $callback) -* @method static \Illuminate\Contracts\Foundation\Application getApplication() -* @method static \Illuminate\Broadcasting\BroadcastManager setApplication(\Illuminate\Contracts\Foundation\Application $app) -* @method static \Illuminate\Broadcasting\BroadcastManager forgetDrivers() -* @method static mixed auth(\Illuminate\Http\Request $request) -* @method static mixed validAuthenticationResponse(\Illuminate\Http\Request $request, mixed $result) +* @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager extend(string $driver, \Closure $callback) +* @method static \Psr\Container\ContainerInterface getApplication() +* @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager setApplication(\Psr\Container\ContainerInterface $app) +* @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager forgetDrivers() +* @method static mixed auth(\Hyperf\HttpServer\Contract\RequestInterface $request) +* @method static mixed validAuthenticationResponse(\Hyperf\HttpServer\Contract\RequestInterface $request, mixed $result) * @method static void broadcast(array $channels, string $event, array $payload = []) -* @method static array|null resolveAuthenticatedUser(\Illuminate\Http\Request $request) +* @method static array|null resolveAuthenticatedUser(\Hyperf\HttpServer\Contract\RequestInterface $request) * @method static void resolveAuthenticatedUserUsing(\Closure $callback) -* @method static \Illuminate\Broadcasting\Broadcasters\Broadcaster channel(\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $channel, callable|string $callback, array $options = []) -* @method static \Illuminate\Support\Collection getChannels() +* @method static \SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster channel(\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $channel, callable|string $callback, array $options = []) +* @method static \Hyperf\Collection\Collection getChannels() * -* @see \Illuminate\Broadcasting\BroadcastManager -* @see \Illuminate\Broadcasting\Broadcasters\Broadcaster +* @see \SwooleTW\Hyperf\Broadcasting\BroadcastManager +* @see \SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster */ class Broadcast extends Facade { From 2a8148e125e8111bbbb1c5b27f63f624b4af2812 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 21 Nov 2024 09:37:21 +0800 Subject: [PATCH 22/30] add integration test --- src/broadcasting/src/AnonymousEvent.php | 2 +- src/broadcasting/src/BroadcastManager.php | 4 +- .../Broadcasting/BroadcastManagerTest.php | 111 ++++++++++++++++++ ...SendingBroadcastsViaAnonymousEventTest.php | 14 +++ 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/Broadcasting/BroadcastManagerTest.php create mode 100644 tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php diff --git a/src/broadcasting/src/AnonymousEvent.php b/src/broadcasting/src/AnonymousEvent.php index 0cfeb142..ae0aeb04 100644 --- a/src/broadcasting/src/AnonymousEvent.php +++ b/src/broadcasting/src/AnonymousEvent.php @@ -134,7 +134,7 @@ public function broadcastWith(): array /** * Get the channels the event should broadcast on. * - * @return \Illuminate\Broadcasting\Channel|\Illuminate\Broadcasting\Channel[]|string[]|string + * @return Channel|Channel[]|string[]|string */ public function broadcastOn(): Channel|array { diff --git a/src/broadcasting/src/BroadcastManager.php b/src/broadcasting/src/BroadcastManager.php index c5b8aaf9..e07e597f 100644 --- a/src/broadcasting/src/BroadcastManager.php +++ b/src/broadcasting/src/BroadcastManager.php @@ -339,7 +339,7 @@ protected function createRedisDriver(array $config): Broadcaster $this->app, $this->app->get(RedisFactory::class), $config['connection'] ?? null, - $this->app->get(ConfigInterface::class)->set('database.redis.options.prefix', ''), + $this->app->get(ConfigInterface::class)->get('database.redis.options.prefix', ''), ); } @@ -368,7 +368,7 @@ protected function createNullDriver(array $config): Broadcaster * @param string $name * @return array */ - protected function getConfig(string $name): array + protected function getConfig(string $name): ?array { if (! is_null($name) && $name !== 'null') { return $this->app->get(ConfigInterface::class)->get("broadcasting.connections.{$name}"); diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php new file mode 100644 index 00000000..3cadd74a --- /dev/null +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -0,0 +1,111 @@ +assertFalse($this->app->get(Cache::class)->lock($lockKey, 10)->get()); + // } + + public function testThrowExceptionWhenUnknownStoreIsUsed() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Broadcast connection [alien_connection] is not defined.'); + + $config = m::mock(ContainerInterface::class); + $config->shouldReceive('get')->with('broadcasting.connections.alien_connection')->andReturn(null); + + $app = m::mock(ContainerInterface::class); + $app->shouldReceive('get')->with(ConfigInterface::class)->andReturn($config); + + $broadcastManager = new BroadcastManager($app); + + $broadcastManager->connection('alien_connection'); + } +} + +class TestEvent implements ShouldBroadcast +{ + /** + * Get the channels the event should broadcast on. + * + * @return Channel|Channel[]|string[]|string + */ + public function broadcastOn(): Channel|string|array + { + return []; + } +} + +class TestEventNow implements ShouldBroadcastNow +{ + /** + * Get the channels the event should broadcast on. + * + * @return Channel|Channel[]|string[]|string + */ + public function broadcastOn(): Channel|string|array + { + return []; + } +} + +class TestEventUnique implements ShouldBroadcast, ShouldBeUnique +{ + /** + * Get the channels the event should broadcast on. + * + * @return Channel|Channel[]|string[]|string + */ + public function broadcastOn(): Channel|string|array + { + return []; + } +} diff --git a/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php b/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php new file mode 100644 index 00000000..fdd9fc54 --- /dev/null +++ b/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php @@ -0,0 +1,14 @@ +assertTrue(true); + } +} From 02b99e81a53fdc3b180fb4e4fe5314518cab114a Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 21 Nov 2024 09:41:54 +0800 Subject: [PATCH 23/30] cx fix --- src/broadcasting/src/AnonymousEvent.php | 13 ++-- src/broadcasting/src/BroadcastEvent.php | 6 +- src/broadcasting/src/BroadcastManager.php | 22 +++---- .../src/Broadcasters/LogBroadcaster.php | 11 +--- .../src/Broadcasters/NullBroadcaster.php | 9 --- .../src/Broadcasters/PusherBroadcaster.php | 1 + src/broadcasting/src/Contracts/Factory.php | 2 - .../src/Contracts/ShouldBeUnique.php | 1 - .../src/Contracts/ShouldBroadcast.php | 4 +- .../src/EncryptedPrivateChannel.php | 2 +- .../src/InteractsWithBroadcasting.php | 2 +- src/broadcasting/src/PresenceChannel.php | 2 +- src/broadcasting/src/PrivateChannel.php | 2 +- src/support/src/Facades/Broadcast.php | 63 +++++++++---------- tests/Broadcasting/AblyBroadcasterTest.php | 2 + tests/Broadcasting/BroadcastEventTest.php | 21 +++++-- tests/Broadcasting/BroadcasterTest.php | 1 + tests/Broadcasting/PusherBroadcasterTest.php | 2 + tests/Broadcasting/RedisBroadcasterTest.php | 5 +- .../UsePusherChannelsNamesTest.php | 12 ++-- .../Broadcasting/BroadcastManagerTest.php | 16 +++-- ...SendingBroadcastsViaAnonymousEventTest.php | 6 ++ 22 files changed, 105 insertions(+), 100 deletions(-) diff --git a/src/broadcasting/src/AnonymousEvent.php b/src/broadcasting/src/AnonymousEvent.php index ae0aeb04..2b8ab529 100644 --- a/src/broadcasting/src/AnonymousEvent.php +++ b/src/broadcasting/src/AnonymousEvent.php @@ -7,12 +7,13 @@ use Hyperf\Collection\Arr; use Hyperf\Contract\Arrayable; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcast; -use SwooleTW\Hyperf\Broadcasting\InteractsWithBroadcasting; use SwooleTW\Hyperf\Foundation\Events\Dispatchable; class AnonymousEvent implements ShouldBroadcast { - use Dispatchable, InteractsWithBroadcasting, InteractsWithSockets; + use Dispatchable; + use InteractsWithBroadcasting; + use InteractsWithSockets; /** * The connection the event should be broadcast on. @@ -42,7 +43,7 @@ class AnonymousEvent implements ShouldBroadcast /** * Create a new anonymous broadcastable event instance. */ - public function __construct(protected Channel|array|string $channels) + public function __construct(protected array|Channel|string $channels) { $this->channels = Arr::wrap($channels); } @@ -70,7 +71,7 @@ public function as(string $name): static /** * Set the payload the event should be broadcast with. */ - public function with(Arrayable|array $payload): static + public function with(array|Arrayable $payload): static { $this->payload = $payload instanceof Arrayable ? $payload->toArray() @@ -134,9 +135,9 @@ public function broadcastWith(): array /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string[]|string + * @return Channel|Channel[]|string|string[] */ - public function broadcastOn(): Channel|array + public function broadcastOn(): array|Channel { return $this->channels; } diff --git a/src/broadcasting/src/BroadcastEvent.php b/src/broadcasting/src/BroadcastEvent.php index 5f4303b3..f42025b5 100644 --- a/src/broadcasting/src/BroadcastEvent.php +++ b/src/broadcasting/src/BroadcastEvent.php @@ -38,7 +38,7 @@ class BroadcastEvent public ?int $backoff; /** - * Indicate that the event should be dispatched after all open database transactions have been committed + * Indicate that the event should be dispatched after all open database transactions have been committed. */ public ?bool $afterCommit; @@ -83,7 +83,9 @@ public function handle(BroadcastingFactory $manager): void foreach ($connections as $connection) { $manager->connection($connection)->broadcast( - $channels, $name, $payload + $channels, + $name, + $payload ); } } diff --git a/src/broadcasting/src/BroadcastManager.php b/src/broadcasting/src/BroadcastManager.php index e07e597f..acf7c1ce 100644 --- a/src/broadcasting/src/BroadcastManager.php +++ b/src/broadcasting/src/BroadcastManager.php @@ -71,8 +71,8 @@ public function routes(?array $attributes = null): void $this->app->get(DispatcherFactory::class)->getRouter() ->group($attributes, function ($router) { - $router->get('/broadcasting/auth', '\\'.BroadcastController::class.'@authenticate'); - $router->post('/broadcasting/auth', '\\'.BroadcastController::class.'@authenticate'); + $router->get('/broadcasting/auth', '\\' . BroadcastController::class . '@authenticate'); + $router->post('/broadcasting/auth', '\\' . BroadcastController::class . '@authenticate'); }); } @@ -85,8 +85,8 @@ public function userRoutes(?array $attributes = null): void $this->app->get(DispatcherFactory::class)->getRouter() ->group($attributes, function ($router) { - $router->get('/broadcasting/user-auth', '\\'.BroadcastController::class.'@authenticateUser'); - $router->post('/broadcasting/user-auth', '\\'.BroadcastController::class.'@authenticateUser'); + $router->get('/broadcasting/user-auth', '\\' . BroadcastController::class . '@authenticateUser'); + $router->post('/broadcasting/user-auth', '\\' . BroadcastController::class . '@authenticateUser'); }); } @@ -113,7 +113,7 @@ public function socket(?RequestInterface $request = null): ?string /** * Begin sending an anonymous broadcast to the given channels. */ - public function on(Channel|string|array $channels): AnonymousEvent + public function on(array|Channel|string $channels): AnonymousEvent { return new AnonymousEvent($channels); } @@ -236,11 +236,10 @@ protected function resolve(string $name): Broadcaster $config['pool'] ?? [] ) : $this->doResolve($config); - } /** - * Resolve the given broadcaster + * Resolve the given broadcaster. * * @throws InvalidArgumentException */ @@ -250,7 +249,7 @@ protected function doResolve(array $config): Broadcaster return $this->callCustomCreator($config); } - $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; + $driverMethod = 'create' . ucfirst($config['driver']) . 'Driver'; if (! method_exists($this, $driverMethod)) { throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported."); @@ -359,14 +358,11 @@ protected function createLogDriver(array $config): Broadcaster */ protected function createNullDriver(array $config): Broadcaster { - return new NullBroadcaster; + return new NullBroadcaster(); } /** * Get the connection configuration. - * - * @param string $name - * @return array */ protected function getConfig(string $name): ?array { @@ -446,6 +442,6 @@ public function forgetDrivers(): static */ public function __call(string $method, array $parameters): mixed { - return $this->driver()->$method(...$parameters); + return $this->driver()->{$method}(...$parameters); } } diff --git a/src/broadcasting/src/Broadcasters/LogBroadcaster.php b/src/broadcasting/src/Broadcasters/LogBroadcaster.php index 04d4b90d..7762d60f 100644 --- a/src/broadcasting/src/Broadcasters/LogBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/LogBroadcaster.php @@ -19,31 +19,22 @@ public function __construct( ) { } - /** - * {@inheritdoc} - */ public function auth(RequestInterface $request): mixed { return null; } - /** - * {@inheritdoc} - */ public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed { return null; } - /** - * {@inheritdoc} - */ public function broadcast(array $channels, string $event, array $payload = []): void { $channels = implode(', ', $this->formatChannels($channels)); $payload = json_encode($payload, JSON_PRETTY_PRINT); - $this->logger->info('Broadcasting ['.$event.'] on channels ['.$channels.'] with payload:'.PHP_EOL.$payload); + $this->logger->info("Broadcasting [{$event}] on channels [{$channels}] with payload:" . PHP_EOL . $payload); } } diff --git a/src/broadcasting/src/Broadcasters/NullBroadcaster.php b/src/broadcasting/src/Broadcasters/NullBroadcaster.php index e2344378..c454e9f6 100644 --- a/src/broadcasting/src/Broadcasters/NullBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/NullBroadcaster.php @@ -8,25 +8,16 @@ class NullBroadcaster extends Broadcaster { - /** - * {@inheritdoc} - */ public function auth(RequestInterface $request): mixed { return null; } - /** - * {@inheritdoc} - */ public function validAuthenticationResponse(RequestInterface $request, mixed $result): mixed { return null; } - /** - * {@inheritdoc} - */ public function broadcast(array $channels, string $event, array $payload = []): void { } diff --git a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php index bc33cc76..ce226ee6 100644 --- a/src/broadcasting/src/Broadcasters/PusherBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/PusherBroadcaster.php @@ -16,6 +16,7 @@ class PusherBroadcaster extends Broadcaster { use UsePusherChannelConventions; + /** * Create a new broadcaster instance. */ diff --git a/src/broadcasting/src/Contracts/Factory.php b/src/broadcasting/src/Contracts/Factory.php index 116df256..01daae89 100644 --- a/src/broadcasting/src/Contracts/Factory.php +++ b/src/broadcasting/src/Contracts/Factory.php @@ -4,8 +4,6 @@ namespace SwooleTW\Hyperf\Broadcasting\Contracts; -use SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster; - interface Factory { /** diff --git a/src/broadcasting/src/Contracts/ShouldBeUnique.php b/src/broadcasting/src/Contracts/ShouldBeUnique.php index 99628cd2..29b41ca0 100644 --- a/src/broadcasting/src/Contracts/ShouldBeUnique.php +++ b/src/broadcasting/src/Contracts/ShouldBeUnique.php @@ -6,5 +6,4 @@ interface ShouldBeUnique { - // } diff --git a/src/broadcasting/src/Contracts/ShouldBroadcast.php b/src/broadcasting/src/Contracts/ShouldBroadcast.php index 5d1f3d57..c22cf160 100644 --- a/src/broadcasting/src/Contracts/ShouldBroadcast.php +++ b/src/broadcasting/src/Contracts/ShouldBroadcast.php @@ -11,7 +11,7 @@ interface ShouldBroadcast /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string[]|string + * @return Channel|Channel[]|string|string[] */ - public function broadcastOn(): Channel|string|array; + public function broadcastOn(): array|Channel|string; } diff --git a/src/broadcasting/src/EncryptedPrivateChannel.php b/src/broadcasting/src/EncryptedPrivateChannel.php index acc42377..fe2d528a 100644 --- a/src/broadcasting/src/EncryptedPrivateChannel.php +++ b/src/broadcasting/src/EncryptedPrivateChannel.php @@ -11,6 +11,6 @@ class EncryptedPrivateChannel extends Channel */ public function __construct(string $name) { - parent::__construct('private-encrypted-'.$name); + parent::__construct('private-encrypted-' . $name); } } diff --git a/src/broadcasting/src/InteractsWithBroadcasting.php b/src/broadcasting/src/InteractsWithBroadcasting.php index 2b953e01..cccbebff 100644 --- a/src/broadcasting/src/InteractsWithBroadcasting.php +++ b/src/broadcasting/src/InteractsWithBroadcasting.php @@ -16,7 +16,7 @@ trait InteractsWithBroadcasting /** * Broadcast the event using a specific broadcaster. */ - public function broadcastVia(array|string|null $connection = null): static + public function broadcastVia(null|array|string $connection = null): static { $this->broadcastConnection = is_null($connection) ? [null] diff --git a/src/broadcasting/src/PresenceChannel.php b/src/broadcasting/src/PresenceChannel.php index 071514bb..38b00332 100644 --- a/src/broadcasting/src/PresenceChannel.php +++ b/src/broadcasting/src/PresenceChannel.php @@ -11,6 +11,6 @@ class PresenceChannel extends Channel */ public function __construct(string $name) { - parent::__construct('presence-'.$name); + parent::__construct('presence-' . $name); } } diff --git a/src/broadcasting/src/PrivateChannel.php b/src/broadcasting/src/PrivateChannel.php index f8a614d3..a550ae59 100644 --- a/src/broadcasting/src/PrivateChannel.php +++ b/src/broadcasting/src/PrivateChannel.php @@ -15,6 +15,6 @@ public function __construct(HasBroadcastChannel|string $name) { $name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name; - parent::__construct('private-'.$name); + parent::__construct('private-' . $name); } } diff --git a/src/support/src/Facades/Broadcast.php b/src/support/src/Facades/Broadcast.php index 2bab627f..5f6e1dd6 100644 --- a/src/support/src/Facades/Broadcast.php +++ b/src/support/src/Facades/Broadcast.php @@ -7,37 +7,37 @@ use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastingFactoryContract; /** -* @method static void routes(array|null $attributes = null) -* @method static void userRoutes(array|null $attributes = null) -* @method static void channelRoutes(array|null $attributes = null) -* @method static string|null socket(\Hyperf\HttpServer\Contract\RequestInterface|null $request = null) -* @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent on(\SwooleTW\Hyperf\Broadcasting\Channel|array|string $channels) -* @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent private(string $channel) -* @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent presence(string $channel) -* @method static \SwooleTW\Hyperf\Broadcasting\PendingBroadcast event(mixed|null $event = null) -* @method static void queue(mixed $event) -* @method static \SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster connection(string|null $driver = null) -* @method static \SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster driver(string|null $name = null) -* @method static \Pusher\Pusher pusher(array $config) -* @method static \Ably\AblyRest ably(array $config) -* @method static string getDefaultDriver() -* @method static void setDefaultDriver(string $name) -* @method static void purge(string|null $name = null) -* @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager extend(string $driver, \Closure $callback) -* @method static \Psr\Container\ContainerInterface getApplication() -* @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager setApplication(\Psr\Container\ContainerInterface $app) -* @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager forgetDrivers() -* @method static mixed auth(\Hyperf\HttpServer\Contract\RequestInterface $request) -* @method static mixed validAuthenticationResponse(\Hyperf\HttpServer\Contract\RequestInterface $request, mixed $result) -* @method static void broadcast(array $channels, string $event, array $payload = []) -* @method static array|null resolveAuthenticatedUser(\Hyperf\HttpServer\Contract\RequestInterface $request) -* @method static void resolveAuthenticatedUserUsing(\Closure $callback) -* @method static \SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster channel(\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $channel, callable|string $callback, array $options = []) -* @method static \Hyperf\Collection\Collection getChannels() -* -* @see \SwooleTW\Hyperf\Broadcasting\BroadcastManager -* @see \SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster -*/ + * @method static void routes(array|null $attributes = null) + * @method static void userRoutes(array|null $attributes = null) + * @method static void channelRoutes(array|null $attributes = null) + * @method static string|null socket(\Hyperf\HttpServer\Contract\RequestInterface|null $request = null) + * @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent on(\SwooleTW\Hyperf\Broadcasting\Channel|array|string $channels) + * @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent private(string $channel) + * @method static \SwooleTW\Hyperf\Broadcasting\AnonymousEvent presence(string $channel) + * @method static \SwooleTW\Hyperf\Broadcasting\PendingBroadcast event(mixed|null $event = null) + * @method static void queue(mixed $event) + * @method static \SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster connection(string|null $driver = null) + * @method static \SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster driver(string|null $name = null) + * @method static \Pusher\Pusher pusher(array $config) + * @method static \Ably\AblyRest ably(array $config) + * @method static string getDefaultDriver() + * @method static void setDefaultDriver(string $name) + * @method static void purge(string|null $name = null) + * @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager extend(string $driver, \Closure $callback) + * @method static \Psr\Container\ContainerInterface getApplication() + * @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager setApplication(\Psr\Container\ContainerInterface $app) + * @method static \SwooleTW\Hyperf\Broadcasting\BroadcastManager forgetDrivers() + * @method static mixed auth(\Hyperf\HttpServer\Contract\RequestInterface $request) + * @method static mixed validAuthenticationResponse(\Hyperf\HttpServer\Contract\RequestInterface $request, mixed $result) + * @method static void broadcast(array $channels, string $event, array $payload = []) + * @method static array|null resolveAuthenticatedUser(\Hyperf\HttpServer\Contract\RequestInterface $request) + * @method static void resolveAuthenticatedUserUsing(\Closure $callback) + * @method static \SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster channel(\Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $channel, callable|string $callback, array $options = []) + * @method static \Hyperf\Collection\Collection getChannels() + * + * @see \SwooleTW\Hyperf\Broadcasting\BroadcastManager + * @see \SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster + */ class Broadcast extends Facade { /** @@ -48,4 +48,3 @@ protected static function getFacadeAccessor(): string return BroadcastingFactoryContract::class; } } - diff --git a/tests/Broadcasting/AblyBroadcasterTest.php b/tests/Broadcasting/AblyBroadcasterTest.php index 9b1bc52d..823031db 100644 --- a/tests/Broadcasting/AblyBroadcasterTest.php +++ b/tests/Broadcasting/AblyBroadcasterTest.php @@ -20,7 +20,9 @@ class AblyBroadcasterTest extends TestCase { protected AblyBroadcaster $broadcaster; + protected AblyRest $ably; + protected ContainerInterface $container; protected function setUp(): void diff --git a/tests/Broadcasting/BroadcastEventTest.php b/tests/Broadcasting/BroadcastEventTest.php index f1abac92..29a81505 100644 --- a/tests/Broadcasting/BroadcastEventTest.php +++ b/tests/Broadcasting/BroadcastEventTest.php @@ -11,6 +11,10 @@ use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastingFactory; use SwooleTW\Hyperf\Broadcasting\InteractsWithBroadcasting; +/** + * @internal + * @coversNothing + */ class BroadcastEventTest extends TestCase { protected function tearDown(): void @@ -23,14 +27,16 @@ public function testBasicEventBroadcastParameterFormatting() $broadcaster = m::mock(Broadcaster::class); $broadcaster->shouldReceive('broadcast')->once()->with( - ['test-channel'], TestBroadcastEvent::class, ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] + ['test-channel'], + TestBroadcastEvent::class, + ['firstName' => 'Taylor', 'lastName' => 'Otwell', 'collection' => ['foo' => 'bar']] ); $manager = m::mock(BroadcastingFactory::class); $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); - $event = new TestBroadcastEvent; + $event = new TestBroadcastEvent(); (new BroadcastEvent($event))->handle($manager); } @@ -40,14 +46,16 @@ public function testManualParameterSpecification() $broadcaster = m::mock(Broadcaster::class); $broadcaster->shouldReceive('broadcast')->once()->with( - ['test-channel'], TestBroadcastEventWithManualData::class, ['name' => 'Taylor', 'socket' => null] + ['test-channel'], + TestBroadcastEventWithManualData::class, + ['name' => 'Taylor', 'socket' => null] ); $manager = m::mock(BroadcastingFactory::class); $manager->shouldReceive('connection')->once()->with(null)->andReturn($broadcaster); - $event = new TestBroadcastEventWithManualData; + $event = new TestBroadcastEventWithManualData(); (new BroadcastEvent($event))->handle($manager); } @@ -62,7 +70,7 @@ public function testSpecificBroadcasterGiven() $manager->shouldReceive('connection')->once()->with('log')->andReturn($broadcaster); - $event = new TestBroadcastEventWithSpecificBroadcaster; + $event = new TestBroadcastEventWithSpecificBroadcaster(); (new BroadcastEvent($event))->handle($manager); } @@ -71,8 +79,11 @@ public function testSpecificBroadcasterGiven() class TestBroadcastEvent { public $firstName = 'Taylor'; + public $lastName = 'Otwell'; + public $collection; + private $title = 'Developer'; public function __construct() diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index ffae49fe..950437a7 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -28,6 +28,7 @@ class BroadcasterTest extends TestCase { protected ContainerInterface $container; + protected FakeBroadcaster $broadcaster; protected function setUp(): void diff --git a/tests/Broadcasting/PusherBroadcasterTest.php b/tests/Broadcasting/PusherBroadcasterTest.php index 2540d7bd..f1e7b5da 100644 --- a/tests/Broadcasting/PusherBroadcasterTest.php +++ b/tests/Broadcasting/PusherBroadcasterTest.php @@ -20,7 +20,9 @@ class PusherBroadcasterTest extends TestCase { protected ContainerInterface $container; + protected PusherBroadcaster $broadcaster; + protected Pusher $pusher; protected function setUp(): void diff --git a/tests/Broadcasting/RedisBroadcasterTest.php b/tests/Broadcasting/RedisBroadcasterTest.php index a9d5e616..fbdbafc4 100644 --- a/tests/Broadcasting/RedisBroadcasterTest.php +++ b/tests/Broadcasting/RedisBroadcasterTest.php @@ -9,13 +9,9 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; -use stdClass; use SwooleTW\Hyperf\Auth\AuthManager; -use SwooleTW\Hyperf\Auth\Contracts\FactoryContract; use SwooleTW\Hyperf\Broadcasting\Broadcasters\RedisBroadcaster; -use SwooleTW\Hyperf\Foundation\ApplicationContext; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; -use SwooleTW\Hyperf\Support\Facades\Auth; use SwooleTW\Hyperf\Support\Facades\Facade; use SwooleTW\Hyperf\Tests\Foundation\Concerns\HasMockedApplication; @@ -28,6 +24,7 @@ class RedisBroadcasterTest extends TestCase use HasMockedApplication; protected RedisBroadcaster $broadcaster; + protected ContainerInterface $container; protected function setUp(): void diff --git a/tests/Broadcasting/UsePusherChannelsNamesTest.php b/tests/Broadcasting/UsePusherChannelsNamesTest.php index 70b5bda3..65e684b6 100644 --- a/tests/Broadcasting/UsePusherChannelsNamesTest.php +++ b/tests/Broadcasting/UsePusherChannelsNamesTest.php @@ -10,12 +10,16 @@ use SwooleTW\Hyperf\Broadcasting\Broadcasters\Broadcaster; use SwooleTW\Hyperf\Broadcasting\Broadcasters\UsePusherChannelConventions; +/** + * @internal + * @coversNothing + */ class UsePusherChannelsNamesTest extends TestCase { #[DataProvider('channelsProvider')] public function testChannelNameNormalization($requestChannelName, $normalizedName) { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames(); $this->assertSame( $normalizedName, @@ -25,7 +29,7 @@ public function testChannelNameNormalization($requestChannelName, $normalizedNam public function testChannelNameNormalizationSpecialCase() { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames(); $this->assertSame( 'private-123', @@ -36,7 +40,7 @@ public function testChannelNameNormalizationSpecialCase() #[DataProvider('channelsProvider')] public function testIsGuardedChannel($requestChannelName, $_, $guarded) { - $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; + $broadcaster = new FakeBroadcasterUsingPusherChannelsNames(); $this->assertSame( $guarded, @@ -71,7 +75,7 @@ public static function channelsProvider() foreach ($prefixesInfos as $prefixInfos) { foreach ($channels as $channel) { $tests[] = [ - $prefixInfos['prefix'].$channel, + $prefixInfos['prefix'] . $channel, $channel, $prefixInfos['guarded'], ]; diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index 3cadd74a..b74efe72 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -15,6 +15,10 @@ use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcast; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcastNow; +/** + * @internal + * @coversNothing + */ class BroadcastManagerTest extends TestCase { // TODO: waiting for queue implementation @@ -76,9 +80,9 @@ class TestEvent implements ShouldBroadcast /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string[]|string + * @return Channel|Channel[]|string|string[] */ - public function broadcastOn(): Channel|string|array + public function broadcastOn(): array|Channel|string { return []; } @@ -89,9 +93,9 @@ class TestEventNow implements ShouldBroadcastNow /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string[]|string + * @return Channel|Channel[]|string|string[] */ - public function broadcastOn(): Channel|string|array + public function broadcastOn(): array|Channel|string { return []; } @@ -102,9 +106,9 @@ class TestEventUnique implements ShouldBroadcast, ShouldBeUnique /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string[]|string + * @return Channel|Channel[]|string|string[] */ - public function broadcastOn(): Channel|string|array + public function broadcastOn(): array|Channel|string { return []; } diff --git a/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php b/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php index fdd9fc54..558d9690 100644 --- a/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php +++ b/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php @@ -1,9 +1,15 @@ Date: Thu, 21 Nov 2024 12:19:24 +0800 Subject: [PATCH 24/30] add SendingBroadcastsViaAnonymousEventTest i --- src/broadcasting/src/AnonymousEvent.php | 4 +- src/broadcasting/src/BroadcastController.php | 2 +- .../src/Broadcasters/AblyBroadcaster.php | 7 +- .../src/Contracts/ShouldBroadcast.php | 4 +- src/broadcasting/src/InteractsWithSockets.php | 2 +- src/foundation/src/Events/Dispatchable.php | 8 +- .../Broadcasting/BroadcastManagerTest.php | 12 +- ...SendingBroadcastsViaAnonymousEventTest.php | 181 +++++++++++++++++- 8 files changed, 194 insertions(+), 26 deletions(-) diff --git a/src/broadcasting/src/AnonymousEvent.php b/src/broadcasting/src/AnonymousEvent.php index 2b8ab529..fc56818b 100644 --- a/src/broadcasting/src/AnonymousEvent.php +++ b/src/broadcasting/src/AnonymousEvent.php @@ -135,9 +135,9 @@ public function broadcastWith(): array /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string|string[] + * @return Channel[]|string[] */ - public function broadcastOn(): array|Channel + public function broadcastOn(): array { return $this->channels; } diff --git a/src/broadcasting/src/BroadcastController.php b/src/broadcasting/src/BroadcastController.php index be6e21d8..a233990a 100644 --- a/src/broadcasting/src/BroadcastController.php +++ b/src/broadcasting/src/BroadcastController.php @@ -23,7 +23,7 @@ public function authenticate(RequestInterface $request): mixed * * See: https://pusher.com/docs/channels/server_api/authenticating-users/#user-authentication. * - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @throws AccessDeniedHttpException */ public function authenticateUser(RequestInterface $request): array { diff --git a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php index db0ab0bf..59522354 100644 --- a/src/broadcasting/src/Broadcasters/AblyBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/AblyBroadcaster.php @@ -104,10 +104,9 @@ public function broadcast(array $channels, string $event, array $payload = []): { try { foreach ($this->formatChannels($channels) as $channel) { - // @phpstan-ignore-line - $this->ably->channels->get($channel)->publish( - $this->buildAblyMessage($event, $payload) - ); + $this->ably->channels->get($channel)->publish( // @phpstan-ignore-line + $this->buildAblyMessage($event, $payload) // @phpstan-ignore-line + ); // @phpstan-ignore-line } } catch (AblyException $e) { throw new BroadcastException( diff --git a/src/broadcasting/src/Contracts/ShouldBroadcast.php b/src/broadcasting/src/Contracts/ShouldBroadcast.php index c22cf160..415094a3 100644 --- a/src/broadcasting/src/Contracts/ShouldBroadcast.php +++ b/src/broadcasting/src/Contracts/ShouldBroadcast.php @@ -11,7 +11,7 @@ interface ShouldBroadcast /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string|string[] + * @return Channel[]|string[] */ - public function broadcastOn(): array|Channel|string; + public function broadcastOn(): array; } diff --git a/src/broadcasting/src/InteractsWithSockets.php b/src/broadcasting/src/InteractsWithSockets.php index 94da9666..454c8afe 100644 --- a/src/broadcasting/src/InteractsWithSockets.php +++ b/src/broadcasting/src/InteractsWithSockets.php @@ -11,7 +11,7 @@ trait InteractsWithSockets /** * The socket ID for the user that raised the event. */ - public ?string $socket; + public ?string $socket = null; /** * Exclude the current user from receiving the broadcast. diff --git a/src/foundation/src/Events/Dispatchable.php b/src/foundation/src/Events/Dispatchable.php index dc22072f..27dbd63c 100644 --- a/src/foundation/src/Events/Dispatchable.php +++ b/src/foundation/src/Events/Dispatchable.php @@ -21,9 +21,7 @@ public static function dispatch(): mixed */ public static function dispatchIf(bool $boolean, mixed ...$arguments): mixed { - if ($boolean) { - return event(new static(...$arguments)); - } + return $boolean ? event(new static(...$arguments)) : null; } /** @@ -31,9 +29,7 @@ public static function dispatchIf(bool $boolean, mixed ...$arguments): mixed */ public static function dispatchUnless(bool $boolean, mixed ...$arguments): mixed { - if (! $boolean) { - return event(new static(...$arguments)); - } + return $boolean ? null : event(new static(...$arguments)); } /** diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index b74efe72..acc379fa 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -80,9 +80,9 @@ class TestEvent implements ShouldBroadcast /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string|string[] + * @return Channel[]|string[] */ - public function broadcastOn(): array|Channel|string + public function broadcastOn(): array { return []; } @@ -93,9 +93,9 @@ class TestEventNow implements ShouldBroadcastNow /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string|string[] + * @return Channel[]|string[] */ - public function broadcastOn(): array|Channel|string + public function broadcastOn(): array { return []; } @@ -106,9 +106,9 @@ class TestEventUnique implements ShouldBroadcast, ShouldBeUnique /** * Get the channels the event should broadcast on. * - * @return Channel|Channel[]|string|string[] + * @return Channel[]|string[] */ - public function broadcastOn(): array|Channel|string + public function broadcastOn(): array { return []; } diff --git a/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php b/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php index 558d9690..3090c1e1 100644 --- a/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php +++ b/tests/Integration/Broadcasting/SendingBroadcastsViaAnonymousEventTest.php @@ -2,9 +2,24 @@ declare(strict_types=1); -namespace Illuminate\Tests\Integration\Broadcasting; +namespace SwooleTW\Hyperf\Tests\Integration\Broadcasting; +use Hyperf\HttpServer\Contract\RequestInterface; +use Mockery as m; use PHPUnit\Framework\TestCase; +use Psr\EventDispatcher\EventDispatcherInterface; +use ReflectionClass; +use SwooleTW\Hyperf\Broadcasting\AnonymousEvent; +use SwooleTW\Hyperf\Broadcasting\BroadcastManager; +use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastingFactoryContract; +use SwooleTW\Hyperf\Broadcasting\PresenceChannel; +use SwooleTW\Hyperf\Broadcasting\PrivateChannel; +use SwooleTW\Hyperf\Container\DefinitionSource; +use SwooleTW\Hyperf\Foundation\Application; +use SwooleTW\Hyperf\Foundation\ApplicationContext; +use SwooleTW\Hyperf\Support\Facades\Broadcast; +use SwooleTW\Hyperf\Support\Facades\Event; +use SwooleTW\Hyperf\Support\Facades\Facade; /** * @internal @@ -12,9 +27,167 @@ */ class SendingBroadcastsViaAnonymousEventTest extends TestCase { - // TODO: waiting for queue implementation, then copy test file from laravel - public function testEventsCanBeBroadcast() + protected Application $container; + + protected function setUp(): void + { + parent::setUp(); + + $this->container = new Application( + new DefinitionSource([ + EventDispatcherInterface::class => fn () => m::mock(EventDispatcherInterface::class), + BroadcastingFactoryContract::class => fn ($container) => new BroadcastManager($container), + ]), + 'bath_path', + ); + + ApplicationContext::setContainer($this->container); + } + + protected function tearDown(): void + { + parent::tearDown(); + + m::close(); + + Facade::clearResolvedInstances(); + } + + public function testBroadcastIsSent() + { + Event::fake(); + + Broadcast::on('test-channel') + ->with(['some' => 'data']) + ->as('test-event') + ->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + return (new ReflectionClass($event))->getProperty('connection')->getValue($event) === null + && $event->broadcastOn() === ['test-channel'] + && $event->broadcastAs() === 'test-event' + && $event->broadcastWith() === ['some' => 'data']; + }); + } + + public function testBroadcastIsSentNow() + { + Event::fake(); + + Broadcast::on('test-channel') + ->with(['some' => 'data']) + ->as('test-event') + ->sendNow(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + return (new ReflectionClass($event))->getProperty('connection')->getValue($event) === null + && $event->shouldBroadcastNow(); + }); + } + + public function testDefaultNameIsSet() { - $this->assertTrue(true); + Event::fake(); + + Broadcast::on('test-channel') + ->with(['some' => 'data']) + ->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + return $event->broadcastAs() === 'AnonymousEvent'; + }); + } + + public function testDefaultPayloadIsSet() + { + Event::fake(); + + Broadcast::on('test-channel')->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + return $event->broadcastWith() === []; + }); + } + + public function testSendToMultipleChannels() + { + Event::fake(); + + Broadcast::on([ + 'test-channel', + new PrivateChannel('test-channel'), + 'presence-test-channel', + ])->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + [$one, $two, $three] = $event->broadcastOn(); + + return $one === 'test-channel' + && $two instanceof PrivateChannel + && $two->name === 'private-test-channel' + && $three === 'presence-test-channel'; + }); + } + + public function testSendViaANonDefaultConnection() + { + Event::fake(); + + Broadcast::on('test-channel') + ->via('pusher') + ->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + return (new ReflectionClass($event))->getProperty('connection')->getValue($event) === 'pusher'; + }); + } + + public function testSendToOthersOnly() + { + Event::fake(); + + $request = m::mock(RequestInterface::class); + $request->shouldReceive('header')->with('X-Socket-ID')->andReturn('12345'); + $this->container->set(RequestInterface::class, $request); + + Broadcast::on('test-channel')->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + return $event->socket === null; + }); + + Broadcast::on('test-channel') + ->toOthers() + ->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + return $event->socket = '12345'; + }); + } + + public function testSendToPrivateChannel() + { + Event::fake(); + + Broadcast::private('test-channel')->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + $channel = $event->broadcastOn()[0]; + + return $channel instanceof PrivateChannel && $channel->name === 'private-test-channel'; + }); + } + + public function testSendToPresenceChannel() + { + Event::fake(); + + Broadcast::presence('test-channel')->send(); + + Event::assertDispatched(AnonymousEvent::class, function ($event) { + $channel = $event->broadcastOn()[0]; + + return $channel instanceof PresenceChannel && $channel->name === 'presence-test-channel'; + }); } } From be5f3eda80cd6413efefdb07c2e443ccdc1c08f6 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 21 Nov 2024 13:49:15 +0800 Subject: [PATCH 25/30] fix test --- tests/Broadcasting/UsePusherChannelsNamesTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Broadcasting/UsePusherChannelsNamesTest.php b/tests/Broadcasting/UsePusherChannelsNamesTest.php index 65e684b6..6c40c628 100644 --- a/tests/Broadcasting/UsePusherChannelsNamesTest.php +++ b/tests/Broadcasting/UsePusherChannelsNamesTest.php @@ -87,7 +87,6 @@ public static function channelsProvider() $tests[] = ['presence-private-test', 'private-test', true]; $tests[] = ['presence-presence-test', 'presence-test', true]; $tests[] = ['public-test', 'public-test', false]; - dd($tests); return $tests; } From e3de47f8baf974c3b36569fb41990591c59525fb Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 21 Nov 2024 17:58:05 +0800 Subject: [PATCH 26/30] remove route binding code --- .../src/Broadcasters/Broadcaster.php | 40 +------------------ src/broadcasting/src/UniqueBroadcastEvent.php | 4 +- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index a566b447..e097406b 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -181,28 +181,11 @@ protected function extractChannelKeys(string $pattern, string $channel): array */ protected function resolveBinding(string $key, string $value, array $callbackParameters): mixed { - $newValue = $this->resolveExplicitBindingIfPossible($key, $value); - - return $newValue === $value ? $this->resolveImplicitBindingIfPossible( + return $this->resolveImplicitBindingIfPossible( $key, $value, $callbackParameters - ) : $newValue; - } - - /** - * Resolve an explicit parameter binding if applicable. - */ - protected function resolveExplicitBindingIfPossible(string $key, string $value): mixed - { - // TODO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar - // $binder = $this->binder(); - - // if ($binder && $binder->getBindingCallback($key)) { - // return call_user_func($binder->getBindingCallback($key), $value); - // } - - return $value; + ); } /** @@ -248,25 +231,6 @@ protected function formatChannels(array $channels): array }, $channels); } - // TODO: 實作 \Illuminate\Contracts\Routing\BindingRegistrar - /** - * Get the model binding registrar instance. - * - * @param mixed $callback - * @return \Illuminate\Contracts\Routing\BindingRegistrar - */ - // protected function binder() - // { - // if (! $this->bindingRegistrar) { - // $this->bindingRegistrar = $this->container->has(BindingRegistrar::class) - // ? $this->container->get(BindingRegistrar::class) - // : null; - // } - // - // return $this->bindingRegistrar; - // return null; - // } - /** * Normalize the given callback into a callable. * diff --git a/src/broadcasting/src/UniqueBroadcastEvent.php b/src/broadcasting/src/UniqueBroadcastEvent.php index 595e3cc2..01431cfa 100644 --- a/src/broadcasting/src/UniqueBroadcastEvent.php +++ b/src/broadcasting/src/UniqueBroadcastEvent.php @@ -6,7 +6,6 @@ use Psr\Container\ContainerInterface; use SwooleTW\Hyperf\Cache\Contracts\Factory as Cache; -use SwooleTW\Hyperf\Cache\Contracts\Repository; // TODO: wait queue // use Illuminate\Contracts\Queue\ShouldBeUnique; @@ -57,9 +56,8 @@ public function __construct(ContainerInterface $container, mixed $event) /** * Resolve the cache implementation that should manage the event's uniqueness. */ - public function uniqueVia(): Repository + public function uniqueVia(): Cache { - // TODO: Repository 好像沒有註冊在 SwooleTW\Hyperf\Foundation\Application@registerCoreContainerAliases return method_exists($this->event, 'uniqueVia') ? $this->event->uniqueVia() : $this->container->get(Cache::class); From 05b4b51ae42d4401bd8d897544e52195cabae616 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 21 Nov 2024 17:58:29 +0800 Subject: [PATCH 27/30] dispatch broadcaster --- src/event/src/EventDispatcher.php | 34 ++++++++- tests/Broadcasting/BroadcasterTest.php | 36 --------- tests/Event/BroadcastedEventsTest.php | 102 +++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 tests/Event/BroadcastedEventsTest.php diff --git a/src/event/src/EventDispatcher.php b/src/event/src/EventDispatcher.php index b08bf293..1aa419c8 100644 --- a/src/event/src/EventDispatcher.php +++ b/src/event/src/EventDispatcher.php @@ -14,6 +14,8 @@ use Psr\EventDispatcher\StoppableEventInterface; use Psr\Log\LoggerInterface; use ReflectionClass; +use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastFactory; +use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcast; use SwooleTW\Hyperf\Event\Contracts\EventDispatcherContract; use SwooleTW\Hyperf\Event\Contracts\ListenerProviderContract; use SwooleTW\Hyperf\Foundation\Contracts\Queue\ShouldQueue; @@ -109,10 +111,14 @@ public function until(object|string $event, mixed $payload = []): object|string } /** - * Fire an event and call the listeners. + * Broadcast an event and call the listeners. */ protected function invokeListeners(object|string $event, mixed $payload, bool $halt = false): object|string { + if ($this->shouldBroadcast($event)) { + $this->broadcastEvent($event); + } + foreach ($this->getListeners($event) as $listener) { $response = $listener($event, $payload); @@ -126,6 +132,32 @@ protected function invokeListeners(object|string $event, mixed $payload, bool $h return $event; } + /** + * Determine if the payload has a broadcastable event. + */ + protected function shouldBroadcast(object|string $event): bool + { + return is_object($event) + && $event instanceof ShouldBroadcast + && $this->broadcastWhen($event); + } + + /** + * Check if the event should be broadcasted by the condition. + */ + protected function broadcastWhen(mixed $event): bool + { + return method_exists($event, 'broadcastWhen') ? $event->broadcastWhen() : true; + } + + /** + * Broadcast the given event class. + */ + protected function broadcastEvent(ShouldBroadcast $event): void + { + $this->container->get(BroadcastFactory::class)->queue($event); + } + /** * Get all of the listeners for a given event name. */ diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 950437a7..4c3cfc52 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -70,24 +70,6 @@ public function testExtractingParametersWhileCheckingForUserAccess() }; $parameters = $this->broadcaster->extractAuthParameters('asd', 'asd', $callback); $this->assertEquals([], $parameters); - - /* - * Test Explicit Binding... - */ - // TODO: 要等 binder 實作 - // $container = new Container; - // Container::setInstance($container); - // $binder = m::mock(BindingRegistrar::class); - // $binder->shouldReceive('getBindingCallback')->times(2)->with('model')->andReturn(function () { - // return 'bound'; - // }); - // $container->instance(BindingRegistrar::class, $binder); - // $callback = function ($user, $model) { - // // - // }; - // $parameters = $this->broadcaster->extractAuthParameters('something.{model}', 'something.1', $callback); - // $this->assertEquals(['bound'], $parameters); - // Container::setInstance(new Container); } public function testCanUseChannelClasses() @@ -96,24 +78,6 @@ public function testCanUseChannelClasses() $this->assertEquals(['model.1.instance', 'something'], $parameters); } - // TODO: 要等 binder 實作 - // public function testModelRouteBinding() - // { - // $container = new Container; - // Container::setInstance($container); - // $binder = m::mock(BindingRegistrar::class); - // $callback = RouteBinding::forModel($container, BroadcasterTestEloquentModelStub::class); - // - // $binder->shouldReceive('getBindingCallback')->times(2)->with('model')->andReturn($callback); - // $container->instance(BindingRegistrar::class, $binder); - // $callback = function ($user, $model) { - // // - // }; - // $parameters = $this->broadcaster->extractAuthParameters('something.{model}', 'something.1', $callback); - // $this->assertEquals(['model.1.instance'], $parameters); - // Container::setInstance(new Container); - // } - public function testUnknownChannelAuthHandlerTypeThrowsException() { $this->expectException(Exception::class); diff --git a/tests/Event/BroadcastedEventsTest.php b/tests/Event/BroadcastedEventsTest.php new file mode 100644 index 00000000..904f0dad --- /dev/null +++ b/tests/Event/BroadcastedEventsTest.php @@ -0,0 +1,102 @@ +makePartial()->shouldAllowMockingProtectedMethods(); + + $event = new BroadcastEvent(); + + $this->assertTrue($d->shouldBroadcast($event)); + + $event = new AlwaysBroadcastEvent(); + + $this->assertTrue($d->shouldBroadcast($event)); + } + + public function testShouldBroadcastAsQueuedAndCallNormalListeners() + { + unset($_SERVER['__event.test']); + $broadcast = m::mock(BroadcastFactory::class); + $broadcast->shouldReceive('queue')->once(); + $container = m::mock(ContainerInterface::class); + $container->shouldReceive('get')->once()->with(BroadcastFactory::class)->andReturn($broadcast); + $d = new EventDispatcher(new ListenerProvider(), null, $container); + + $d->listen(AlwaysBroadcastEvent::class, function ($payload) { + $_SERVER['__event.test'] = $payload; + }); + + $d->dispatch($e = new AlwaysBroadcastEvent()); + + $this->assertSame($e, $_SERVER['__event.test']); + } + + public function testShouldBroadcastFail() + { + $d = m::mock(EventDispatcher::class); + + $d->makePartial()->shouldAllowMockingProtectedMethods(); + + $event = new BroadcastFalseCondition(); + + $this->assertFalse($d->shouldBroadcast($event)); + + $event = new ExampleEvent(); + + $this->assertFalse($d->shouldBroadcast($event)); + } +} + +class BroadcastEvent implements ShouldBroadcast +{ + public function broadcastOn(): array + { + return ['test-channel']; + } + + public function broadcastWhen() + { + return true; + } +} + +class AlwaysBroadcastEvent implements ShouldBroadcast +{ + public function broadcastOn(): array + { + return ['test-channel']; + } +} + +class BroadcastFalseCondition extends BroadcastEvent +{ + public function broadcastWhen() + { + return false; + } +} From b113bb7e1d572edf9c9d2016d4950e8dcdc2ebf2 Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Thu, 21 Nov 2024 18:17:53 +0800 Subject: [PATCH 28/30] add require package and remove useless code --- src/broadcasting/composer.json | 7 ++++++- src/broadcasting/src/Broadcasters/Broadcaster.php | 6 ------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/broadcasting/composer.json b/src/broadcasting/composer.json index 49a5814c..d38d64fa 100644 --- a/src/broadcasting/composer.json +++ b/src/broadcasting/composer.json @@ -29,9 +29,14 @@ "require": { "php": "^8.2", "hyperf/collection": "~3.1.0", - "hyperf/context": "~3.1.0", + "hyperf/contract": "~3.1.0", "hyperf/http-server": "~3.1.0", + "hyperf/pool": "~3.1.0", "hyperf/redis": "~3.1.0", + "hyperf/stringable": "~3.1.0", + "swooletw/hyperf-auth": "dev-master", + "swooletw/hyperf-cache": "dev-master", + "swooletw/hyperf-foundation": "dev-master", "swooletw/hyperf-framework": "dev-master", "swooletw/hyperf-router": "dev-master", "swooletw/hyperf-support": "dev-master", diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index e097406b..a43a31af 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -18,7 +18,6 @@ use SwooleTW\Hyperf\Broadcasting\Contracts\HasBroadcastChannel; use SwooleTW\Hyperf\HttpMessage\Exceptions\AccessDeniedHttpException; use SwooleTW\Hyperf\Router\Contracts\UrlRoutable; -use SwooleTW\Hyperf\Router\Router; use SwooleTW\Hyperf\Support\Reflector; abstract class Broadcaster implements BroadcasterContract @@ -43,11 +42,6 @@ abstract class Broadcaster implements BroadcasterContract */ protected array $channelOptions = []; - /** - * The binding registrar instance. - */ - protected Router $bindingRegistrar; - /** * Resolve the authenticated user payload for the incoming connection request. * From 9b34d29d8c95ade11f5eaca23a1d076a6fc9bf7a Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Fri, 22 Nov 2024 14:28:56 +0800 Subject: [PATCH 29/30] fix --- src/broadcasting/src/AnonymousEvent.php | 4 +- src/broadcasting/src/BroadcastEvent.php | 10 ++--- src/broadcasting/src/BroadcastManager.php | 37 ++++++++++--------- .../src/Broadcasters/Broadcaster.php | 11 +++--- .../src/Broadcasters/LogBroadcaster.php | 2 - src/broadcasting/src/PendingBroadcast.php | 20 +++------- src/broadcasting/src/UniqueBroadcastEvent.php | 2 +- 7 files changed, 37 insertions(+), 49 deletions(-) diff --git a/src/broadcasting/src/AnonymousEvent.php b/src/broadcasting/src/AnonymousEvent.php index fc56818b..a5b0ee6d 100644 --- a/src/broadcasting/src/AnonymousEvent.php +++ b/src/broadcasting/src/AnonymousEvent.php @@ -43,7 +43,7 @@ class AnonymousEvent implements ShouldBroadcast /** * Create a new anonymous broadcastable event instance. */ - public function __construct(protected array|Channel|string $channels) + public function __construct(protected BroadcastManager $broadcastManager, protected array|Channel|string $channels) { $this->channels = Arr::wrap($channels); } @@ -107,7 +107,7 @@ public function sendNow(): void */ public function send(): void { - $broadcast = broadcast($this)->via($this->connection); + $broadcast = $this->broadcastManager->event($this)->via($this->connection); if (! $this->includeCurrentUser) { $broadcast->toOthers(); diff --git a/src/broadcasting/src/BroadcastEvent.php b/src/broadcasting/src/BroadcastEvent.php index f42025b5..6b65a8d0 100644 --- a/src/broadcasting/src/BroadcastEvent.php +++ b/src/broadcasting/src/BroadcastEvent.php @@ -53,11 +53,11 @@ class BroadcastEvent public function __construct(mixed $event) { $this->event = $event; - $this->tries = property_exists($event, 'tries') ? $event->tries : null; - $this->timeout = property_exists($event, 'timeout') ? $event->timeout : null; - $this->backoff = property_exists($event, 'backoff') ? $event->backoff : null; - $this->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null; - $this->maxExceptions = property_exists($event, 'maxExceptions') ? $event->maxExceptions : null; + $this->tries = $event->tries ?? null; + $this->timeout = $event->timeout ?? null; + $this->backoff = $event->backoff ?? null; + $this->afterCommit = $event->afterCommit ?? null; + $this->maxExceptions = $event->maxExceptions ?? null; } /** diff --git a/src/broadcasting/src/BroadcastManager.php b/src/broadcasting/src/BroadcastManager.php index acf7c1ce..507c9b88 100644 --- a/src/broadcasting/src/BroadcastManager.php +++ b/src/broadcasting/src/BroadcastManager.php @@ -9,7 +9,7 @@ use GuzzleHttp\Client as GuzzleClient; use Hyperf\Contract\ConfigInterface; use Hyperf\HttpServer\Contract\RequestInterface; -use Hyperf\HttpServer\Router\DispatcherFactory; +use Hyperf\HttpServer\Router\DispatcherFactory as RouterDispatcherFactory; use Hyperf\Redis\RedisFactory; use InvalidArgumentException; use Psr\Container\ContainerInterface; @@ -22,7 +22,7 @@ use SwooleTW\Hyperf\Broadcasting\Broadcasters\PusherBroadcaster; use SwooleTW\Hyperf\Broadcasting\Broadcasters\RedisBroadcaster; use SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster; -use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as FactoryContract; +use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastingFactoryContract; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBeUnique; // use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcastNow; use SwooleTW\Hyperf\ObjectPool\Traits\HasPoolProxy; @@ -30,7 +30,7 @@ /** * @mixin Broadcaster */ -class BroadcastManager implements FactoryContract +class BroadcastManager implements BroadcastingFactoryContract { use HasPoolProxy; @@ -69,11 +69,13 @@ public function routes(?array $attributes = null): void { $attributes = $attributes ?: ['middleware' => ['web']]; - $this->app->get(DispatcherFactory::class)->getRouter() - ->group($attributes, function ($router) { - $router->get('/broadcasting/auth', '\\' . BroadcastController::class . '@authenticate'); - $router->post('/broadcasting/auth', '\\' . BroadcastController::class . '@authenticate'); - }); + $this->app->get(RouterDispatcherFactory::class)->getRouter() + ->addRoute( + ['GET', 'POST'], + '/broadcasting/auth', + [BroadcastController::class, 'authenticate'], + $attributes, + ); } /** @@ -83,11 +85,13 @@ public function userRoutes(?array $attributes = null): void { $attributes = $attributes ?: ['middleware' => ['web']]; - $this->app->get(DispatcherFactory::class)->getRouter() - ->group($attributes, function ($router) { - $router->get('/broadcasting/user-auth', '\\' . BroadcastController::class . '@authenticateUser'); - $router->post('/broadcasting/user-auth', '\\' . BroadcastController::class . '@authenticateUser'); - }); + $this->app->get(RouterDispatcherFactory::class)->getRouter() + ->addRoute( + ['GET', 'POST'], + '/broadcasting/user-auth', + [BroadcastController::class, 'authenticateUser'], + $attributes, + ); } /** @@ -115,7 +119,7 @@ public function socket(?RequestInterface $request = null): ?string */ public function on(array|Channel|string $channels): AnonymousEvent { - return new AnonymousEvent($channels); + return new AnonymousEvent($this, $channels); } /** @@ -347,10 +351,7 @@ protected function createRedisDriver(array $config): Broadcaster */ protected function createLogDriver(array $config): Broadcaster { - return new LogBroadcaster( - $this->app, - $this->app->get(LoggerInterface::class) - ); + return new LogBroadcaster($this->app->get(LoggerInterface::class)); } /** diff --git a/src/broadcasting/src/Broadcasters/Broadcaster.php b/src/broadcasting/src/Broadcasters/Broadcaster.php index a43a31af..0a6503f8 100644 --- a/src/broadcasting/src/Broadcasters/Broadcaster.php +++ b/src/broadcasting/src/Broadcasters/Broadcaster.php @@ -134,12 +134,11 @@ protected function extractAuthParameters(string $pattern, string $channel, calla */ protected function extractParameters(callable|string $callback): array { - if (is_callable($callback)) { - return (new ReflectionFunction($callback))->getParameters(); - } - if (is_string($callback)) { - return $this->extractParametersFromClass($callback); - } + return match (true) { + is_callable($callback) => (new ReflectionFunction($callback))->getParameters(), + is_string($callback) => $this->extractParametersFromClass($callback), + default => [], + }; } /** diff --git a/src/broadcasting/src/Broadcasters/LogBroadcaster.php b/src/broadcasting/src/Broadcasters/LogBroadcaster.php index 7762d60f..8903115f 100644 --- a/src/broadcasting/src/Broadcasters/LogBroadcaster.php +++ b/src/broadcasting/src/Broadcasters/LogBroadcaster.php @@ -5,7 +5,6 @@ namespace SwooleTW\Hyperf\Broadcasting\Broadcasters; use Hyperf\HttpServer\Contract\RequestInterface; -use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; class LogBroadcaster extends Broadcaster @@ -14,7 +13,6 @@ class LogBroadcaster extends Broadcaster * Create a new broadcaster instance. */ public function __construct( - protected ContainerInterface $container, protected LoggerInterface $logger ) { } diff --git a/src/broadcasting/src/PendingBroadcast.php b/src/broadcasting/src/PendingBroadcast.php index 2458a0c6..8d760ddc 100644 --- a/src/broadcasting/src/PendingBroadcast.php +++ b/src/broadcasting/src/PendingBroadcast.php @@ -8,23 +8,13 @@ class PendingBroadcast { - /** - * The event dispatcher implementation. - */ - protected EventDispatcherInterface $events; - - /** - * The event instance. - */ - protected mixed $event; - /** * Create a new pending broadcast instance. */ - public function __construct(EventDispatcherInterface $events, mixed $event) - { - $this->event = $event; - $this->events = $events; + public function __construct( + protected EventDispatcherInterface $eventDispatcher, + protected mixed $event + ) { } /** @@ -56,6 +46,6 @@ public function toOthers(): static */ public function __destruct() { - $this->events->dispatch($this->event); + $this->eventDispatcher->dispatch($this->event); } } diff --git a/src/broadcasting/src/UniqueBroadcastEvent.php b/src/broadcasting/src/UniqueBroadcastEvent.php index 01431cfa..53d7db60 100644 --- a/src/broadcasting/src/UniqueBroadcastEvent.php +++ b/src/broadcasting/src/UniqueBroadcastEvent.php @@ -22,7 +22,7 @@ class UniqueBroadcastEvent extends BroadcastEvent /** * The unique lock identifier. */ - public mixed $uniqueId; + public string $uniqueId; /** * The number of seconds the unique lock should be maintained. From f444b3be0a9551f703028698a7a0554b667fd2af Mon Sep 17 00:00:00 2001 From: Yu Shing Date: Fri, 3 Jan 2025 23:08:42 +0800 Subject: [PATCH 30/30] add queue --- src/broadcasting/composer.json | 2 + src/broadcasting/src/BroadcastEvent.php | 14 +-- src/broadcasting/src/BroadcastManager.php | 32 ++--- src/broadcasting/src/UniqueBroadcastEvent.php | 8 +- tests/Broadcasting/BroadcasterTest.php | 29 +++++ .../Broadcasting/BroadcastManagerTest.php | 116 ++++++++++++------ 6 files changed, 135 insertions(+), 66 deletions(-) diff --git a/src/broadcasting/composer.json b/src/broadcasting/composer.json index d38d64fa..d6eecb1d 100644 --- a/src/broadcasting/composer.json +++ b/src/broadcasting/composer.json @@ -35,9 +35,11 @@ "hyperf/redis": "~3.1.0", "hyperf/stringable": "~3.1.0", "swooletw/hyperf-auth": "dev-master", + "swooletw/hyperf-bus": "dev-master", "swooletw/hyperf-cache": "dev-master", "swooletw/hyperf-foundation": "dev-master", "swooletw/hyperf-framework": "dev-master", + "swooletw/hyperf-queue": "dev-master", "swooletw/hyperf-router": "dev-master", "swooletw/hyperf-support": "dev-master", "swooletw/object-pool": "dev-master" diff --git a/src/broadcasting/src/BroadcastEvent.php b/src/broadcasting/src/BroadcastEvent.php index 6b65a8d0..5549498d 100644 --- a/src/broadcasting/src/BroadcastEvent.php +++ b/src/broadcasting/src/BroadcastEvent.php @@ -9,13 +9,12 @@ use ReflectionClass; use ReflectionProperty; use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastingFactory; +use SwooleTW\Hyperf\Bus\Queueable; +use SwooleTW\Hyperf\Queue\Contracts\ShouldQueue; -// TODO: 當 queue 移植過來後補上 -// class BroadcastEvent implements ShouldQueue -class BroadcastEvent +class BroadcastEvent implements ShouldQueue { - // TODO: 當 queue 移植過來後補上 - // use Queueable; + use Queueable; /** * The event instance. @@ -37,11 +36,6 @@ class BroadcastEvent */ public ?int $backoff; - /** - * Indicate that the event should be dispatched after all open database transactions have been committed. - */ - public ?bool $afterCommit; - /** * The maximum number of unhandled exceptions to allow before failing. */ diff --git a/src/broadcasting/src/BroadcastManager.php b/src/broadcasting/src/BroadcastManager.php index 507c9b88..cade8832 100644 --- a/src/broadcasting/src/BroadcastManager.php +++ b/src/broadcasting/src/BroadcastManager.php @@ -24,7 +24,10 @@ use SwooleTW\Hyperf\Broadcasting\Contracts\Broadcaster; use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastingFactoryContract; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBeUnique; -// use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcastNow; +use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcastNow; +use SwooleTW\Hyperf\Bus\Contracts\Dispatcher; +use SwooleTW\Hyperf\Bus\UniqueLock; +use SwooleTW\Hyperf\Cache\Contracts\Factory as Cache; use SwooleTW\Hyperf\ObjectPool\Traits\HasPoolProxy; /** @@ -154,12 +157,12 @@ public function event(mixed $event = null): PendingBroadcast */ public function queue(mixed $event): void { - // TODO: wait bus package - // if ($event instanceof ShouldBroadcastNow - // || (is_object($event) && method_exists($event, 'shouldBroadcastNow') && $event->shouldBroadcastNow()) - // ) { - // return $this->app->get(BusDispatcherContract::class)->dispatchNow(new BroadcastEvent(clone $event)); - // } + if ($event instanceof ShouldBroadcastNow + || (is_object($event) && method_exists($event, 'shouldBroadcastNow') && $event->shouldBroadcastNow()) + ) { + $this->app->get(Dispatcher::class)->dispatchNow(new BroadcastEvent(clone $event)); + return; + } $queue = match (true) { method_exists($event, 'broadcastQueue') => $event->broadcastQueue(), @@ -178,10 +181,9 @@ public function queue(mixed $event): void } } - // TODO: wait queue package - // $this->app->get('queue') - // ->connection($event->connection ?? null) - // ->pushOn($queue, $broadcastEvent); + $this->app->get('queue') + ->connection($event->connection ?? null) + ->pushOn($queue, $broadcastEvent); } /** @@ -189,9 +191,11 @@ public function queue(mixed $event): void */ protected function mustBeUniqueAndCannotAcquireLock(UniqueBroadcastEvent $event): bool { - return false; - // TODO: wait bus package - // return ! (new UniqueLock($event->uniqueVia()))->acquire($event); + return ! (new UniqueLock( + method_exists($event, 'uniqueVia') + ? $event->uniqueVia() + : $this->app->get(Cache::class) + ))->acquire($event); } /** diff --git a/src/broadcasting/src/UniqueBroadcastEvent.php b/src/broadcasting/src/UniqueBroadcastEvent.php index 53d7db60..a1287ce7 100644 --- a/src/broadcasting/src/UniqueBroadcastEvent.php +++ b/src/broadcasting/src/UniqueBroadcastEvent.php @@ -6,13 +6,9 @@ use Psr\Container\ContainerInterface; use SwooleTW\Hyperf\Cache\Contracts\Factory as Cache; +use SwooleTW\Hyperf\Queue\Contracts\ShouldBeUnique; -// TODO: wait queue -// use Illuminate\Contracts\Queue\ShouldBeUnique; - -// TODO: wait queue -// class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique -class UniqueBroadcastEvent extends BroadcastEvent +class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique { /** * The container instance. diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index 4c3cfc52..d7718cb9 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -284,6 +284,35 @@ public function testRetrieveUserDontUseDefaultGuardWhenOneGuardSpecified() $this->broadcaster->retrieveUser('somechannel'); } + public function testRetrieveUserDontUseDefaultGuardWhenMultipleGuardsSpecified() + { + $this->broadcaster->channel('somechannel', function () { + }, ['guards' => ['myguard1', 'myguard2']]); + + $guard = m::mock(Guard::class); + $guard->shouldReceive('user') + ->twice() + ->andReturn(null); + $authManager = m::mock(AuthManager::class); + $authManager->shouldReceive('guard') + ->once() + ->with('myguard1') + ->andReturn($guard); + $authManager->shouldReceive('guard') + ->once() + ->with('myguard2') + ->andReturn($guard); + $authManager->shouldNotReceive('guard') + ->withNoArgs(); + + $this->container->shouldReceive('get') + ->once() + ->with(AuthManager::class) + ->andReturn($authManager); + + $this->broadcaster->retrieveUser('somechannel'); + } + public function testUserAuthenticationWithValidUser() { $this->broadcaster->resolveAuthenticatedUserUsing(function ($request) { diff --git a/tests/Integration/Broadcasting/BroadcastManagerTest.php b/tests/Integration/Broadcasting/BroadcastManagerTest.php index acc379fa..e715167a 100644 --- a/tests/Integration/Broadcasting/BroadcastManagerTest.php +++ b/tests/Integration/Broadcasting/BroadcastManagerTest.php @@ -9,11 +9,25 @@ use Mockery as m; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use SwooleTW\Hyperf\Broadcasting\BroadcastEvent; use SwooleTW\Hyperf\Broadcasting\BroadcastManager; use SwooleTW\Hyperf\Broadcasting\Channel; +use SwooleTW\Hyperf\Broadcasting\Contracts\Factory as BroadcastingFactoryContract; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBeUnique; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcast; use SwooleTW\Hyperf\Broadcasting\Contracts\ShouldBroadcastNow; +use SwooleTW\Hyperf\Broadcasting\UniqueBroadcastEvent; +use SwooleTW\Hyperf\Bus\Contracts\Dispatcher as BusDispatcherContract; +use SwooleTW\Hyperf\Bus\Contracts\QueueingDispatcher; +use SwooleTW\Hyperf\Cache\Contracts\Factory as Cache; +use SwooleTW\Hyperf\Container\DefinitionSource; +use SwooleTW\Hyperf\Foundation\Application; +use SwooleTW\Hyperf\Foundation\ApplicationContext; +use SwooleTW\Hyperf\Queue\Contracts\Factory as QueueFactoryContract; +use SwooleTW\Hyperf\Support\Facades\Broadcast; +use SwooleTW\Hyperf\Support\Facades\Bus; +use SwooleTW\Hyperf\Support\Facades\Facade; +use SwooleTW\Hyperf\Support\Facades\Queue; /** * @internal @@ -21,42 +35,72 @@ */ class BroadcastManagerTest extends TestCase { - // TODO: waiting for queue implementation - // public function testEventCanBeBroadcastNow() - // { - // Bus::fake(); - // Queue::fake(); - // - // Broadcast::queue(new TestEventNow); - // - // Bus::assertDispatched(BroadcastEvent::class); - // Queue::assertNotPushed(BroadcastEvent::class); - // } - // - // public function testEventsCanBeBroadcast() - // { - // Bus::fake(); - // Queue::fake(); - // - // Broadcast::queue(new TestEvent); - // - // Bus::assertNotDispatched(BroadcastEvent::class); - // Queue::assertPushed(BroadcastEvent::class); - // } - // - // public function testUniqueEventsCanBeBroadcast() - // { - // Bus::fake(); - // Queue::fake(); - // - // Broadcast::queue(new TestEventUnique); - // - // Bus::assertNotDispatched(UniqueBroadcastEvent::class); - // Queue::assertPushed(UniqueBroadcastEvent::class); - // - // $lockKey = 'laravel_unique_job:'.UniqueBroadcastEvent::class.':'.TestEventUnique::class; - // $this->assertFalse($this->app->get(Cache::class)->lock($lockKey, 10)->get()); - // } + protected Application $container; + + protected function setUp(): void + { + parent::setUp(); + + $this->container = new Application( + new DefinitionSource([ + BusDispatcherContract::class => fn () => m::mock(QueueingDispatcher::class), + ConfigInterface::class => fn () => m::mock(ConfigInterface::class), + QueueFactoryContract::class => fn () => m::mock(QueueFactoryContract::class), + BroadcastingFactoryContract::class => fn ($container) => new BroadcastManager($container), + ]), + 'bath_path', + ); + + ApplicationContext::setContainer($this->container); + } + + protected function tearDown(): void + { + parent::tearDown(); + + m::close(); + + Facade::clearResolvedInstances(); + } + + public function testEventCanBeBroadcastNow() + { + Bus::fake(); + Queue::fake(); + + Broadcast::queue(new TestEventNow()); + + Bus::assertDispatched(BroadcastEvent::class); + Queue::assertNotPushed(BroadcastEvent::class); + } + + public function testEventsCanBeBroadcast() + { + Bus::fake(); + Queue::fake(); + + Broadcast::queue(new TestEvent()); + + Bus::assertNotDispatched(BroadcastEvent::class); + Queue::assertPushed(BroadcastEvent::class); + } + + public function testUniqueEventsCanBeBroadcast() + { + Bus::fake(); + Queue::fake(); + + $lockKey = 'laravel_unique_job:' . UniqueBroadcastEvent::class . ':' . TestEventUnique::class; + $cache = m::mock(Cache::class); + $cache->shouldReceive('lock')->with($lockKey, 0)->andReturnSelf(); + $cache->shouldReceive('get')->andReturn(true); + $this->container->bind(Cache::class, fn () => $cache); + + Broadcast::queue(new TestEventUnique()); + + Bus::assertNotDispatched(UniqueBroadcastEvent::class); + Queue::assertPushed(UniqueBroadcastEvent::class); + } public function testThrowExceptionWhenUnknownStoreIsUsed() {