diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5393c452..31f5bfdb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+# Release Notes for 0.7.x
+
+## [v0.7.0 (2025-09-26)](https://github.com/phenixphp/framework/compare/0.6.0...0.7.0)
+
+### Added
+
+- Event system. ([#67](https://github.com/phenixphp/framework/pull/67))
+
# Release Notes for 0.6.x
## [v0.6.0 (2025-08-22)](https://github.com/phenixphp/framework/compare/0.5.2...0.6.0)
diff --git a/src/App.php b/src/App.php
index 780b049e..95ded4c7 100644
--- a/src/App.php
+++ b/src/App.php
@@ -12,6 +12,8 @@
use Amp\Socket;
use League\Container\Container;
use League\Uri\Uri;
+use Mockery\LegacyMockInterface;
+use Mockery\MockInterface;
use Monolog\Logger;
use Phenix\Console\Phenix;
use Phenix\Contracts\App as AppContract;
@@ -105,6 +107,11 @@ public static function make(string $key): object
return self::$container->get($key);
}
+ public static function fake(string $key, LegacyMockInterface|MockInterface $concrete): void
+ {
+ self::$container->extend($key)->setConcrete($concrete);
+ }
+
public static function path(): string
{
return self::$path;
diff --git a/src/Console/Commands/MakeModel.php b/src/Console/Commands/MakeModel.php
index 069377b6..3ff88f0d 100644
--- a/src/Console/Commands/MakeModel.php
+++ b/src/Console/Commands/MakeModel.php
@@ -102,7 +102,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
File::put($filePath, $stub);
- $output->writeln(["{$this->commonName()} successfully generated!", self::EMPTY_LINE]);
+ $outputPath = str_replace(base_path(), '', $filePath);
+
+ $output->writeln(["{$this->commonName()} [{$outputPath}] successfully generated!", self::EMPTY_LINE]);
return parent::SUCCESS;
}
diff --git a/src/Console/Maker.php b/src/Console/Maker.php
index 14a90479..b9d8b03e 100644
--- a/src/Console/Maker.php
+++ b/src/Console/Maker.php
@@ -48,7 +48,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
File::put($filePath, $stub);
- $output->writeln(["{$this->commonName()} successfully generated!", self::EMPTY_LINE]);
+ $outputPath = str_replace(base_path(), '', $filePath);
+
+ $output->writeln(["{$this->commonName()} [{$outputPath}] successfully generated!", self::EMPTY_LINE]);
return Command::SUCCESS;
}
diff --git a/src/Database/Console/MakeSeeder.php b/src/Database/Console/MakeSeeder.php
index 89b603a2..990e38f2 100644
--- a/src/Database/Console/MakeSeeder.php
+++ b/src/Database/Console/MakeSeeder.php
@@ -36,7 +36,7 @@ protected function configure(): void
protected function outputDirectory(): string
{
- return 'database' . DIRECTORY_SEPARATOR . 'seeds';
+ return 'database' . DIRECTORY_SEPARATOR . 'seeders';
}
protected function stub(): string
diff --git a/src/Events/AbstractEvent.php b/src/Events/AbstractEvent.php
new file mode 100644
index 00000000..04f1fcf9
--- /dev/null
+++ b/src/Events/AbstractEvent.php
@@ -0,0 +1,34 @@
+payload;
+ }
+
+ public function isPropagationStopped(): bool
+ {
+ return $this->propagationStopped;
+ }
+
+ public function stopPropagation(): void
+ {
+ $this->propagationStopped = true;
+ }
+}
diff --git a/src/Events/AbstractListener.php b/src/Events/AbstractListener.php
new file mode 100644
index 00000000..9314fbfe
--- /dev/null
+++ b/src/Events/AbstractListener.php
@@ -0,0 +1,57 @@
+priority = $this->normalizePriority($priority);
+
+ return $this;
+ }
+
+ public function getPriority(): int
+ {
+ return $this->priority;
+ }
+
+ public function shouldHandle(Event $event): bool
+ {
+ return true;
+ }
+
+ public function isOnce(): bool
+ {
+ return $this->once;
+ }
+
+ public function setOnce(bool $once = true): self
+ {
+ $this->once = $once;
+
+ return $this;
+ }
+
+ public function getHandler(): Closure|static|string
+ {
+ return $this;
+ }
+
+ protected function normalizePriority(int $priority): int
+ {
+ return max(0, min($priority, 100));
+ }
+}
diff --git a/src/Events/Console/MakeEvent.php b/src/Events/Console/MakeEvent.php
new file mode 100644
index 00000000..a7bbf2d3
--- /dev/null
+++ b/src/Events/Console/MakeEvent.php
@@ -0,0 +1,39 @@
+addArgument('name', InputArgument::REQUIRED, 'The name of the event');
+
+ $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create event');
+ }
+
+ protected function outputDirectory(): string
+ {
+ return 'app' . DIRECTORY_SEPARATOR . 'Events';
+ }
+
+ protected function commonName(): string
+ {
+ return 'Event';
+ }
+
+ protected function stub(): string
+ {
+ return 'event.stub';
+ }
+}
diff --git a/src/Events/Console/MakeListener.php b/src/Events/Console/MakeListener.php
new file mode 100644
index 00000000..fbdbc8c2
--- /dev/null
+++ b/src/Events/Console/MakeListener.php
@@ -0,0 +1,39 @@
+addArgument('name', InputArgument::REQUIRED, 'The name of the listener');
+
+ $this->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to create listener');
+ }
+
+ protected function outputDirectory(): string
+ {
+ return 'app' . DIRECTORY_SEPARATOR . 'Listeners';
+ }
+
+ protected function commonName(): string
+ {
+ return 'Event Listener';
+ }
+
+ protected function stub(): string
+ {
+ return 'listener.stub';
+ }
+}
diff --git a/src/Events/Contracts/Event.php b/src/Events/Contracts/Event.php
new file mode 100644
index 00000000..b8428e87
--- /dev/null
+++ b/src/Events/Contracts/Event.php
@@ -0,0 +1,16 @@
+payload = $payload;
+ $this->timestamp = microtime(true);
+ }
+
+ public function getTimestamp(): float
+ {
+ return $this->timestamp;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+}
diff --git a/src/Events/EventEmitter.php b/src/Events/EventEmitter.php
new file mode 100644
index 00000000..0b383f43
--- /dev/null
+++ b/src/Events/EventEmitter.php
@@ -0,0 +1,310 @@
+>
+ */
+ protected array $listeners = [];
+
+ /**
+ * @var array
+ */
+ protected array $listenerCounts = [];
+
+ /**
+ * Maximum number of listeners per event.
+ */
+ protected int $maxListeners = 10;
+
+ /**
+ * Whether to emit warnings for too many listeners.
+ */
+ protected bool $emitWarnings = true;
+
+ public function on(string $event, Closure|EventListenerContract|string $listener, int $priority = 0): void
+ {
+ $eventListener = $this->createEventListener($listener, $priority);
+
+ $this->listeners[$event][] = $eventListener;
+ $this->listenerCounts[$event] = ($this->listenerCounts[$event] ?? 0) + 1;
+
+ $this->sortListenersByPriority($event);
+ $this->checkMaxListeners($event);
+ }
+
+ public function once(string $event, Closure|EventListenerContract|string $listener, int $priority = 0): void
+ {
+ $eventListener = $this->createEventListener($listener, $priority);
+ $eventListener->setOnce(true);
+
+ $this->listeners[$event][] = $eventListener;
+ $this->listenerCounts[$event] = ($this->listenerCounts[$event] ?? 0) + 1;
+
+ $this->sortListenersByPriority($event);
+ $this->checkMaxListeners($event);
+ }
+
+ public function off(string $event, Closure|EventListenerContract|string|null $listener = null): void
+ {
+ if (! isset($this->listeners[$event])) {
+ return;
+ }
+
+ if ($listener === null) {
+ unset($this->listeners[$event]);
+ $this->listenerCounts[$event] = 0;
+
+ return;
+ }
+
+ $this->listeners[$event] = array_filter(
+ $this->listeners[$event],
+ fn (EventListenerContract $eventListener): bool => ! $this->isSameListener($eventListener, $listener)
+ );
+
+ $this->listenerCounts[$event] = count($this->listeners[$event]);
+
+ if ($this->listenerCounts[$event] === 0) {
+ unset($this->listeners[$event]);
+ }
+ }
+
+ public function emit(string|EventContract $event, mixed $payload = null): array
+ {
+ $eventObject = $this->createEvent($event, $payload);
+ $results = [];
+
+ $listeners = $this->getListeners($eventObject->getName());
+
+ foreach ($listeners as $listener) {
+ if ($eventObject->isPropagationStopped()) {
+ break;
+ }
+
+ if (! $listener->shouldHandle($eventObject)) {
+ continue;
+ }
+
+ try {
+ $result = $listener->handle($eventObject);
+ $results[] = $result;
+
+ // Remove one-time listeners after execution
+ if ($listener->isOnce()) {
+ $this->removeListener($eventObject->getName(), $listener);
+ }
+ } catch (Throwable $e) {
+ Log::error('Event listener error', [
+ 'event' => $eventObject->getName(),
+ 'error' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]);
+
+ if ($this->emitWarnings) {
+ throw new EventException(
+ "Error in event listener for '{$eventObject->getName()}': {$e->getMessage()}",
+ 0,
+ $e
+ );
+ }
+ }
+ }
+
+ return $results;
+ }
+
+ public function emitAsync(string|EventContract $event, mixed $payload = null): Future
+ {
+ return async(function () use ($event, $payload): array {
+ $eventObject = $this->createEvent($event, $payload);
+ $listeners = $this->getListeners($eventObject->getName());
+ $futures = [];
+
+ foreach ($listeners as $listener) {
+ if (! $listener->shouldHandle($eventObject)) {
+ continue;
+ }
+
+ $futures[] = $this->handleListenerAsync($listener, $eventObject);
+ }
+
+ $results = [];
+
+ foreach ($futures as $future) {
+ try {
+ $results[] = $future->await();
+ } catch (Throwable $e) {
+ Log::error('Future await error', [
+ 'event' => $eventObject->getName(),
+ 'error' => $e->getMessage(),
+ ]);
+
+ $results[] = null;
+ }
+ }
+
+ return $results;
+ });
+ }
+
+ protected function handleListenerAsync(EventListenerContract $listener, EventContract $eventObject): Future
+ {
+ return async(function () use ($listener, $eventObject): mixed {
+ try {
+ if ($eventObject->isPropagationStopped()) {
+ return null;
+ }
+
+ $result = $listener->handle($eventObject);
+
+ // Remove one-time listeners after execution
+ if ($listener->isOnce()) {
+ $this->removeListener($eventObject->getName(), $listener);
+ }
+
+ return $result;
+ } catch (Throwable $e) {
+ Log::error('Async event listener error', [
+ 'event' => $eventObject->getName(),
+ 'error' => $e->getMessage(),
+ 'file' => $e->getFile(),
+ 'line' => $e->getLine(),
+ ]);
+
+ if ($this->emitWarnings) {
+ throw new EventException(
+ "Error in async event listener for '{$eventObject->getName()}': {$e->getMessage()}",
+ 0,
+ $e
+ );
+ }
+
+ return null;
+ }
+ });
+ }
+
+ /**
+ * @return array
+ */
+ public function getListeners(string $event): array
+ {
+ return $this->listeners[$event] ?? [];
+ }
+
+ public function hasListeners(string $event): bool
+ {
+ return isset($this->listeners[$event]) && count($this->listeners[$event]) > 0;
+ }
+
+ public function removeAllListeners(): void
+ {
+ $this->listeners = [];
+ $this->listenerCounts = [];
+ }
+
+ public function setMaxListeners(int $maxListeners): void
+ {
+ $this->maxListeners = $maxListeners;
+ }
+
+ public function getMaxListeners(): int
+ {
+ return $this->maxListeners;
+ }
+
+ public function setEmitWarnings(bool $emitWarnings): void
+ {
+ $this->emitWarnings = $emitWarnings;
+ }
+
+ public function getListenerCount(string $event): int
+ {
+ return $this->listenerCounts[$event] ?? 0;
+ }
+
+ public function getEventNames(): array
+ {
+ return array_keys($this->listeners);
+ }
+
+ protected function createEventListener(Closure|EventListenerContract|string $listener, int $priority): EventListenerContract
+ {
+ if ($listener instanceof EventListenerContract) {
+ return $listener;
+ }
+
+ return new EventListener($listener, $priority);
+ }
+
+ protected function createEvent(string|EventContract $event, mixed $payload): EventContract
+ {
+ if ($event instanceof EventContract) {
+ return $event;
+ }
+
+ return new Event($event, $payload);
+ }
+
+ protected function sortListenersByPriority(string $event): void
+ {
+ usort($this->listeners[$event], function (EventListenerContract $a, EventListenerContract $b): int {
+ return $b->getPriority() <=> $a->getPriority();
+ });
+ }
+
+ protected function checkMaxListeners(string $event): void
+ {
+ if (! $this->emitWarnings) {
+ return;
+ }
+
+ $count = $this->getListenerCount($event);
+
+ if ($count > $this->maxListeners) {
+ Log::warning("Possible memory leak detected. Event '{$event}' has {$count} listeners. Maximum is {$this->maxListeners}.");
+ }
+ }
+
+ protected function isSameListener(EventListenerContract $eventListener, Closure|EventListenerContract|string $listener): bool
+ {
+ $handler = $eventListener->getHandler();
+
+ if ($listener instanceof EventListenerContract) {
+ return $eventListener::class === $listener::class;
+ }
+
+ return $handler === $listener;
+ }
+
+ protected function removeListener(string $event, EventListenerContract $listener): void
+ {
+ $this->listeners[$event] = array_filter(
+ $this->listeners[$event],
+ fn (EventListenerContract $eventListener): bool => ! $this->isSameListener($eventListener, $listener)
+ );
+
+ $this->listenerCounts[$event] = count($this->listeners[$event]);
+
+ if ($this->listenerCounts[$event] === 0) {
+ unset($this->listeners[$event]);
+ }
+ }
+}
diff --git a/src/Events/EventListener.php b/src/Events/EventListener.php
new file mode 100644
index 00000000..8e30ed5a
--- /dev/null
+++ b/src/Events/EventListener.php
@@ -0,0 +1,49 @@
+priority = $this->normalizePriority($priority);
+ }
+
+ public function handle(Event $event): mixed
+ {
+ if ($this->handler instanceof Closure) {
+ return ($this->handler)($event);
+ }
+
+ $listener = $this->resolveListener();
+
+ if (! $listener || ! (method_exists($listener, 'handle') || is_callable($listener))) {
+ return null;
+ }
+
+ return method_exists($listener, 'handle') ? $listener->handle($event) : $listener($event);
+ }
+
+ public function getHandler(): Closure|string
+ {
+ return $this->handler;
+ }
+
+ private function resolveListener(): object|null
+ {
+ return class_exists($this->handler)
+ ? new $this->handler()
+ : null;
+ }
+}
diff --git a/src/Events/EventServiceProvider.php b/src/Events/EventServiceProvider.php
new file mode 100644
index 00000000..07b72665
--- /dev/null
+++ b/src/Events/EventServiceProvider.php
@@ -0,0 +1,37 @@
+provides);
+ }
+
+ public function register(): void
+ {
+ $this->getContainer()->addShared(EventEmitter::class, EventEmitter::class);
+ $this->getContainer()->add(EventEmitterContract::class, EventEmitter::class);
+ }
+
+ public function boot(): void
+ {
+ $this->commands([
+ MakeEvent::class,
+ MakeListener::class,
+ ]);
+ }
+}
diff --git a/src/Events/Exceptions/EventException.php b/src/Events/Exceptions/EventException.php
new file mode 100644
index 00000000..47ef792c
--- /dev/null
+++ b/src/Events/Exceptions/EventException.php
@@ -0,0 +1,12 @@
+shouldAllowMockingProtectedMethods()->makePartial();
+
+ App::fake(self::getKeyName(), $mock);
+
+ return $mock->shouldReceive($method);
+ }
}
diff --git a/src/stubs/controller.stub b/src/stubs/controller.stub
index fca5395a..2d455916 100644
--- a/src/stubs/controller.stub
+++ b/src/stubs/controller.stub
@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace {namespace};
+use Phenix\Http\Constants\HttpStatus;
+use Phenix\Http\Controller;
use Phenix\Http\Request;
use Phenix\Http\Response;
-use Amp\Http\Status;
-use Phenix\Http\Controller;
class {name} extends Controller
{
diff --git a/src/stubs/event.stub b/src/stubs/event.stub
new file mode 100644
index 00000000..132a05d5
--- /dev/null
+++ b/src/stubs/event.stub
@@ -0,0 +1,12 @@
+phenix('make:collection', [
- 'name' => 'User',
+ 'name' => 'UserCollection',
]);
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Collection successfully generated!');
+ expect($command->getDisplay())->toContain('Collection [app/Collections/UserCollection.php] successfully generated!');
});
it('does not create the collection because it already exists', function () {
@@ -72,13 +72,13 @@
/** @var CommandTester $command */
$command = $this->phenix('make:collection', [
- 'name' => 'User',
+ 'name' => 'UserCollection',
'--force' => true,
]);
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Collection successfully generated!');
+ expect($command->getDisplay())->toContain('Collection [app/Collections/UserCollection.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -101,5 +101,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Collection successfully generated!');
+ expect($command->getDisplay())->toContain('Collection [app/Collections/Admin/User.php] successfully generated!');
});
diff --git a/tests/Unit/Console/MakeControllerCommandTest.php b/tests/Unit/Console/MakeControllerCommandTest.php
index ed86ae8c..e2ed621f 100644
--- a/tests/Unit/Console/MakeControllerCommandTest.php
+++ b/tests/Unit/Console/MakeControllerCommandTest.php
@@ -24,7 +24,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Controller successfully generated!');
+ expect($command->getDisplay())->toContain('Controller [app/Http/Controllers/TestController.php] successfully generated!');
});
it('does not create the controller because it already exists', function () {
@@ -75,7 +75,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Controller successfully generated!');
+ expect($command->getDisplay())->toContain('Controller [app/Http/Controllers/TestController.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -98,7 +98,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Controller successfully generated!');
+ expect($command->getDisplay())->toContain('Controller [app/Http/Controllers/Admin/UserController.php] successfully generated!');
});
it('creates controller successfully with api option', function () {
@@ -124,6 +124,6 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Controller successfully generated!');
+ expect($command->getDisplay())->toContain('Controller [app/Http/Controllers/TestController.php] successfully generated!');
expect(file_get_contents($tempPath))->toContain('Hello, world!');
});
diff --git a/tests/Unit/Console/MakeMiddlewareCommandTest.php b/tests/Unit/Console/MakeMiddlewareCommandTest.php
index bed1025d..8e9345a8 100644
--- a/tests/Unit/Console/MakeMiddlewareCommandTest.php
+++ b/tests/Unit/Console/MakeMiddlewareCommandTest.php
@@ -28,7 +28,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Middleware successfully generated!');
+ expect($command->getDisplay())->toContain('Middleware [app/Http/Middleware/AwesomeMiddleware.php] successfully generated!');
});
it('does not create the middleware because it already exists', function () {
@@ -79,7 +79,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Middleware successfully generated!');
+ expect($command->getDisplay())->toContain('Middleware [app/Http/Middleware/TestMiddleware.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -106,5 +106,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Middleware successfully generated!');
+ expect($command->getDisplay())->toContain('Middleware [app/Http/Middleware/Admin/TestMiddleware.php] successfully generated!');
});
diff --git a/tests/Unit/Console/MakeModelCommandTest.php b/tests/Unit/Console/MakeModelCommandTest.php
index 26981e4b..1bde3f09 100644
--- a/tests/Unit/Console/MakeModelCommandTest.php
+++ b/tests/Unit/Console/MakeModelCommandTest.php
@@ -27,7 +27,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/User.php] successfully generated!');
});
it('does not create the model because it already exists', function () {
@@ -78,7 +78,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/User.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -101,7 +101,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/Admin/User.php] successfully generated!');
});
it('creates model with custom collection', function () {
@@ -139,8 +139,8 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
- expect($command->getDisplay())->toContain('Collection successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/User.php] successfully generated!');
+ expect($command->getDisplay())->toContain('Collection [app/Collections/UserCollection.php] successfully generated!');
});
it('creates model with custom query builder', function () {
@@ -178,8 +178,8 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
- expect($command->getDisplay())->toContain('Query successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/User.php] successfully generated!');
+ expect($command->getDisplay())->toContain('Query [app/Queries/UserQuery.php] successfully generated!');
});
it('creates model with all', function () {
@@ -223,9 +223,9 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
- expect($command->getDisplay())->toContain('Query successfully generated!');
- expect($command->getDisplay())->toContain('Collection successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/User.php] successfully generated!');
+ expect($command->getDisplay())->toContain('Query [app/Queries/UserQuery.php] successfully generated!');
+ expect($command->getDisplay())->toContain('Collection [app/Collections/UserCollection.php] successfully generated!');
});
it('creates model with migration', function () {
@@ -261,8 +261,9 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
- expect($command->getDisplay())->toContain('Migration successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/User.php] successfully generated!');
+ expect($command->getDisplay())->toContain('Migration [database/migrations/');
+ expect($command->getDisplay())->toContain('_create_users_table.php] successfully generated!');
});
it('creates model with controller', function () {
@@ -298,6 +299,6 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Model successfully generated!');
- expect($command->getDisplay())->toContain('Controller successfully generated!');
+ expect($command->getDisplay())->toContain('Model [app/Models/User.php] successfully generated!');
+ expect($command->getDisplay())->toContain('Controller [app/Http/Controllers/UserController.php] successfully generated!');
});
diff --git a/tests/Unit/Console/MakeQueryCommandTest.php b/tests/Unit/Console/MakeQueryCommandTest.php
index 16b4399c..4ca9ba42 100644
--- a/tests/Unit/Console/MakeQueryCommandTest.php
+++ b/tests/Unit/Console/MakeQueryCommandTest.php
@@ -27,7 +27,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Query successfully generated!');
+ expect($command->getDisplay())->toContain('Query [app/Queries/UserQuery.php] successfully generated!');
});
it('does not create the query because it already exists', function () {
@@ -78,7 +78,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Query successfully generated!');
+ expect($command->getDisplay())->toContain('Query [app/Queries/UserQuery.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -101,5 +101,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Query successfully generated!');
+ expect($command->getDisplay())->toContain('Query [app/Queries/Domain/UserQuery.php] successfully generated!');
});
diff --git a/tests/Unit/Console/MakeRequestCommandTest.php b/tests/Unit/Console/MakeRequestCommandTest.php
index 9e41f65a..27faaeab 100644
--- a/tests/Unit/Console/MakeRequestCommandTest.php
+++ b/tests/Unit/Console/MakeRequestCommandTest.php
@@ -28,7 +28,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Request successfully generated!');
+ expect($command->getDisplay())->toContain('Request [app/Http/Requests/StoreUserRequest.php] successfully generated!');
});
it('does not create the form request because it already exists', function () {
@@ -79,7 +79,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Request successfully generated!');
+ expect($command->getDisplay())->toContain('Request [app/Http/Requests/StoreUserRequest.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -106,5 +106,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Request successfully generated!');
+ expect($command->getDisplay())->toContain('Request [app/Http/Requests/Admin/StoreUserRequest.php] successfully generated!');
});
diff --git a/tests/Unit/Console/MakeServiceProviderCommandTest.php b/tests/Unit/Console/MakeServiceProviderCommandTest.php
index d21cf691..e2583a27 100644
--- a/tests/Unit/Console/MakeServiceProviderCommandTest.php
+++ b/tests/Unit/Console/MakeServiceProviderCommandTest.php
@@ -28,5 +28,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Service provider successfully generated!');
+ expect($command->getDisplay())->toContain('Service provider [app/Providers/SlackServiceProvider.php] successfully generated!');
});
diff --git a/tests/Unit/Console/MakeTestCommandTest.php b/tests/Unit/Console/MakeTestCommandTest.php
index b79fcdef..f218e0af 100644
--- a/tests/Unit/Console/MakeTestCommandTest.php
+++ b/tests/Unit/Console/MakeTestCommandTest.php
@@ -28,7 +28,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Test successfully generated!');
+ expect($command->getDisplay())->toContain('Test [tests/Feature/ExampleTest.php] successfully generated!');
});
it('does not create the test because it already exists', function () {
@@ -79,7 +79,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Test successfully generated!');
+ expect($command->getDisplay())->toContain('Test [tests/Feature/ExampleTest.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -102,7 +102,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Test successfully generated!');
+ expect($command->getDisplay())->toContain('Test [tests/Feature/Admin/ExampleTest.php] successfully generated!');
});
it('creates test successfully with unit option', function () {
@@ -129,5 +129,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Test successfully generated!');
+ expect($command->getDisplay())->toContain('Test [tests/Unit/ExampleTest.php] successfully generated!');
});
diff --git a/tests/Unit/Database/Console/MakeMigrationCommandTest.php b/tests/Unit/Database/Console/MakeMigrationCommandTest.php
index 9907938b..2eb89fd4 100644
--- a/tests/Unit/Database/Console/MakeMigrationCommandTest.php
+++ b/tests/Unit/Database/Console/MakeMigrationCommandTest.php
@@ -24,5 +24,6 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Migration successfully generated!');
+ expect($command->getDisplay())->toContain('Migration [database/migrations/');
+ expect($command->getDisplay())->toContain('_create_products_table.php] successfully generated!');
});
diff --git a/tests/Unit/Database/Console/MakeSeederCommandTest.php b/tests/Unit/Database/Console/MakeSeederCommandTest.php
index 4784fedb..a4152f5e 100644
--- a/tests/Unit/Database/Console/MakeSeederCommandTest.php
+++ b/tests/Unit/Database/Console/MakeSeederCommandTest.php
@@ -24,5 +24,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Seeder successfully generated!');
+ expect($command->getDisplay())->toContain('Seeder [database/seeders/UsersSeeder.php] successfully generated!');
});
diff --git a/tests/Unit/Events/Console/MakeEventCommandTest.php b/tests/Unit/Events/Console/MakeEventCommandTest.php
new file mode 100644
index 00000000..293954a4
--- /dev/null
+++ b/tests/Unit/Events/Console/MakeEventCommandTest.php
@@ -0,0 +1,110 @@
+expect(
+ exists: fn (string $path) => false,
+ get: fn (string $path) => '',
+ put: function (string $path) {
+ expect($path)->toBe(base_path('app/Events/AwesomeEvent.php'));
+
+ return true;
+ },
+ createDirectory: function (string $path): void {
+ // ..
+ }
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:event', [
+ 'name' => 'AwesomeEvent',
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Event [app/Events/AwesomeEvent.php] successfully generated!');
+});
+
+it('does not create the event because it already exists', function () {
+ $mock = Mock::of(File::class)->expect(
+ exists: fn (string $path) => true,
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ $this->phenix('make:event', [
+ 'name' => 'TestEvent',
+ ]);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:event', [
+ 'name' => 'TestEvent',
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Event already exists!');
+});
+
+it('creates event successfully with force option', function () {
+ $tempDir = sys_get_temp_dir();
+ $tempPath = $tempDir . DIRECTORY_SEPARATOR . 'TestEvent.php';
+
+ file_put_contents($tempPath, 'old content');
+
+ $this->assertEquals('old content', file_get_contents($tempPath));
+
+ $mock = Mock::of(File::class)->expect(
+ exists: fn (string $path) => false,
+ get: fn (string $path) => 'new content',
+ put: fn (string $path, string $content) => file_put_contents($tempPath, $content),
+ createDirectory: function (string $path): void {
+ // ..
+ }
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:event', [
+ 'name' => 'TestEvent',
+ '--force' => true,
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Event [app/Events/TestEvent.php] successfully generated!');
+ expect('new content')->toBe(file_get_contents($tempPath));
+});
+
+it('creates event successfully in nested namespace', function () {
+ $mock = Mock::of(File::class)->expect(
+ exists: fn (string $path) => false,
+ get: fn (string $path) => '',
+ put: function (string $path) {
+ expect($path)->toBe(base_path('app/Events/Admin/TestEvent.php'));
+
+ return true;
+ },
+ createDirectory: function (string $path): void {
+ // ..
+ }
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:event', [
+ 'name' => 'Admin/TestEvent',
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Event [app/Events/Admin/TestEvent.php] successfully generated!');
+});
diff --git a/tests/Unit/Events/Console/MakeListenerCommandTest.php b/tests/Unit/Events/Console/MakeListenerCommandTest.php
new file mode 100644
index 00000000..9ac325f6
--- /dev/null
+++ b/tests/Unit/Events/Console/MakeListenerCommandTest.php
@@ -0,0 +1,110 @@
+expect(
+ exists: fn (string $path) => false,
+ get: fn (string $path) => '',
+ put: function (string $path) {
+ expect($path)->toBe(base_path('app/Listeners/AwesomeListener.php'));
+
+ return true;
+ },
+ createDirectory: function (string $path): void {
+ // ..
+ }
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:listener', [
+ 'name' => 'AwesomeListener',
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Listener [app/Listeners/AwesomeListener.php] successfully generated!');
+});
+
+it('does not create the listener because it already exists', function () {
+ $mock = Mock::of(File::class)->expect(
+ exists: fn (string $path) => true,
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ $this->phenix('make:listener', [
+ 'name' => 'TestListener',
+ ]);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:listener', [
+ 'name' => 'TestListener',
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Listener already exists!');
+});
+
+it('creates listener successfully with force option', function () {
+ $tempDir = sys_get_temp_dir();
+ $tempPath = $tempDir . DIRECTORY_SEPARATOR . 'TestListener.php';
+
+ file_put_contents($tempPath, 'old content');
+
+ $this->assertEquals('old content', file_get_contents($tempPath));
+
+ $mock = Mock::of(File::class)->expect(
+ exists: fn (string $path) => false,
+ get: fn (string $path) => 'new content',
+ put: fn (string $path, string $content) => file_put_contents($tempPath, $content),
+ createDirectory: function (string $path): void {
+ // ..
+ }
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:listener', [
+ 'name' => 'TestListener',
+ '--force' => true,
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Listener [app/Listeners/TestListener.php] successfully generated!');
+ expect('new content')->toBe(file_get_contents($tempPath));
+});
+
+it('creates listener successfully in nested namespace', function () {
+ $mock = Mock::of(File::class)->expect(
+ exists: fn (string $path) => false,
+ get: fn (string $path) => '',
+ put: function (string $path) {
+ expect($path)->toBe(base_path('app/Listeners/Admin/TestListener.php'));
+
+ return true;
+ },
+ createDirectory: function (string $path): void {
+ // ..
+ }
+ );
+
+ $this->app->swap(File::class, $mock);
+
+ /** @var \Symfony\Component\Console\Tester\CommandTester $command */
+ $command = $this->phenix('make:listener', [
+ 'name' => 'Admin/TestListener',
+ ]);
+
+ $command->assertCommandIsSuccessful();
+
+ expect($command->getDisplay())->toContain('Listener [app/Listeners/Admin/TestListener.php] successfully generated!');
+});
diff --git a/tests/Unit/Events/EventEmitterTest.php b/tests/Unit/Events/EventEmitterTest.php
new file mode 100644
index 00000000..e12259ab
--- /dev/null
+++ b/tests/Unit/Events/EventEmitterTest.php
@@ -0,0 +1,468 @@
+on('test.event', function (EventContract $event) use (&$called): void {
+ $called = true;
+ expect($event->getName())->toBe('test.event');
+ expect($event->getPayload())->toBe('test data');
+ });
+
+ $emitter->emit('test.event', 'test data');
+
+ expect($called)->toBeTrue();
+});
+
+it('can register and emit string-class events', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on(StandardEvent::class, fn (EventContract $event): string => 'string result');
+
+ $results = $emitter->emit(StandardEvent::class, 'test data');
+
+ expect($results)->toBe(['string result']);
+});
+
+it('can register and emit async events', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on('test.event', fn (EventContract $event): string => $event->getPayload());
+
+ $future = $emitter->emitAsync('test.event', 'test data');
+
+ $results = $future->await();
+
+ expect($results)->toBe(['test data']);
+});
+
+it('can register and emit basic events with string-class listeners', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on('test.event', StandardListener::class);
+
+ $results = $emitter->emit('test.event', 'test data');
+
+ expect($results)->toBe(['Event name: test.event']);
+});
+
+it('can register and emit basic events and listener with custom priority', function (): void {
+ $emitter = new EventEmitter();
+
+ $listener = new StandardListener();
+ $listener->setPriority(10);
+
+ $emitter->on('test.event', $listener);
+
+ $results = $emitter->emit('test.event', 'test data');
+
+ expect($results)->toBe(['Event name: test.event']);
+});
+
+it('returns null for invalid listeners', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on('test.event', InvalidListener::class);
+
+ $results = $emitter->emit('test.event', 'test data');
+
+ expect($results)->toBe([null]);
+});
+
+it('can register and emit events with facade syntax', function (): void {
+ $called = false;
+
+ EventFacade::on('facade.event', function (EventContract $event) use (&$called): void {
+ $called = true;
+ expect($event->getName())->toBe('facade.event');
+ expect($event->getPayload())->toBe('facade data');
+ });
+
+ EventFacade::emit('facade.event', 'facade data');
+
+ expect($called)->toBeTrue();
+});
+
+it('can register multiple listeners for same event', function (): void {
+ $emitter = new EventEmitter();
+ $count = 0;
+
+ $emitter->on('multi.event', function () use (&$count): void {
+ $count++;
+ });
+
+ $emitter->on('multi.event', function () use (&$count): void {
+ $count++;
+ });
+
+ $emitter->emit('multi.event');
+
+ expect($count)->toBe(2);
+});
+
+it('respects listener priorities', function (): void {
+ $emitter = new EventEmitter();
+ $order = [];
+
+ $emitter->on('priority.test', function () use (&$order): void {
+ $order[] = 'low';
+ }, 1);
+
+ $emitter->on('priority.test', function () use (&$order): void {
+ $order[] = 'high';
+ }, 10);
+
+ $emitter->on('priority.test', function () use (&$order): void {
+ $order[] = 'medium';
+ }, 5);
+
+ $emitter->emit('priority.test');
+
+ expect($order)->toBe(['high', 'medium', 'low']);
+});
+
+it('can register one-time listeners', function (): void {
+ $emitter = new EventEmitter();
+ $count = 0;
+
+ $emitter->once('once.event', function () use (&$count): void {
+ $count++;
+ });
+
+ $emitter->emit('once.event');
+ $emitter->emit('once.event');
+ $emitter->emit('once.event');
+
+ expect($count)->toBe(1);
+});
+
+it('can remove listeners', function (): void {
+ $emitter = new EventEmitter();
+ $called = false;
+
+ $listener = function () use (&$called): void {
+ $called = true;
+ };
+
+ $emitter->on('removable.event', $listener);
+ $emitter->off('removable.event', $listener);
+ $emitter->emit('removable.event');
+
+ expect($called)->toBeFalse();
+});
+
+it('tries to remove non registered event', function (): void {
+ $emitter = new EventEmitter();
+ $called = false;
+
+ $listener = function () use (&$called): void {
+ $called = true;
+ };
+
+ $emitter->off('removable.event', $listener);
+
+ expect($called)->toBeFalse();
+});
+
+it('can remove all listeners for an event', function (): void {
+ $emitter = new EventEmitter();
+ $count = 0;
+
+ $emitter->on('clear.event', function () use (&$count): void {
+ $count++;
+ });
+
+ $emitter->on('clear.event', function () use (&$count): void {
+ $count++;
+ });
+
+ $emitter->off('clear.event');
+ $emitter->emit('clear.event');
+
+ expect($count)->toBe(0);
+});
+
+it('can stop event propagation', function (): void {
+ $emitter = new EventEmitter();
+ $count = 0;
+
+ $emitter->on('stop.event', function (EventContract $event) use (&$count): void {
+ $count++;
+ $event->stopPropagation();
+ });
+
+ $emitter->on('stop.event', function (EventContract $event) use (&$count): void {
+ $count++;
+ });
+
+ $emitter->emit('stop.event');
+
+ expect($count)->toBe(1);
+});
+
+it('can stop async event propagation', function (): void {
+ $emitter = new EventEmitter();
+ $count = 0;
+
+ $emitter->on('stop.event', function (EventContract $event) use (&$count): void {
+ $count++;
+ $event->stopPropagation();
+ });
+
+ $emitter->on('stop.event', function (EventContract $event) use (&$count): void {
+ $count++;
+ });
+
+ $future = $emitter->emitAsync('stop.event');
+
+ $future->await();
+
+ expect($count)->toBe(1);
+});
+
+it('returns results from listeners', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on('result.event', fn (): string => 'first result');
+
+ $emitter->on('result.event', fn (): string => 'second result');
+
+ $results = $emitter->emit('result.event');
+
+ expect($results)->toBe(['first result', 'second result']);
+});
+
+it('can handle Event objects', function (): void {
+ $emitter = new EventEmitter();
+ $called = false;
+
+ $emitter->on('custom.event', function ($event) use (&$called): void {
+ $called = true;
+ expect($event->getName())->toBe('custom.event');
+ expect($event->getPayload())->toBe('custom data');
+ });
+
+ $event = new Event('custom.event', 'custom data');
+ $emitter->emit($event);
+
+ expect($called)->toBeTrue();
+ expect($event->getTimestamp())->toBeFloat();
+});
+
+it('skip the listener when this should not be handled', function (): void {
+ $emitter = new EventEmitter();
+
+ $listener = $this->getMockBuilder(StandardListener::class)
+ ->onlyMethods(['shouldHandle', 'handle'])
+ ->getMock();
+
+ $listener->expects($this->once())
+ ->method('shouldHandle')
+ ->willReturn(false);
+
+ $listener->expects($this->never())
+ ->method('handle');
+
+ $emitter->on('custom.event', $listener);
+
+ $emitter->emit('custom.event', 'data');
+});
+
+it('skip the listener when this should not be handled in async event', function (): void {
+ $emitter = new EventEmitter();
+
+ $listener = $this->getMockBuilder(StandardListener::class)
+ ->onlyMethods(['shouldHandle', 'handle'])
+ ->getMock();
+
+ $listener->expects($this->once())
+ ->method('shouldHandle')
+ ->willReturn(false);
+
+ $listener->expects($this->never())
+ ->method('handle');
+
+ $emitter->on('custom.event', $listener);
+
+ $future = $emitter->emitAsync('custom.event', 'data');
+
+ $future->await();
+});
+
+it('uses listener once and removes this after use', function (): void {
+ $emitter = new EventEmitter();
+
+ $listener = $this->getMockBuilder(StandardListener::class)
+ ->onlyMethods(['shouldHandle', 'isOnce', 'handle'])
+ ->getMock();
+
+ $listener->expects($this->once())
+ ->method('shouldHandle')
+ ->willReturn(true);
+
+ $listener->expects($this->once())
+ ->method('handle')
+ ->willReturn('Event name: custom.event');
+
+ $listener->expects($this->once())
+ ->method('isOnce')
+ ->willReturn(true);
+
+
+ $emitter->on('custom.event', $listener);
+
+ $future = $emitter->emitAsync('custom.event', 'data');
+
+ $future->await();
+});
+
+it('handle listener error gracefully', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on('error.event', function (): never {
+ throw new RuntimeError('Listener error');
+ });
+
+ Log::shouldReceive('error')
+ ->once();
+
+ $emitter->emit('error.event');
+})->throws(EventException::class);
+
+it('handle listener error gracefully in async event', function (): void {
+ $emitter = new EventEmitter();
+ $emitter->setEmitWarnings(true);
+
+ $emitter->on('error.event', function (): never {
+ throw new RuntimeError('Listener error');
+ });
+
+ Log::shouldReceive('error')
+ ->times(2);
+
+ $future = $emitter->emitAsync('error.event');
+
+ $future->await();
+});
+
+it('handle listener error gracefully in async event without warnings', function (): void {
+ $emitter = new EventEmitter();
+ $emitter->setEmitWarnings(false);
+
+ $emitter->on('error.event', function (): never {
+ throw new RuntimeError('Listener error');
+ });
+
+ Log::shouldReceive('error')->once();
+
+ $future = $emitter->emitAsync('error.event');
+
+ $future->await();
+});
+
+it('can check if event has listeners', function (): void {
+ $emitter = new EventEmitter();
+
+ expect($emitter->hasListeners('nonexistent.event'))->toBeFalse();
+
+ $emitter->on('existing.event', function (): void {
+ // Do something
+ });
+
+ expect($emitter->hasListeners('existing.event'))->toBeTrue();
+});
+
+it('can get listener count', function (): void {
+ $emitter = new EventEmitter();
+
+ expect($emitter->getListenerCount('count.event'))->toBe(0);
+
+ $emitter->on('count.event', function (): void {
+ // Do something
+ });
+ $emitter->on('count.event', function (): void {
+ // Do something
+ });
+
+ expect($emitter->getListenerCount('count.event'))->toBe(2);
+});
+
+it('can get event names', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on('event.one', function (): void {
+ // Do something
+ });
+ $emitter->on('event.two', function (): void {
+ // Do something
+ });
+
+ $eventNames = $emitter->getEventNames();
+
+ expect($eventNames)->toContain('event.one');
+ expect($eventNames)->toContain('event.two');
+});
+
+it('can set max listeners', function () {
+ $emitter = new EventEmitter();
+ $emitter->setMaxListeners(2);
+
+ expect($emitter->getMaxListeners())->toBe(2);
+});
+
+it('can clear all listeners', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->on('event.one', function (): void {
+ // Do something
+ });
+ $emitter->on('event.two', function (): void {
+ // Do something
+ });
+
+ $emitter->removeAllListeners();
+
+ expect($emitter->getEventNames())->toBeEmpty();
+});
+
+it('warns when exceeding the maximum number of listeners for an event', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->setMaxListeners(1);
+ $emitter->setEmitWarnings(true);
+
+ Log::shouldReceive('warning')->once();
+
+ $emitter->on('warn.event', fn (): null => null);
+ $emitter->on('warn.event', fn (): null => null); // This pushes it over the limit and should log a warning
+
+ expect($emitter->getListenerCount('warn.event'))->toBe(2);
+});
+
+it('does not warn when exceeding maximum listeners if warnings disabled', function (): void {
+ $emitter = new EventEmitter();
+
+ $emitter->setMaxListeners(1);
+ $emitter->setEmitWarnings(false);
+
+ Log::shouldReceive('warning')->never();
+
+ $emitter->on('warn.event', fn (): null => null);
+ $emitter->on('warn.event', fn (): null => null);
+
+ expect($emitter->getListenerCount('warn.event'))->toBe(2);
+});
diff --git a/tests/Unit/Events/Internal/InvalidListener.php b/tests/Unit/Events/Internal/InvalidListener.php
new file mode 100644
index 00000000..77e4441f
--- /dev/null
+++ b/tests/Unit/Events/Internal/InvalidListener.php
@@ -0,0 +1,10 @@
+getName();
+ }
+}
diff --git a/tests/Unit/Queue/Console/TableCommandTest.php b/tests/Unit/Queue/Console/TableCommandTest.php
index 89b679b1..f05f15cf 100644
--- a/tests/Unit/Queue/Console/TableCommandTest.php
+++ b/tests/Unit/Queue/Console/TableCommandTest.php
@@ -27,5 +27,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Queue table successfully generated!');
+ expect($command->getDisplay())->toContain('Queue table [database/migrations/20250101205638_create_tasks_table.php] successfully generated!');
});
diff --git a/tests/Unit/Tasks/Console/MakeTaskCommandTest.php b/tests/Unit/Tasks/Console/MakeTaskCommandTest.php
index 23778d18..6fa74ded 100644
--- a/tests/Unit/Tasks/Console/MakeTaskCommandTest.php
+++ b/tests/Unit/Tasks/Console/MakeTaskCommandTest.php
@@ -28,7 +28,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Task successfully generated!');
+ expect($command->getDisplay())->toContain('Task [app/Tasks/AwesomeTask.php] successfully generated!');
});
it('creates queuable task successfully', function () {
@@ -55,7 +55,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Task successfully generated!');
+ expect($command->getDisplay())->toContain('Task [app/Tasks/AwesomeTask.php] successfully generated!');
});
it('does not create the task because it already exists', function () {
@@ -106,7 +106,7 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Task successfully generated!');
+ expect($command->getDisplay())->toContain('Task [app/Tasks/TestTask.php] successfully generated!');
expect('new content')->toBe(file_get_contents($tempPath));
});
@@ -133,5 +133,5 @@
$command->assertCommandIsSuccessful();
- expect($command->getDisplay())->toContain('Task successfully generated!');
+ expect($command->getDisplay())->toContain('Task [app/Tasks/Admin/TestTask.php] successfully generated!');
});
diff --git a/tests/fixtures/application/config/app.php b/tests/fixtures/application/config/app.php
index 6393975d..4778ad35 100644
--- a/tests/fixtures/application/config/app.php
+++ b/tests/fixtures/application/config/app.php
@@ -27,5 +27,6 @@
\Phenix\Mail\MailServiceProvider::class,
\Phenix\Crypto\CryptoServiceProvider::class,
\Phenix\Queue\QueueServiceProvider::class,
+ \Phenix\Events\EventServiceProvider::class,
],
];