mirror of https://github.com/flightphp/core
				
				
				
			
							parent
							
								
									d6854cb078
								
							
						
					
					
						commit
						5068995d17
					
				| @ -0,0 +1,23 @@ | ||||
| name: Pull Request Check | ||||
| on: [pull_request] | ||||
| 
 | ||||
| jobs: | ||||
|     unit-test: | ||||
|         name: Unit testing | ||||
|         strategy: | ||||
|             fail-fast: false | ||||
|             matrix: | ||||
|                 php: [7.4, 8.0, 8.1, 8.2, 8.3, 8.4] | ||||
|         runs-on: ubuntu-latest | ||||
|         steps: | ||||
|             - name: Checkout repository | ||||
|               uses: actions/checkout@v4 | ||||
|               with: | ||||
|                   fetch-depth: 0 | ||||
|             - uses: shivammathur/setup-php@v2 | ||||
|               with: | ||||
|                   php-version: ${{ matrix.php }} | ||||
|                   extensions: curl, mbstring | ||||
|                   tools: composer:v2 | ||||
|             - run: composer install | ||||
|             - run: composer test | ||||
| @ -0,0 +1,137 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace flight\core; | ||||
| 
 | ||||
| class EventDispatcher | ||||
| { | ||||
|     /** @var self|null Singleton instance of the EventDispatcher */ | ||||
|     private static ?self $instance = null; | ||||
| 
 | ||||
|     /** @var array<string, array<int, callable>> */ | ||||
|     protected array $listeners = []; | ||||
| 
 | ||||
|     /** | ||||
|      * Singleton instance of the EventDispatcher. | ||||
|      * | ||||
|      * @return self | ||||
|      */ | ||||
|     public static function getInstance(): self | ||||
|     { | ||||
|         if (self::$instance === null) { | ||||
|             self::$instance = new self(); | ||||
|         } | ||||
|         return self::$instance; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Register a callback for an event. | ||||
|      * | ||||
|      * @param string $event Event name | ||||
|      * @param callable $callback Callback function | ||||
|      */ | ||||
|     public function on(string $event, callable $callback): void | ||||
|     { | ||||
|         if (isset($this->listeners[$event]) === false) { | ||||
|             $this->listeners[$event] = []; | ||||
|         } | ||||
|         $this->listeners[$event][] = $callback; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Trigger an event with optional arguments. | ||||
|      * | ||||
|      * @param string $event Event name | ||||
|      * @param mixed ...$args Arguments to pass to the callbacks | ||||
|      */ | ||||
|     public function trigger(string $event, ...$args): void | ||||
|     { | ||||
|         if (isset($this->listeners[$event]) === true) { | ||||
|             foreach ($this->listeners[$event] as $callback) { | ||||
|                 $result = call_user_func_array($callback, $args); | ||||
| 
 | ||||
|                 // If you return false, it will break the loop and stop the other event listeners. | ||||
|                 if ($result === false) { | ||||
|                     break; // Stop executing further listeners | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if an event has any registered listeners. | ||||
|      * | ||||
|      * @param string $event Event name | ||||
|      * | ||||
|      * @return bool True if the event has listeners, false otherwise | ||||
|      */ | ||||
|     public function hasListeners(string $event): bool | ||||
|     { | ||||
|         return isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get all listeners registered for a specific event. | ||||
|      * | ||||
|      * @param string $event Event name | ||||
|      * | ||||
|      * @return array<int, callable> Array of callbacks registered for the event | ||||
|      */ | ||||
|     public function getListeners(string $event): array | ||||
|     { | ||||
|         return $this->listeners[$event] ?? []; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a list of all events that have registered listeners. | ||||
|      * | ||||
|      * @return array<int, string> Array of event names | ||||
|      */ | ||||
|     public function getAllRegisteredEvents(): array | ||||
|     { | ||||
|         return array_keys($this->listeners); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove a specific listener for an event. | ||||
|      * | ||||
|      * @param string   $event    the event name | ||||
|      * @param callable $callback the exact callback to remove | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public function removeListener(string $event, callable $callback): void | ||||
|     { | ||||
|         if (isset($this->listeners[$event]) === true && count($this->listeners[$event]) > 0) { | ||||
|             $this->listeners[$event] = array_filter($this->listeners[$event], function ($listener) use ($callback) { | ||||
|                 return $listener !== $callback; | ||||
|             }); | ||||
|             $this->listeners[$event] = array_values($this->listeners[$event]); // Re-index the array | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove all listeners for a specific event. | ||||
|      * | ||||
|      * @param string $event the event name | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public function removeAllListeners(string $event): void | ||||
|     { | ||||
|         if (isset($this->listeners[$event]) === true) { | ||||
|             unset($this->listeners[$event]); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Remove the current singleton instance of the EventDispatcher. | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public static function resetInstance(): void | ||||
|     { | ||||
|         self::$instance = null; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,157 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace flight\net; | ||||
| 
 | ||||
| use Exception; | ||||
| 
 | ||||
| class UploadedFile | ||||
| { | ||||
|     /** | ||||
|      * @var string $name The name of the uploaded file. | ||||
|      */ | ||||
|     private string $name; | ||||
| 
 | ||||
|     /** | ||||
|      * @var string $mimeType The MIME type of the uploaded file. | ||||
|      */ | ||||
|     private string $mimeType; | ||||
| 
 | ||||
|     /** | ||||
|      * @var int $size The size of the uploaded file in bytes. | ||||
|      */ | ||||
|     private int $size; | ||||
| 
 | ||||
|     /** | ||||
|      * @var string $tmpName The temporary name of the uploaded file. | ||||
|      */ | ||||
|     private string $tmpName; | ||||
| 
 | ||||
|     /** | ||||
|      * @var int $error The error code associated with the uploaded file. | ||||
|      */ | ||||
|     private int $error; | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs a new UploadedFile object. | ||||
|      * | ||||
|      * @param string $name The name of the uploaded file. | ||||
|      * @param string $mimeType The MIME type of the uploaded file. | ||||
|      * @param int $size The size of the uploaded file in bytes. | ||||
|      * @param string $tmpName The temporary name of the uploaded file. | ||||
|      * @param int $error The error code associated with the uploaded file. | ||||
|      */ | ||||
|     public function __construct(string $name, string $mimeType, int $size, string $tmpName, int $error) | ||||
|     { | ||||
|         $this->name = $name; | ||||
|         $this->mimeType = $mimeType; | ||||
|         $this->size = $size; | ||||
|         $this->tmpName = $tmpName; | ||||
|         $this->error = $error; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieves the client-side filename of the uploaded file. | ||||
|      * | ||||
|      * @return string The client-side filename. | ||||
|      */ | ||||
|     public function getClientFilename(): string | ||||
|     { | ||||
|         return $this->name; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieves the media type of the uploaded file as provided by the client. | ||||
|      * | ||||
|      * @return string The media type of the uploaded file. | ||||
|      */ | ||||
|     public function getClientMediaType(): string | ||||
|     { | ||||
|         return $this->mimeType; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the size of the uploaded file. | ||||
|      * | ||||
|      * @return int The size of the uploaded file. | ||||
|      */ | ||||
|     public function getSize(): int | ||||
|     { | ||||
|         return $this->size; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieves the temporary name of the uploaded file. | ||||
|      * | ||||
|      * @return string The temporary name of the uploaded file. | ||||
|      */ | ||||
|     public function getTempName(): string | ||||
|     { | ||||
|         return $this->tmpName; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the error code associated with the uploaded file. | ||||
|      * | ||||
|      * @return int The error code. | ||||
|      */ | ||||
|     public function getError(): int | ||||
|     { | ||||
|         return $this->error; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Moves the uploaded file to the specified target path. | ||||
|      * | ||||
|      * @param string $targetPath The path to move the file to. | ||||
|      * | ||||
|      * @return void | ||||
|      */ | ||||
|     public function moveTo(string $targetPath): void | ||||
|     { | ||||
|         if ($this->error !== UPLOAD_ERR_OK) { | ||||
|             throw new Exception($this->getUploadErrorMessage($this->error)); | ||||
|         } | ||||
| 
 | ||||
|         $isUploadedFile = is_uploaded_file($this->tmpName) === true; | ||||
|         if ( | ||||
|             $isUploadedFile === true | ||||
|             && | ||||
|             move_uploaded_file($this->tmpName, $targetPath) === false | ||||
|         ) { | ||||
|             throw new Exception('Cannot move uploaded file'); // @codeCoverageIgnore | ||||
|         } elseif ($isUploadedFile === false && getenv('PHPUNIT_TEST')) { | ||||
|             rename($this->tmpName, $targetPath); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieves the error message for a given upload error code. | ||||
|      * | ||||
|      * @param int $error The upload error code. | ||||
|      * | ||||
|      * @return string The error message. | ||||
|      */ | ||||
|     protected function getUploadErrorMessage(int $error): string | ||||
|     { | ||||
|         switch ($error) { | ||||
|             case UPLOAD_ERR_INI_SIZE: | ||||
|                 return 'The uploaded file exceeds the upload_max_filesize directive in php.ini.'; | ||||
|             case UPLOAD_ERR_FORM_SIZE: | ||||
|                 return 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'; | ||||
|             case UPLOAD_ERR_PARTIAL: | ||||
|                 return 'The uploaded file was only partially uploaded.'; | ||||
|             case UPLOAD_ERR_NO_FILE: | ||||
|                 return 'No file was uploaded.'; | ||||
|             case UPLOAD_ERR_NO_TMP_DIR: | ||||
|                 return 'Missing a temporary folder.'; | ||||
|             case UPLOAD_ERR_CANT_WRITE: | ||||
|                 return 'Failed to write file to disk.'; | ||||
|             case UPLOAD_ERR_EXTENSION: | ||||
|                 return 'A PHP extension stopped the file upload.'; | ||||
|             default: | ||||
|                 return 'An unknown error occurred. Error code: ' . $error; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,348 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace flight\tests; | ||||
| 
 | ||||
| use Flight; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use flight\Engine; | ||||
| use TypeError; | ||||
| 
 | ||||
| class EventSystemTest extends TestCase | ||||
| { | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         // Reset the Flight engine before each test to ensure a clean state | ||||
|         Flight::setEngine(new Engine()); | ||||
|         Flight::app()->init(); | ||||
|         Flight::eventDispatcher()->resetInstance(); // Clear any existing listeners | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test registering and triggering a single listener. | ||||
|      */ | ||||
|     public function testRegisterAndTriggerSingleListener() | ||||
|     { | ||||
|         $called = false; | ||||
|         Flight::onEvent('test.event', function () use (&$called) { | ||||
|             $called = true; | ||||
|         }); | ||||
|         Flight::triggerEvent('test.event'); | ||||
|         $this->assertTrue($called, 'Single listener should be called when event is triggered.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test registering multiple listeners for the same event. | ||||
|      */ | ||||
|     public function testRegisterMultipleListeners() | ||||
|     { | ||||
|         $counter = 0; | ||||
|         Flight::onEvent('test.event', function () use (&$counter) { | ||||
|             $counter++; | ||||
|         }); | ||||
|         Flight::onEvent('test.event', function () use (&$counter) { | ||||
|             $counter++; | ||||
|         }); | ||||
|         Flight::triggerEvent('test.event'); | ||||
|         $this->assertEquals(2, $counter, 'All registered listeners should be called.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test triggering an event with no listeners registered. | ||||
|      */ | ||||
|     public function testTriggerWithNoListeners() | ||||
|     { | ||||
|         // Should not throw any errors | ||||
|         Flight::triggerEvent('non.existent.event'); | ||||
|         $this->assertTrue(true, 'Triggering an event with no listeners should not throw an error.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that a listener receives a single argument correctly. | ||||
|      */ | ||||
|     public function testListenerReceivesSingleArgument() | ||||
|     { | ||||
|         $received = null; | ||||
|         Flight::onEvent('test.event', function ($arg) use (&$received) { | ||||
|             $received = $arg; | ||||
|         }); | ||||
|         Flight::triggerEvent('test.event', 'hello'); | ||||
|         $this->assertEquals('hello', $received, 'Listener should receive the passed argument.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that a listener receives multiple arguments correctly. | ||||
|      */ | ||||
|     public function testListenerReceivesMultipleArguments() | ||||
|     { | ||||
|         $received = []; | ||||
|         Flight::onEvent('test.event', function ($arg1, $arg2) use (&$received) { | ||||
|             $received = [$arg1, $arg2]; | ||||
|         }); | ||||
|         Flight::triggerEvent('test.event', 'first', 'second'); | ||||
|         $this->assertEquals(['first', 'second'], $received, 'Listener should receive all passed arguments.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that listeners are called in the order they were registered. | ||||
|      */ | ||||
|     public function testListenersCalledInOrder() | ||||
|     { | ||||
|         $order = []; | ||||
|         Flight::onEvent('test.event', function () use (&$order) { | ||||
|             $order[] = 1; | ||||
|         }); | ||||
|         Flight::onEvent('test.event', function () use (&$order) { | ||||
|             $order[] = 2; | ||||
|         }); | ||||
|         Flight::triggerEvent('test.event'); | ||||
|         $this->assertEquals([1, 2], $order, 'Listeners should be called in registration order.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that listeners are not called for unrelated events. | ||||
|      */ | ||||
|     public function testListenerNotCalledForOtherEvents() | ||||
|     { | ||||
|         $called = false; | ||||
|         Flight::onEvent('test.event1', function () use (&$called) { | ||||
|             $called = true; | ||||
|         }); | ||||
|         Flight::triggerEvent('test.event2'); | ||||
|         $this->assertFalse($called, 'Listeners should not be called for different events.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test overriding the onEvent method. | ||||
|      */ | ||||
|     public function testOverrideOnEvent() | ||||
|     { | ||||
|         $called = false; | ||||
|         Flight::map('onEvent', function ($event, $callback) use (&$called) { | ||||
|             $called = true; | ||||
|         }); | ||||
|         Flight::onEvent('test.event', function () { | ||||
|         }); | ||||
|         $this->assertTrue($called, 'Overridden onEvent method should be called.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test overriding the triggerEvent method. | ||||
|      */ | ||||
|     public function testOverrideTriggerEvent() | ||||
|     { | ||||
|         $called = false; | ||||
|         Flight::map('triggerEvent', function ($event, ...$args) use (&$called) { | ||||
|             $called = true; | ||||
|         }); | ||||
|         Flight::triggerEvent('test.event'); | ||||
|         $this->assertTrue($called, 'Overridden triggerEvent method should be called.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that an overridden onEvent can still register listeners by calling the original method. | ||||
|      */ | ||||
|     public function testOverrideOnEventStillRegistersListener() | ||||
|     { | ||||
|         $overrideCalled = false; | ||||
|         Flight::map('onEvent', function ($event, $callback) use (&$overrideCalled) { | ||||
|             $overrideCalled = true; | ||||
|             // Call the original method | ||||
|             Flight::app()->_onEvent($event, $callback); | ||||
|         }); | ||||
| 
 | ||||
|         $listenerCalled = false; | ||||
|         Flight::onEvent('test.event', function () use (&$listenerCalled) { | ||||
|             $listenerCalled = true; | ||||
|         }); | ||||
| 
 | ||||
|         Flight::triggerEvent('test.event'); | ||||
| 
 | ||||
|         $this->assertTrue($overrideCalled, 'Overridden onEvent should be called.'); | ||||
|         $this->assertTrue($listenerCalled, 'Listener should still be triggered after override.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that an overridden triggerEvent can still trigger listeners by calling the original method. | ||||
|      */ | ||||
|     public function testOverrideTriggerEventStillTriggersListeners() | ||||
|     { | ||||
|         $overrideCalled = false; | ||||
|         Flight::map('triggerEvent', function ($event, ...$args) use (&$overrideCalled) { | ||||
|             $overrideCalled = true; | ||||
|             // Call the original method | ||||
|             Flight::app()->_triggerEvent($event, ...$args); | ||||
|         }); | ||||
| 
 | ||||
|         $listenerCalled = false; | ||||
|         Flight::onEvent('test.event', function () use (&$listenerCalled) { | ||||
|             $listenerCalled = true; | ||||
|         }); | ||||
| 
 | ||||
|         Flight::triggerEvent('test.event'); | ||||
| 
 | ||||
|         $this->assertTrue($overrideCalled, 'Overridden triggerEvent should be called.'); | ||||
|         $this->assertTrue($listenerCalled, 'Listeners should still be triggered after override.'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that an invalid callable throws an exception (if applicable). | ||||
|      */ | ||||
|     public function testInvalidCallableThrowsException() | ||||
|     { | ||||
|         $this->expectException(TypeError::class); | ||||
|         // Assuming the event system validates callables | ||||
|         Flight::onEvent('test.event', 'not_a_callable'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that event propagation stops if a listener returns false. | ||||
|      */ | ||||
|     public function testStopPropagation() | ||||
|     { | ||||
|         $firstCalled = false; | ||||
|         $secondCalled = false; | ||||
|         $thirdCalled = false; | ||||
| 
 | ||||
|         Flight::onEvent('test.event', function () use (&$firstCalled) { | ||||
|             $firstCalled = true; | ||||
|             return true; // Continue propagation | ||||
|         }); | ||||
| 
 | ||||
|         Flight::onEvent('test.event', function () use (&$secondCalled) { | ||||
|             $secondCalled = true; | ||||
|             return false; // Stop propagation | ||||
|         }); | ||||
| 
 | ||||
|         Flight::onEvent('test.event', function () use (&$thirdCalled) { | ||||
|             $thirdCalled = true; | ||||
|         }); | ||||
| 
 | ||||
|         Flight::triggerEvent('test.event'); | ||||
| 
 | ||||
|         $this->assertTrue($firstCalled, 'First listener should be called'); | ||||
|         $this->assertTrue($secondCalled, 'Second listener should be called'); | ||||
|         $this->assertFalse($thirdCalled, 'Third listener should not be called after propagation stopped'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that hasListeners() correctly identifies events with listeners. | ||||
|      */ | ||||
|     public function testHasListeners() | ||||
|     { | ||||
|         $this->assertFalse(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should not have listeners before registration'); | ||||
| 
 | ||||
|         Flight::onEvent('test.event', function () { | ||||
|         }); | ||||
| 
 | ||||
|         $this->assertTrue(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should have listeners after registration'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that getListeners() returns the correct listeners for an event. | ||||
|      */ | ||||
|     public function testGetListeners() | ||||
|     { | ||||
|         $callback1 = function () { | ||||
|         }; | ||||
|         $callback2 = function () { | ||||
|         }; | ||||
| 
 | ||||
|         $this->assertEmpty(Flight::eventDispatcher()->getListeners('test.event'), 'Event should have no listeners before registration'); | ||||
| 
 | ||||
|         Flight::onEvent('test.event', $callback1); | ||||
|         Flight::onEvent('test.event', $callback2); | ||||
| 
 | ||||
|         $listeners = Flight::eventDispatcher()->getListeners('test.event'); | ||||
|         $this->assertCount(2, $listeners, 'Event should have two registered listeners'); | ||||
|         $this->assertSame($callback1, $listeners[0], 'First listener should match the first callback'); | ||||
|         $this->assertSame($callback2, $listeners[1], 'Second listener should match the second callback'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that getListeners() returns an empty array for events with no listeners. | ||||
|      */ | ||||
|     public function testGetListenersForNonexistentEvent() | ||||
|     { | ||||
|         $listeners = Flight::eventDispatcher()->getListeners('nonexistent.event'); | ||||
|         $this->assertIsArray($listeners, 'Should return an array for nonexistent events'); | ||||
|         $this->assertEmpty($listeners, 'Should return an empty array for nonexistent events'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that getAllRegisteredEvents() returns all event names with registered listeners. | ||||
|      */ | ||||
|     public function testGetAllRegisteredEvents() | ||||
|     { | ||||
|         $this->assertEmpty(Flight::eventDispatcher()->getAllRegisteredEvents(), 'No events should be registered initially'); | ||||
| 
 | ||||
|         Flight::onEvent('test.event1', function () { | ||||
|         }); | ||||
|         Flight::onEvent('test.event2', function () { | ||||
|         }); | ||||
| 
 | ||||
|         $events = Flight::eventDispatcher()->getAllRegisteredEvents(); | ||||
|         $this->assertCount(2, $events, 'Should return all registered event names'); | ||||
|         $this->assertContains('test.event1', $events, 'Should contain the first event'); | ||||
|         $this->assertContains('test.event2', $events, 'Should contain the second event'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that removeListener() correctly removes a specific listener from an event. | ||||
|      */ | ||||
|     public function testRemoveListener() | ||||
|     { | ||||
|         $callback1 = function () { | ||||
|             return 'callback1'; | ||||
|         }; | ||||
|         $callback2 = function () { | ||||
|             return 'callback2'; | ||||
|         }; | ||||
| 
 | ||||
|         Flight::onEvent('test.event', $callback1); | ||||
|         Flight::onEvent('test.event', $callback2); | ||||
| 
 | ||||
|         $this->assertCount(2, Flight::eventDispatcher()->getListeners('test.event'), 'Event should have two listeners initially'); | ||||
| 
 | ||||
|         Flight::eventDispatcher()->removeListener('test.event', $callback1); | ||||
| 
 | ||||
|         $listeners = Flight::eventDispatcher()->getListeners('test.event'); | ||||
|         $this->assertCount(1, $listeners, 'Event should have one listener after removal'); | ||||
|         $this->assertSame($callback2, $listeners[0], 'Remaining listener should be the second callback'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that removeAllListeners() correctly removes all listeners for an event. | ||||
|      */ | ||||
|     public function testRemoveAllListeners() | ||||
|     { | ||||
|         Flight::onEvent('test.event', function () { | ||||
|         }); | ||||
|         Flight::onEvent('test.event', function () { | ||||
|         }); | ||||
|         Flight::onEvent('another.event', function () { | ||||
|         }); | ||||
| 
 | ||||
|         $this->assertTrue(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should have listeners before removal'); | ||||
|         $this->assertTrue(Flight::eventDispatcher()->hasListeners('another.event'), 'Another event should have listeners'); | ||||
| 
 | ||||
|         Flight::eventDispatcher()->removeAllListeners('test.event'); | ||||
| 
 | ||||
|         $this->assertFalse(Flight::eventDispatcher()->hasListeners('test.event'), 'Event should have no listeners after removal'); | ||||
|         $this->assertTrue(Flight::eventDispatcher()->hasListeners('another.event'), 'Another event should still have listeners'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Test that trying to remove listeners for nonexistent events doesn't cause errors. | ||||
|      */ | ||||
|     public function testRemoveListenersForNonexistentEvent() | ||||
|     { | ||||
|         // Should not throw any errors | ||||
|         Flight::eventDispatcher()->removeListener('nonexistent.event', function () { | ||||
|         }); | ||||
|         Flight::eventDispatcher()->removeAllListeners('nonexistent.event'); | ||||
| 
 | ||||
|         $this->assertTrue(true, 'Removing listeners for nonexistent events should not throw errors'); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,83 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace tests; | ||||
| 
 | ||||
| use Flight; | ||||
| use flight\Engine; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| 
 | ||||
| class FlightAsyncTest extends TestCase | ||||
| { | ||||
|     public static function setUpBeforeClass(): void | ||||
|     { | ||||
|         Flight::setEngine(new Engine()); | ||||
|     } | ||||
| 
 | ||||
|     protected function setUp(): void | ||||
|     { | ||||
|         $_SERVER = []; | ||||
|         $_REQUEST = []; | ||||
|     } | ||||
| 
 | ||||
|     protected function tearDown(): void | ||||
|     { | ||||
|         unset($_REQUEST); | ||||
|         unset($_SERVER); | ||||
|     } | ||||
| 
 | ||||
|     // Checks that default components are loaded | ||||
|     public function testSingleRoute() | ||||
|     { | ||||
|         Flight::route('GET /', function () { | ||||
|             echo 'hello world'; | ||||
|         }); | ||||
| 
 | ||||
|         $this->expectOutputString('hello world'); | ||||
|         Flight::start(); | ||||
|     } | ||||
| 
 | ||||
|     public function testMultipleRoutes() | ||||
|     { | ||||
|         Flight::route('GET /', function () { | ||||
|             echo 'hello world'; | ||||
|         }); | ||||
| 
 | ||||
|         Flight::route('GET /test', function () { | ||||
|             echo 'test'; | ||||
|         }); | ||||
| 
 | ||||
|         $this->expectOutputString('test'); | ||||
|         $_SERVER['REQUEST_URI'] = '/test'; | ||||
|         Flight::start(); | ||||
|     } | ||||
| 
 | ||||
|     public function testMultipleStartsSingleRoute() | ||||
|     { | ||||
|         Flight::route('GET /', function () { | ||||
|             echo 'hello world'; | ||||
|         }); | ||||
| 
 | ||||
|         $this->expectOutputString('hello worldhello world'); | ||||
|         Flight::start(); | ||||
|         Flight::start(); | ||||
|     } | ||||
| 
 | ||||
|     public function testMultipleStartsMultipleRoutes() | ||||
|     { | ||||
|         Flight::route('GET /', function () { | ||||
|             echo 'hello world'; | ||||
|         }); | ||||
| 
 | ||||
|         Flight::route('GET /test', function () { | ||||
|             echo 'test'; | ||||
|         }); | ||||
| 
 | ||||
|         $this->expectOutputString('testhello world'); | ||||
|         $_SERVER['REQUEST_URI'] = '/test'; | ||||
|         Flight::start(); | ||||
|         $_SERVER['REQUEST_URI'] = '/'; | ||||
|         Flight::start(); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,56 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace tests; | ||||
| 
 | ||||
| use Exception; | ||||
| use flight\net\UploadedFile; | ||||
| use PHPUnit\Framework\TestCase; | ||||
| 
 | ||||
| class UploadedFileTest extends TestCase | ||||
| { | ||||
|     public function tearDown(): void | ||||
|     { | ||||
|         if (file_exists('file.txt')) { | ||||
|             unlink('file.txt'); | ||||
|         } | ||||
|         if (file_exists('tmp_name')) { | ||||
|             unlink('tmp_name'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public function testMoveToSuccess() | ||||
|     { | ||||
|         file_put_contents('tmp_name', 'test'); | ||||
|         $uploadedFile = new UploadedFile('file.txt', 'text/plain', 4, 'tmp_name', UPLOAD_ERR_OK); | ||||
|         $uploadedFile->moveTo('file.txt'); | ||||
|         $this->assertFileExists('file.txt'); | ||||
|     } | ||||
| 
 | ||||
|     public function getFileErrorMessageTests(): array | ||||
|     { | ||||
|         return [ | ||||
|             [ UPLOAD_ERR_INI_SIZE, 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', ], | ||||
|             [ UPLOAD_ERR_FORM_SIZE, 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', ], | ||||
|             [ UPLOAD_ERR_PARTIAL, 'The uploaded file was only partially uploaded.', ], | ||||
|             [ UPLOAD_ERR_NO_FILE, 'No file was uploaded.', ], | ||||
|             [ UPLOAD_ERR_NO_TMP_DIR, 'Missing a temporary folder.', ], | ||||
|             [ UPLOAD_ERR_CANT_WRITE, 'Failed to write file to disk.', ], | ||||
|             [ UPLOAD_ERR_EXTENSION, 'A PHP extension stopped the file upload.', ], | ||||
|             [ -1, 'An unknown error occurred. Error code: -1' ] | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @dataProvider getFileErrorMessageTests | ||||
|      */ | ||||
|     public function testMoveToFailureMessages($error, $message) | ||||
|     { | ||||
|         file_put_contents('tmp_name', 'test'); | ||||
|         $uploadedFile = new UploadedFile('file.txt', 'text/plain', 4, 'tmp_name', $error); | ||||
|         $this->expectException(Exception::class); | ||||
|         $this->expectExceptionMessage($message); | ||||
|         $uploadedFile->moveTo('file.txt'); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,13 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace tests\classes; | ||||
| 
 | ||||
| class ClassWithExceptionInConstruct | ||||
| { | ||||
|     public function __construct() | ||||
|     { | ||||
|         throw new \Exception('This is an exception in the constructor'); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,142 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| use PHPUnit\Framework\TestCase; | ||||
| use tests\groupcompactsyntax\PostsController; | ||||
| use tests\groupcompactsyntax\TodosController; | ||||
| use tests\groupcompactsyntax\UsersController; | ||||
| 
 | ||||
| require_once __DIR__ . '/UsersController.php'; | ||||
| require_once __DIR__ . '/PostsController.php'; | ||||
| 
 | ||||
| final class FlightRouteCompactSyntaxTest extends TestCase | ||||
| { | ||||
|     public function setUp(): void | ||||
|     { | ||||
|         Flight::router()->clear(); | ||||
|     } | ||||
| 
 | ||||
|     public function testCanMapMethodsWithVerboseSyntax(): void | ||||
|     { | ||||
|         Flight::route('GET /users', [UsersController::class, 'index']); | ||||
|         Flight::route('DELETE /users/@id', [UsersController::class, 'destroy']); | ||||
| 
 | ||||
|         $routes = Flight::router()->getRoutes(); | ||||
| 
 | ||||
|         $this->assertCount(2, $routes); | ||||
| 
 | ||||
|         $this->assertSame('/users', $routes[0]->pattern); | ||||
|         $this->assertSame([UsersController::class, 'index'], $routes[0]->callback); | ||||
|         $this->assertSame('GET', $routes[0]->methods[0]); | ||||
| 
 | ||||
|         $this->assertSame('/users/@id', $routes[1]->pattern); | ||||
|         $this->assertSame([UsersController::class, 'destroy'], $routes[1]->callback); | ||||
|         $this->assertSame('DELETE', $routes[1]->methods[0]); | ||||
|     } | ||||
| 
 | ||||
|     public function testOptionsOnly(): void | ||||
|     { | ||||
|         Flight::resource('/users', UsersController::class, [ | ||||
|             'only' => [ 'index', 'destroy' ] | ||||
|         ]); | ||||
| 
 | ||||
|         $routes = Flight::router()->getRoutes(); | ||||
| 
 | ||||
|         $this->assertCount(2, $routes); | ||||
| 
 | ||||
|         $this->assertSame('/users', $routes[0]->pattern); | ||||
|         $this->assertSame('GET', $routes[0]->methods[0]); | ||||
|         $this->assertSame([UsersController::class, 'index'], $routes[0]->callback); | ||||
| 
 | ||||
|         $this->assertSame('/users/@id', $routes[1]->pattern); | ||||
|         $this->assertSame('DELETE', $routes[1]->methods[0]); | ||||
|         $this->assertSame([UsersController::class, 'destroy'], $routes[1]->callback); | ||||
|     } | ||||
| 
 | ||||
|     public function testDefaultMethods(): void | ||||
|     { | ||||
|         Flight::resource('/posts', PostsController::class); | ||||
| 
 | ||||
|         $routes = Flight::router()->getRoutes(); | ||||
|         $this->assertCount(7, $routes); | ||||
| 
 | ||||
|         $this->assertSame('/posts', $routes[0]->pattern); | ||||
|         $this->assertSame('GET', $routes[0]->methods[0]); | ||||
|         $this->assertSame([PostsController::class, 'index'], $routes[0]->callback); | ||||
|         $this->assertSame('posts.index', $routes[0]->alias); | ||||
| 
 | ||||
|         $this->assertSame('/posts/create', $routes[1]->pattern); | ||||
|         $this->assertSame('GET', $routes[1]->methods[0]); | ||||
|         $this->assertSame([PostsController::class, 'create'], $routes[1]->callback); | ||||
|         $this->assertSame('posts.create', $routes[1]->alias); | ||||
| 
 | ||||
|         $this->assertSame('/posts', $routes[2]->pattern); | ||||
|         $this->assertSame('POST', $routes[2]->methods[0]); | ||||
|         $this->assertSame([PostsController::class, 'store'], $routes[2]->callback); | ||||
|         $this->assertSame('posts.store', $routes[2]->alias); | ||||
| 
 | ||||
|         $this->assertSame('/posts/@id', $routes[3]->pattern); | ||||
|         $this->assertSame('GET', $routes[3]->methods[0]); | ||||
|         $this->assertSame([PostsController::class, 'show'], $routes[3]->callback); | ||||
|         $this->assertSame('posts.show', $routes[3]->alias); | ||||
| 
 | ||||
|         $this->assertSame('/posts/@id/edit', $routes[4]->pattern); | ||||
|         $this->assertSame('GET', $routes[4]->methods[0]); | ||||
|         $this->assertSame([PostsController::class, 'edit'], $routes[4]->callback); | ||||
|         $this->assertSame('posts.edit', $routes[4]->alias); | ||||
| 
 | ||||
|         $this->assertSame('/posts/@id', $routes[5]->pattern); | ||||
|         $this->assertSame('PUT', $routes[5]->methods[0]); | ||||
|         $this->assertSame([PostsController::class, 'update'], $routes[5]->callback); | ||||
|         $this->assertSame('posts.update', $routes[5]->alias); | ||||
| 
 | ||||
|         $this->assertSame('/posts/@id', $routes[6]->pattern); | ||||
|         $this->assertSame('DELETE', $routes[6]->methods[0]); | ||||
|         $this->assertSame([PostsController::class, 'destroy'], $routes[6]->callback); | ||||
|         $this->assertSame('posts.destroy', $routes[6]->alias); | ||||
|     } | ||||
| 
 | ||||
|     public function testOptionsExcept(): void | ||||
|     { | ||||
|         Flight::resource('/todos', TodosController::class, [ | ||||
|             'except' => [ 'create', 'store', 'update', 'destroy', 'edit' ] | ||||
|         ]); | ||||
| 
 | ||||
|         $routes = Flight::router()->getRoutes(); | ||||
| 
 | ||||
|         $this->assertCount(2, $routes); | ||||
| 
 | ||||
|         $this->assertSame('/todos', $routes[0]->pattern); | ||||
|         $this->assertSame('GET', $routes[0]->methods[0]); | ||||
|         $this->assertSame([TodosController::class, 'index'], $routes[0]->callback); | ||||
| 
 | ||||
|         $this->assertSame('/todos/@id', $routes[1]->pattern); | ||||
|         $this->assertSame('GET', $routes[1]->methods[0]); | ||||
|         $this->assertSame([TodosController::class, 'show'], $routes[1]->callback); | ||||
|     } | ||||
| 
 | ||||
|     public function testOptionsMiddlewareAndAliasBase(): void | ||||
|     { | ||||
|         Flight::resource('/todos', TodosController::class, [ | ||||
|             'middleware' => [ 'auth' ], | ||||
|             'alias_base' => 'nothanks' | ||||
|         ]); | ||||
| 
 | ||||
|         $routes = Flight::router()->getRoutes(); | ||||
| 
 | ||||
|         $this->assertCount(7, $routes); | ||||
| 
 | ||||
|         $this->assertSame('/todos', $routes[0]->pattern); | ||||
|         $this->assertSame('GET', $routes[0]->methods[0]); | ||||
|         $this->assertSame([TodosController::class, 'index'], $routes[0]->callback); | ||||
|         $this->assertSame('auth', $routes[0]->middleware[0]); | ||||
|         $this->assertSame('nothanks.index', $routes[0]->alias); | ||||
| 
 | ||||
|         $this->assertSame('/todos/create', $routes[1]->pattern); | ||||
|         $this->assertSame('GET', $routes[1]->methods[0]); | ||||
|         $this->assertSame([TodosController::class, 'create'], $routes[1]->callback); | ||||
|         $this->assertSame('auth', $routes[1]->middleware[0]); | ||||
|         $this->assertSame('nothanks.create', $routes[1]->alias); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,36 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace tests\groupcompactsyntax; | ||||
| 
 | ||||
| final class PostsController | ||||
| { | ||||
|     public function index(): void | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public function show(string $id): void | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public function create(): void | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public function store(): void | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public function edit(string $id): void | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public function update(string $id): void | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public function destroy(string $id): void | ||||
|     { | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,16 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace tests\groupcompactsyntax; | ||||
| 
 | ||||
| final class TodosController | ||||
| { | ||||
|     public function index(): void | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     public function show(string $id): void | ||||
|     { | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,18 @@ | ||||
| <?php | ||||
| 
 | ||||
| declare(strict_types=1); | ||||
| 
 | ||||
| namespace tests\groupcompactsyntax; | ||||
| 
 | ||||
| final class UsersController | ||||
| { | ||||
|     public function index(): void | ||||
|     { | ||||
|         echo __METHOD__; | ||||
|     } | ||||
| 
 | ||||
|     public function destroy(): void | ||||
|     { | ||||
|         echo __METHOD__; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| This file downloaded successfully! | ||||
					Loading…
					
					
				
		Reference in new issue
	
	 fadrian06
						fadrian06