diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 888641921..c4a44076a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: max-parallel: 6 matrix: operatingSystem: [ubuntu-latest, windows-latest] - phpVersion: ['7.2', '7.3', '7.4', '8.0'] + phpVersion: ['7.4', '8.0'] fail-fast: false runs-on: ${{ matrix.operatingSystem }} name: ${{ matrix.operatingSystem }} / PHP ${{ matrix.phpVersion }} diff --git a/.gitignore b/.gitignore index 97fa7e856..09358ced8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,12 @@ composer.lock # Other files .DS_Store -php_errors.log \ No newline at end of file +php_errors.log + +#eclipse +/.buildpath +/.project +/.settings/ + +#phpunit +tests/.phpunit.result.cache diff --git a/composer.json b/composer.json index 99a8abaa4..f44a2f1af 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php": ">=7.2.9", + "php": ">=7.4", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -39,12 +39,12 @@ "linkorb/jsmin-php": "~1.0", "wikimedia/less.php": "~3.0", "scssphp/scssphp": "~1.0", - "symfony/yaml": "^4.4", + "symfony/yaml": "^5.1", "twig/twig": "~2.0", "league/csv": "~9.1", "nesbot/carbon": "^2.0", - "laravel/framework": "~6.0", - "laravel/tinker": "~2.0" + "laravel/framework": "9.x-dev", + "laravel/tinker": "dev-develop" }, "require-dev": { "phpunit/phpunit": "^8.5.12|^9.3.3", diff --git a/src/Database/Pivot.php b/src/Database/Pivot.php index 5172b7df2..665c402ef 100644 --- a/src/Database/Pivot.php +++ b/src/Database/Pivot.php @@ -68,10 +68,10 @@ public function __construct(ModelBase $parent, $attributes, $table, $exists = fa /** * Set the keys for a save update query. * - * @param \Illuminate\Database\Eloquent\Builder + * @param \Illuminate\Database\Eloquent\Builder $query * @return \Illuminate\Database\Eloquent\Builder */ - protected function setKeysForSaveQuery(BuilderBase $query) + protected function setKeysForSaveQuery($query) { $query->where($this->foreignKey, $this->getAttribute($this->foreignKey)); diff --git a/src/Database/Query/Grammars/MySqlGrammar.php b/src/Database/Query/Grammars/MySqlGrammar.php index 0b9501089..5964e8b8f 100644 --- a/src/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Database/Query/Grammars/MySqlGrammar.php @@ -1,32 +1,9 @@ compileInsert($query, $values) . ' on duplicate key update '; - - $columns = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = values(' . $this->wrap($value) . ')' - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - return $sql . $columns; - } } diff --git a/src/Database/Query/Grammars/PostgresGrammar.php b/src/Database/Query/Grammars/PostgresGrammar.php index 2b8a167c3..58ad779ec 100644 --- a/src/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Database/Query/Grammars/PostgresGrammar.php @@ -1,34 +1,9 @@ compileInsert($query, $values); - - $sql .= ' on conflict (' . $this->columnize($uniqueBy) . ') do update set '; - - $columns = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = ' . $this->wrapValue('excluded') . '.' . $this->wrap($value) - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - return $sql . $columns; - } } diff --git a/src/Database/Query/Grammars/SQLiteGrammar.php b/src/Database/Query/Grammars/SQLiteGrammar.php index 942612e1f..4cf7fb76f 100644 --- a/src/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Database/Query/Grammars/SQLiteGrammar.php @@ -1,6 +1,5 @@ wrap($as); } - - /** - * Compile an "upsert" statement into SQL. - * - * @param \Winter\Storm\Database\QueryBuilder $query - * @param array $values - * @param array $uniqueBy - * @param array $update - * @return string - */ - public function compileUpsert(QueryBuilder $query, array $values, array $uniqueBy, array $update) - { - $sql = $this->compileInsert($query, $values); - - $sql .= ' on conflict (' . $this->columnize($uniqueBy) . ') do update set '; - - $columns = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = ' . $this->wrapValue('excluded') . '.' . $this->wrap($value) - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - return $sql . $columns; - } } diff --git a/src/Database/Query/Grammars/SqlServerGrammar.php b/src/Database/Query/Grammars/SqlServerGrammar.php index ac70ec53c..17cb26c97 100644 --- a/src/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Database/Query/Grammars/SqlServerGrammar.php @@ -1,52 +1,9 @@ columnize(array_keys(reset($values))); - - $sql = 'merge ' . $this->wrapTable($query->from) . ' '; - - $parameters = collect($values)->map(function ($record) { - return '(' . $this->parameterize($record) . ')'; - })->implode(', '); - - $sql .= 'using (values ' . $parameters . ') ' . $this->wrapTable('laravel_source') . ' (' . $columns . ') '; - - $on = collect($uniqueBy)->map(function ($column) use ($query) { - return $this->wrap('laravel_source.' . $column) . ' = ' . $this->wrap($query->from . '.' . $column); - })->implode(' and '); - - $sql .= 'on ' . $on . ' '; - - if ($update) { - $update = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = ' . $this->wrap('laravel_source.' . $value) - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - $sql .= 'when matched then update set ' . $update . ' '; - } - - $sql .= 'when not matched then insert (' . $columns . ') values (' . $columns . ')'; - - return $sql; - } } diff --git a/src/Database/Traits/Encryptable.php b/src/Database/Traits/Encryptable.php index 4218243f5..7e1844dc4 100644 --- a/src/Database/Traits/Encryptable.php +++ b/src/Database/Traits/Encryptable.php @@ -14,7 +14,7 @@ trait Encryptable /** * @var \Illuminate\Contracts\Encryption\Encrypter Encrypter instance. */ - protected $encrypter; + protected $encrypterInstance; /** * @var array List of original attribute values before they were encrypted. @@ -108,7 +108,7 @@ public function getOriginalEncryptableValue($attribute) */ public function getEncrypter() { - return (!is_null($this->encrypter)) ? $this->encrypter : App::make('encrypter'); + return (!is_null($this->encrypterInstance)) ? $this->encrypterInstance : App::make('encrypter'); } /** @@ -119,6 +119,6 @@ public function getEncrypter() */ public function setEncrypter(\Illuminate\Contracts\Encryption\Encrypter $encrypter) { - $this->encrypter = $encrypter; + $this->encrypterInstance = $encrypter; } } diff --git a/src/Events/Dispatcher.php b/src/Events/Dispatcher.php index 1d55250ca..6c3f118e6 100644 --- a/src/Events/Dispatcher.php +++ b/src/Events/Dispatcher.php @@ -1,10 +1,14 @@ listen($this->firstClosureParameterType($events), $events, $priority); + } elseif ($events instanceof QueuedClosure) { + return $this->listen($this->firstClosureParameterType($events->closure), $events->resolve(), $priority); + } elseif ($listener instanceof QueuedClosure) { + $listener = $listener->resolve(); + } + $listener = Serialisation::wrapClosure($listener); + foreach ((array) $events as $event) { if (Str::contains($event, '*')) { $this->setupWildcardListen($event, $listener); @@ -109,6 +128,9 @@ public function dispatch($event, $payload = [], $halt = false) } foreach ($this->getListeners($event) as $listener) { + if ($listener instanceof SerializableClosure) { + $listener = $listener->getClosure(); + } $response = $listener($event, $payload); // If a response is returned from the listener and event halting is enabled @@ -171,7 +193,7 @@ protected function sortListeners($eventName) // If listeners exist for the given event, we will sort them by the priority // so that we can call them in the correct order. We will cache off these - // sorted event listeners so we do not have to re-sort on every events. + // sorted event listeners so we do not have to re-sort on every event. if (isset($this->listeners[$eventName])) { krsort($this->listeners[$eventName]); diff --git a/src/Extension/ExtendableTrait.php b/src/Extension/ExtendableTrait.php index fa07e73a7..845ccdcee 100644 --- a/src/Extension/ExtendableTrait.php +++ b/src/Extension/ExtendableTrait.php @@ -1,9 +1,11 @@ extensionData['dynamicMethods'][$dynamicName] = $method; + $this->extensionData['dynamicMethods'][$dynamicName] = Serialisation::wrapClosure($method); } /** @@ -418,7 +418,7 @@ public function extendableCall($name, $params = null) $dynamicCallable = $this->extensionData['dynamicMethods'][$name]; if (is_callable($dynamicCallable)) { - return call_user_func_array($dynamicCallable, array_values($params)); + return call_user_func_array(Serialisation::unwrapClosure($dynamicCallable), array_values($params)); } } diff --git a/src/Extension/ExtensionTrait.php b/src/Extension/ExtensionTrait.php index b587d983a..6ea21d30c 100644 --- a/src/Extension/ExtensionTrait.php +++ b/src/Extension/ExtensionTrait.php @@ -1,5 +1,8 @@ make('events')->fire('exception.beforeReport', [$exception], true) === false) { + if (app()->make('events')->fire('exception.beforeReport', [$throwable], true) === false) { return; } - if ($this->shouldntReport($exception)) { + if ($this->shouldntReport($throwable)) { return; } if (class_exists('Log')) { - Log::error($exception); + Log::error($throwable); } /** @@ -73,24 +72,24 @@ public function report(Exception $exception) * * Example usage (performs additional reporting on the exception) * - * Event::listen('exception.report', function (\Exception $exception) { - * app('sentry')->captureException($exception); + * Event::listen('exception.report', function (\Throwable $throwable) { + * app('sentry')->captureException($throwable); * }); */ - app()->make('events')->fire('exception.report', [$exception]); + app()->make('events')->fire('exception.report', [$throwable]); } /** * Render an exception into an HTTP response. * * @param \Illuminate\Http\Request $request - * @param \Exception $exception + * @param \Throwable $throwable * @return \Illuminate\Http\Response */ - public function render($request, Exception $exception) + public function render($request, Throwable $throwable) { - $statusCode = $this->getStatusCode($exception); - $response = $this->callCustomHandlers($exception); + $statusCode = $this->getStatusCode($throwable); + $response = $this->callCustomHandlers($throwable); if (!is_null($response)) { if ($response instanceof \Symfony\Component\HttpFoundation\Response) { @@ -100,25 +99,25 @@ public function render($request, Exception $exception) return Response::make($response, $statusCode); } - if ($event = app()->make('events')->fire('exception.beforeRender', [$exception, $statusCode, $request], true)) { + if ($event = app()->make('events')->fire('exception.beforeRender', [$throwable, $statusCode, $request], true)) { return Response::make($event, $statusCode); } - return parent::render($request, $exception); + return parent::render($request, $throwable); } /** * Checks if the exception implements the HttpExceptionInterface, or returns * as generic 500 error code for a server side error. - * @param \Exception $exception + * @param \Throwable $throwable * @return int */ - protected function getStatusCode($exception) + protected function getStatusCode($throwable) { - if ($exception instanceof HttpExceptionInterface) { - $code = $exception->getStatusCode(); + if ($throwable instanceof HttpExceptionInterface) { + $code = $throwable->getStatusCode(); } - elseif ($exception instanceof AjaxException) { + elseif ($throwable instanceof AjaxException) { $code = 406; } else { @@ -154,32 +153,32 @@ public function error(Closure $callback) } /** - * Handle the given exception. + * Handle the given throwable. * - * @param \Exception $exception + * @param \Throwable $throwable * @param bool $fromConsole * @return void */ - protected function callCustomHandlers($exception, $fromConsole = false) + protected function callCustomHandlers($throwable, $fromConsole = false) { foreach ($this->handlers as $handler) { - // If this exception handler does not handle the given exception, we will just - // go the next one. A handler may type-hint an exception that it handles so + // If this throwable handler does not handle the given throwable, we will just + // go the next one. A handler may type-hint an throwable that it handles so // we can have more granularity on the error handling for the developer. - if (!$this->handlesException($handler, $exception)) { + if (!$this->handlesThrowable($handler, $throwable)) { continue; } - $code = $this->getStatusCode($exception); + $code = $this->getStatusCode($throwable); // We will wrap this handler in a try / catch and avoid white screens of death - // if any exceptions are thrown from a handler itself. This way we will get + // if any throwables are thrown from a handler itself. This way we will get // at least some errors, and avoid errors with no data or not log writes. try { - $response = $handler($exception, $code, $fromConsole); + $response = $handler($throwable, $code, $fromConsole); } - catch (Exception $e) { - $response = $this->convertExceptionToResponse($e); + catch (Throwable $t) { + $response = $this->convertThrowableToResponse($t); } // If this handler returns a "non-null" response, we will return it so it will // get sent back to the browsers. Once the handler returns a valid response @@ -191,34 +190,34 @@ protected function callCustomHandlers($exception, $fromConsole = false) } /** - * Determine if the given handler handles this exception. + * Determine if the given handler handles this throwable. * * @param \Closure $handler - * @param \Exception $exception + * @param \Throwable $throwable * @return bool */ - protected function handlesException(Closure $handler, $exception) + protected function handlesThrowable(Closure $handler, $throwable) { $reflection = new ReflectionFunction($handler); - return $reflection->getNumberOfParameters() == 0 || $this->hints($reflection, $exception); + return $reflection->getNumberOfParameters() == 0 || $this->hints($reflection, $throwable); } /** - * Determine if the given handler type hints the exception. + * Determine if the given handler type hints the throwable. * * @param \ReflectionFunction $reflection - * @param \Exception $exception + * @param \Throwable $throwable * @return bool */ - protected function hints(ReflectionFunction $reflection, $exception) + protected function hints(ReflectionFunction $reflection, $throwable) { $parameters = $reflection->getParameters(); $expected = $parameters[0]; try { return (new ReflectionClass($expected->getType()->getName())) - ->isInstance($exception); - } catch (Throwable $t) { + ->isInstance($throwable); + } catch (\Throwable $t) { return false; } } diff --git a/src/Mail/TransportManager.php b/src/Mail/TransportManager.php index 800542061..ea794f797 100644 --- a/src/Mail/TransportManager.php +++ b/src/Mail/TransportManager.php @@ -1,9 +1,13 @@ getClosure(); + } + return $callable; + } +} diff --git a/src/Support/Traits/Emitter.php b/src/Support/Traits/Emitter.php index c6629e628..be2b98375 100644 --- a/src/Support/Traits/Emitter.php +++ b/src/Support/Traits/Emitter.php @@ -1,5 +1,11 @@ emitterEventCollection[$event][$priority][] = $callback; + if ($event instanceof Closure || $event instanceof QueuedClosure) { + if ($priority === 0 && (is_int($callback) || filter_var($callback, FILTER_VALIDATE_INT))) { + $priority = (int)$callback; + } + } + if ($event instanceof Closure) { + return $this->bindEvent($this->firstClosureParameterType($event), $event, $priority); + } elseif ($event instanceof QueuedClosure) { + return $this->bindEvent($this->firstClosureParameterType($event->closure), $event->resolve(), $priority); + } elseif ($callback instanceof QueuedClosure) { + $callback = $callback->resolve(); + } + $this->emitterEventCollection[$event][$priority][] = Serialisation::wrapClosure($callback); unset($this->emitterEventSorted[$event]); return $this; } /** * Create a new event binding that fires once only + * @param string|Closure|QueuedClosure $event + * @param Closure|null $callback When a Closure or QueuedClosure is provided as the first parameter + * this parameter can be omitted * @return self */ - public function bindEventOnce($event, $callback) + public function bindEventOnce($event, $callback = null) { - $this->emitterSingleEventCollection[$event][] = $callback; + if ($event instanceof Closure) { + return $this->bindEventOnce($this->firstClosureParameterType($event), $event); + } elseif ($event instanceof QueuedClosure) { + return $this->bindEventOnce($this->firstClosureParameterType($event->closure), $event->resolve()); + } elseif ($callback instanceof QueuedClosure) { + $callback = $callback->resolve(); + } + $this->emitterSingleEventCollection[$event][] = Serialisation::wrapClosure($callback); return $this; } @@ -77,6 +110,10 @@ public function unbindEvent($event = null) return; } + if (is_object($event)) { + $event = get_class($event); + } + if ($event === null) { unset($this->emitterSingleEventCollection, $this->emitterEventCollection, $this->emitterEventSorted); return $this; @@ -106,9 +143,11 @@ public function unbindEvent($event = null) */ public function fireEvent($event, $params = [], $halt = false) { - if (!is_array($params)) { - $params = [$params]; - } + // When the given "event" is actually an object we will assume it is an event + // object and use the class as the event name and this event itself as the + // payload to the handler, which makes object based events quite simple. + list($event, $params) = $this->parseEventAndPayload($event, $params); + $result = []; /* @@ -116,7 +155,7 @@ public function fireEvent($event, $params = [], $halt = false) */ if (isset($this->emitterSingleEventCollection[$event])) { foreach ($this->emitterSingleEventCollection[$event] as $callback) { - $response = call_user_func_array($callback, $params); + $response = call_user_func_array(Serialisation::unwrapClosure($callback), $params); if (is_null($response)) { continue; } @@ -138,7 +177,7 @@ public function fireEvent($event, $params = [], $halt = false) } foreach ($this->emitterEventSorted[$event] as $callback) { - $response = call_user_func_array($callback, $params); + $response = call_user_func_array(Serialisation::unwrapClosure($callback), $params); if (is_null($response)) { continue; } @@ -151,4 +190,20 @@ public function fireEvent($event, $params = [], $halt = false) return $halt ? null : $result; } + + /** + * Parse the given event and payload and prepare them for dispatching. + * + * @param mixed $event + * @param mixed $payload + * @return array + */ + protected function parseEventAndPayload($event, $payload = null) + { + if (is_object($event)) { + [$payload, $event] = [[$event], get_class($event)]; + } + + return [$event, Arr::wrap($payload)]; + } } diff --git a/tests/Database/QueryBuilderTest.php b/tests/Database/QueryBuilderTest.php index 67554578e..56d67aedd 100644 --- a/tests/Database/QueryBuilderTest.php +++ b/tests/Database/QueryBuilderTest.php @@ -131,7 +131,7 @@ public function testUpsert() $builder->getConnection() ->expects($this->once()) ->method('affectingStatement') - ->with('merge [users] using (values (?, ?), (?, ?)) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [email] = [laravel_source].[email], [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name])', ['foo', 'bar', 'foo2', 'bar2']) + ->with('merge [users] using (values (?, ?), (?, ?)) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [email] = [laravel_source].[email], [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name]);', ['foo', 'bar', 'foo2', 'bar2']) ->willReturn(2); $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email'); $this->assertEquals(2, $result); @@ -174,7 +174,7 @@ public function testUpsertWithUpdateColumns() $builder->getConnection() ->expects($this->once()) ->method('affectingStatement') - ->with('merge [users] using (values (?, ?), (?, ?)) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name])', ['foo', 'bar', 'foo2', 'bar2']) + ->with('merge [users] using (values (?, ?), (?, ?)) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name]);', ['foo', 'bar', 'foo2', 'bar2']) ->willReturn(2); $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']); $this->assertEquals(2, $result); @@ -210,9 +210,7 @@ protected function getConnection($connection = null) 'rollBack', 'transactionLevel', 'pretend', - ]) - ->addMethods([ - 'getDatabaseName', + 'getDatabaseName' ]) ->getMock(); diff --git a/tests/Database/RelationsTest.php b/tests/Database/RelationsTest.php index 93a1a854e..81a8914eb 100644 --- a/tests/Database/RelationsTest.php +++ b/tests/Database/RelationsTest.php @@ -262,7 +262,7 @@ protected function seedTables() } } -class Category extends \October\Rain\Database\Model +class Category extends \Winter\Storm\Database\Model { public $table = 'categories'; diff --git a/tests/Events/DispatcherTest.php b/tests/Events/DispatcherTest.php new file mode 100644 index 000000000..c48005a42 --- /dev/null +++ b/tests/Events/DispatcherTest.php @@ -0,0 +1,134 @@ +listen('test.test', function () use (&$magic_value) { + $magic_value = true; + }); + $dispatcher->fire('test.test'); + $this->assertTrue($magic_value); + } + + /** + * Test closure usage + */ + public function testTypedClosureListen() + { + $magic_value = false; + $dispatcher = new Dispatcher(); + $dispatcher->listen(function (EventTest $event) use (&$magic_value) { + $magic_value = true; + }); + $dispatcher->dispatch('test.test'); + $this->assertFalse($magic_value); + $dispatcher->dispatch(new EventTest); + $this->assertTrue($magic_value); + } + + public function testStringEventPriorities() + { + $magic_value = 0; + $dispatcher = new Dispatcher(); + + $dispatcher->listen("test.test", function () use (&$magic_value) { + $magic_value = 42; + }, 1); + $dispatcher->listen("test.test", function () use (&$magic_value) { + $magic_value = 1; + }, 2); + + $dispatcher->dispatch("test.test"); + $this->assertEquals(42, $magic_value); + } + + public function testClosurePriorities() + { + $magic_value = 0; + $dispatcher = new Dispatcher(); + + $dispatcher->listen(function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }, 1); + $dispatcher->listen(function (EventTest $test) use (&$magic_value) { + $magic_value = 1; + }, 2); + + $dispatcher->dispatch(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + public function testQueuedClosurePriorities() + { + $mock_queued_closure_should_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }; + $mock_queued_closure_should_match->method('resolve')->willReturn($mock_queued_closure_should_match->closure); + + $mock_queued_closure_should_not_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_not_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 2; + }; + $mock_queued_closure_should_not_match->method('resolve')->willReturn($mock_queued_closure_should_not_match->closure); + $dispatcher = new Dispatcher(); + $magic_value = 0; + + // Test natural sorting without priority to the queued tasks to be queued. + $dispatcher->listen($mock_queued_closure_should_not_match); + $dispatcher->listen($mock_queued_closure_should_match); + $dispatcher->dispatch(new EventTest()); + $this->assertEquals(42, $magic_value); + + // Test priority sorting for the queued tasks to be queued + $magic_value = 0; + $dispatcher->listen($mock_queued_closure_should_match, 1); + $dispatcher->listen($mock_queued_closure_should_not_match, 2); + $dispatcher->dispatch(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + /** + * Test whether the dispatcher accepts a QueuedClosure + */ + public function testQueuedClosureListen() + { + $magic_value = false; + $mock_queued_closure = $this->createMock(QueuedClosure::class); + $mock_queued_closure->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = true; + }; + $mock_queued_closure->method('resolve')->willReturn($mock_queued_closure->closure); + $dispatcher = new Dispatcher(); + $dispatcher->listen($mock_queued_closure); + $dispatcher->dispatch(new EventTest()); + $this->assertTrue($magic_value); + } +} diff --git a/tests/Extension/ExtendableTest.php b/tests/Extension/ExtendableTest.php index dfbe93227..66d8bca09 100644 --- a/tests/Extension/ExtendableTest.php +++ b/tests/Extension/ExtendableTest.php @@ -149,7 +149,7 @@ public function testInvalidImplementValue() { $this->expectException(Exception::class); $this->expectExceptionMessage('Class ExtendableTestInvalidExtendableClass contains an invalid $implement value'); - + $result = new ExtendableTestInvalidExtendableClass; } @@ -216,6 +216,35 @@ public function testGetClassMethods() $this->assertContains('getFooAnotherWay', $methods); $this->assertNotContains('missingFunction', $methods); } + + public function testClosureSerialisation() + { + $test_string = 'hello world'; + BasicExtendable::extend(function (BasicExtendable $class) use ($test_string) { + $class->addDynamicMethod('foobar', function () use ($test_string) { + $x = function () use ($test_string) { + return $test_string; + }; + return $x(); + }); + $class->addDynamicMethod('bazbal', function () use ($test_string) { + return function () use ($test_string) { + return $test_string; + }; + }); + }); + + $subject = new BasicExtendable(); + + $serialized = serialize($subject); + + $unserialized = unserialize($serialized); + + $this->assertEquals($test_string, $unserialized->foobar()); + $test = $unserialized->bazbal(); + $this->assertInstanceOf(Closure::class, $test); + $this->assertEquals($test(), $test_string); + } } // @@ -308,6 +337,10 @@ public static function getName() } } +class BasicExtendable extends Extendable +{ +} + /* * Example class with soft implement failure */ diff --git a/tests/Mail/MailerTest.php b/tests/Mail/MailerTest.php index a12de01f1..d166fc3c5 100644 --- a/tests/Mail/MailerTest.php +++ b/tests/Mail/MailerTest.php @@ -101,7 +101,7 @@ public function testProcessRecipients() protected function makeMailer() { - return new Mailer(new FactoryMailerTest, new SwiftMailerTest, new DispatcherMailerTest); + return new Mailer("TestMailer", new FactoryMailerTest, new SwiftMailerTest, new DispatcherMailerTest); } } diff --git a/tests/Support/EmitterTest.php b/tests/Support/EmitterTest.php index d01947cb3..098a138a0 100644 --- a/tests/Support/EmitterTest.php +++ b/tests/Support/EmitterTest.php @@ -1,5 +1,7 @@ assertEquals('the quick brown fox jumped over the lazy dog', $result); } + + /** + * Test closure usage + */ + public function testTypedClosureListen() + { + $magic_value = false; + $dispatcher = $this->traitObject; + $dispatcher->bindEvent(function (EventTest $event) use (&$magic_value) { + $magic_value = true; + }); + $dispatcher->fireEvent('test.test'); + $this->assertFalse($magic_value); + $dispatcher->fireEvent(new EventTest); + $this->assertTrue($magic_value); + } + + public function testClosurePriorities() + { + $magic_value = 0; + $dispatcher = $this->traitObject; + + $dispatcher->bindEvent(function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }, 1); + $dispatcher->bindEvent(function (EventTest $test) use (&$magic_value) { + $magic_value = 1; + }, 2); + + $dispatcher->fireEvent(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + public function testQueuedClosurePriorities() + { + $mock_queued_closure_should_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }; + $mock_queued_closure_should_match->method('resolve')->willReturn($mock_queued_closure_should_match->closure); + + $mock_queued_closure_should_not_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_not_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 2; + }; + $mock_queued_closure_should_not_match->method('resolve')->willReturn($mock_queued_closure_should_not_match->closure); + $dispatcher = $this->traitObject; + $magic_value = 0; + + // Test natural sorting without priority to the queued tasks to be queued. + $dispatcher->bindEvent($mock_queued_closure_should_not_match); + $dispatcher->bindEvent($mock_queued_closure_should_match); + $dispatcher->fireEvent(new EventTest()); + $this->assertEquals(42, $magic_value); + + // Test priority sorting for the queued tasks to be queued + $magic_value = 0; + $dispatcher->bindEvent($mock_queued_closure_should_match, 1); + $dispatcher->bindEvent($mock_queued_closure_should_not_match, 2); + $dispatcher->fireEvent(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + /** + * Test whether the Emitter accepts a QueuedClosure + */ + public function testQueuedClosureListen() + { + $magic_value = false; + $mock_queued_closure = $this->createMock(QueuedClosure::class); + $mock_queued_closure->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = true; + }; + $mock_queued_closure->method('resolve')->willReturn($mock_queued_closure->closure); + $dispatcher = $this->traitObject; + $dispatcher->bindEvent($mock_queued_closure); + $dispatcher->fireEvent(new EventTest()); + $this->assertTrue($magic_value); + } + + public function testClosureSerialisation() + { + $emitter = new EmitterClass(); + $test = 'foobar'; + $emitter->bindEvent($test, function () use ($test) { + EmitterClass::$output = $test; + }); + $emitter->bindEvent(function (EventTest $event) use ($test) { + EmitterClass::$output = $test.$test; + }); + $serialized = serialize($emitter); + $unserialized = unserialize($serialized); + $unserialized->fireEvent($test); + $this->assertEquals($test, EmitterClass::$output); + + $unserialized->fireEvent(new EventTest()); + $this->assertEquals($test.$test, EmitterClass::$output); + } + + public function testNestedClosureSerialisation() + { + $emitter = new EmitterClass(); + $test = 'foobar'; + $emitter->bindEvent($test, function () use ($test) { + EmitterClass::$output = function () use ($test) { + return $test; + }; + }); + + $serialized = serialize($emitter); + $unserialized = unserialize($serialized); + $unserialized->fireEvent($test); + + $closure = EmitterClass::$output; + $this->assertInstanceOf(Closure::class, $closure); + $this->assertEquals($test, $closure()); + } +} +class EmitterClass +{ + use \Winter\Storm\Support\Traits\Emitter; + + /** + * @var string $output used for keeping a testable variable as references don't survive serialisation + */ + public static $output; } diff --git a/tests/Support/ExtensionAndEmitterSerialisationTest.php b/tests/Support/ExtensionAndEmitterSerialisationTest.php new file mode 100644 index 000000000..62cdd1577 --- /dev/null +++ b/tests/Support/ExtensionAndEmitterSerialisationTest.php @@ -0,0 +1,34 @@ +bindEvent($test, function () use ($test) { + ExtendableEmitter::$output = $test; + }); + }); + $instance = new ExtendableEmitter(); + $serialized = serialize($instance); + $unserialized = unserialize($serialized); + $unserialized->fireEvent($test); + $this->assertEquals($test, ExtendableEmitter::$output); + } +} + +class ExtendableEmitter extends Extendable +{ + use \Winter\Storm\Support\Traits\Emitter; + + /** + * @var string $output used for keeping a testable variable as references don't survive serialisation + */ + public static $output; +} diff --git a/tests/fixtures/events/EventTest.php b/tests/fixtures/events/EventTest.php new file mode 100644 index 000000000..91b016959 --- /dev/null +++ b/tests/fixtures/events/EventTest.php @@ -0,0 +1,5 @@ +