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