diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index bc33663..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,24 +0,0 @@ -on: push -name: CI - -jobs: - phpunit: - strategy: - matrix: - php: ["8.0", "8.1"] - name: PHP ${{ matrix.php }} - runs-on: ubuntu-latest - container: - image: kirschbaumdevelopment/laravel-test-runner:${{ matrix.php }} - - steps: - - uses: actions/checkout@v1 - with: - fetch-depth: 1 - - - name: Install composer dependencies - run: | - composer install --prefer-dist --no-interaction --no-scripts - - name: Run Testsuite - run: vendor/bin/phpunit tests/ - diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml deleted file mode 100644 index 6f65659..0000000 --- a/.github/workflows/code-style.yml +++ /dev/null @@ -1,20 +0,0 @@ -on: push -name: Code Style - -jobs: - laravel-pint: - runs-on: ubuntu-latest - container: - image: kirschbaumdevelopment/laravel-test-runner:8.0 - - steps: - - uses: actions/checkout@v1 - with: - fetch-depth: 1 - - - name: Install composer dependencies - run: | - composer install --prefer-dist --no-interaction --no-scripts - - name: Check Coding Standards - run: composer pint-check - diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..d936b2a --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,204 @@ +on: push +name: CI + +jobs: + phpunit: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + strategy: + fail-fast: true + matrix: + php: [ 8.0, 8.1, 8.2, 8.3, 8.4 ] + laravel: [ 7.*, 8.*, 9.*, 10.*, 11.*, 12.* ] + include: + - php: 8.0 + laravel: 7.* + phpunit: 9.* + testbench: 5.* + larastan: 1.* + - php: 8.0 + laravel: 8.* + phpunit: 9.* + testbench: 6.* + larastan: 1.* + - php: 8.0 + laravel: 9.* + phpunit: 9.* + testbench: 7.* + larastan: 2.* + + - php: 8.1 + laravel: 8.* + phpunit: 9.* + testbench: 6.* + larastan: 1.* + - php: 8.1 + laravel: 9.* + phpunit: 9.* + testbench: 7.* + larastan: 2.* + - php: 8.1 + laravel: 10.* + phpunit: 10.* + testbench: 8.* + larastan: 2.* + + - php: 8.2 + laravel: 8.* + phpunit: 9.* + testbench: 6.* + larastan: 1.* + - php: 8.2 + laravel: 9.* + phpunit: 9.* + testbench: 7.* + larastan: 2.* + - php: 8.2 + laravel: 10.* + phpunit: 10.* + testbench: 8.* + larastan: 2.* + - php: 8.2 + laravel: 11.* + phpunit: 10.* + testbench: 9.* + larastan: 2.* + - php: 8.2 + laravel: 12.* + phpunit: 11.* + testbench: 10.* + larastan: 3.* + + - php: 8.3 + laravel: 8.* + phpunit: 9.* + testbench: 6.* + larastan: 1.* + - php: 8.3 + laravel: 9.* + phpunit: 9.* + testbench: 7.* + larastan: 2.* + - php: 8.3 + laravel: 10.* + phpunit: 10.* + testbench: 8.* + larastan: 2.* + - php: 8.3 + laravel: 11.* + phpunit: 11.* + testbench: 9.* + larastan: 2.* + - php: 8.3 + laravel: 12.* + phpunit: 11.* + testbench: 10.* + larastan: 3.* + + - php: 8.4 + laravel: 8.* + phpunit: 9.* + testbench: 6.* + larastan: 1.* + - php: 8.4 + laravel: 9.* + phpunit: 9.* + testbench: 7.* + larastan: 2.* + - php: 8.4 + laravel: 10.* + phpunit: 10.* + testbench: 8.* + larastan: 2.* + - php: 8.4 + laravel: 11.* + phpunit: 11.* + testbench: 9.* + larastan: 2.* + - php: 8.4 + laravel: 12.* + phpunit: 11.* + testbench: 10.* + larastan: 3.* + + exclude: + - php: 8.0 + laravel: 10.* + - php: 8.0 + laravel: 11.* + - php: 8.0 + laravel: 12.* + - php: 8.1 + laravel: 7.* + - php: 8.1 + laravel: 11.* + - php: 8.1 + laravel: 12.* + - php: 8.2 + laravel: 7.* + - php: 8.3 + laravel: 7.* + - php: 8.4 + laravel: 7.* + + name: Laravel Actions Tests - PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: dependencies-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: curl, mbstring, zip, pcntl, iconv + coverage: none + tools: composer:v2 + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update + composer require "phpunit/phpunit:${{ matrix.phpunit }}" --no-interaction --no-update + composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer require "nunomaduro/larastan:${{ matrix.larastan }}" --no-interaction --no-update + composer update --prefer-dist --no-interaction + composer dump + + - name: Execute tests + run: vendor/bin/phpunit tests/ + + pint: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + name: Pint Style Check + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: dependencies-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + tools: composer:v2 + + - name: Install dependencies + run: | + composer install --no-interaction + composer dump + + - name: Execute Pint + run: composer pint-check diff --git a/README.md b/README.md index a44939b..538f990 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,29 @@ public function index (Action $action) } ``` +## Macros + +There are times when you may want to add something extra to your actions. We can leverage macros for this! + +Here is an example were we are leveraging Inertia's defer functionality directly on our action. The macro then just calls `act()` on the action class when the deferred prop is requested! + +```php +// In your service provider +Action::macro('defer', function($action, ...$arguments) { + return Inertia::defer(fn () => $action::act(...$arguments)); +}); + +// In your controller +return Inertia::render('Users/Index') + ->with('users', GetUsers::defer()); +``` + +Note how the originating class is passed into the macro function as the first parameter. This is very important, otherwise the macro will be unaware of which action you are actually running as macros are technically run on the parent action class. You are also free to do what you want regarding the subsequent $arguments, but it is considered best practice to pack/unpack the arguments with the spread operator to ensure the actions are as flexible as possible. + +Also take note the `defer()` is defined on the `Kirschbaum\Actions\Action` class, not on the `GetUsers` action class. Individual actions are not macroable in and of themselves. The macro `defer()` will also be available to every action in your application, not just the `GetUsers` action! + +Before and after events will not be fired when using macros. They will get fired however if you use an action's `act()`, `actWhen()`, or `actUnless()` methods with the macro function. + ## Handling Failures We all know Chuck Norris isn't going to fail us, but he isn't the only one using this... Handling failures is pretty easy with Actions. Out of the box, any exceptions thrown by your Action classes get handled by Laravel's exception handler. If you'd rather implement your own logic during a failure, add a `failed()` method to your Action. It's that easy! You can return data from your `failed()` method if you choose as well. diff --git a/pint.json b/pint.json index 40169d0..858164c 100644 --- a/pint.json +++ b/pint.json @@ -25,13 +25,26 @@ "yield" ] }, + "class_definition": { + "multi_line_extends_each_single_line": true, + "single_item_single_line": true, + "single_line": false + }, "concat_space": { "spacing": "one" }, "explicit_string_variable": true, "method_chaining_indentation": true, "new_with_braces": true, + "nullable_type_declaration_for_default_null_value": true, "ordered_traits": true, - "phpdoc_separation": true + "php_unit_method_casing": { + "case": "camel_case" + }, + "php_unit_test_annotation": { + "style": "prefix" + }, + "phpdoc_separation": true, + "single_line_empty_body": true } } diff --git a/src/Action.php b/src/Action.php index 66b05a9..7ce5d31 100644 --- a/src/Action.php +++ b/src/Action.php @@ -2,23 +2,23 @@ namespace Kirschbaum\Actions; +use Illuminate\Support\Traits\Macroable; use Kirschbaum\Actions\Contracts\Actionable; use Kirschbaum\Actions\Exceptions\ActionableInterfaceNotFoundException; use Throwable; class Action { + use Macroable; + /** * Arguments to pass into the action's constructor. - * - * @var array */ - protected $arguments; + protected array $arguments; /** * Initiate the given action. * - * @param string $action * @param mixed ...$arguments * * @return mixed @@ -35,8 +35,6 @@ public function act(string $action, ...$arguments) /** * Initiate the given action if the given condition is true. * - * @param $condition - * @param string $action * @param mixed ...$arguments * * @return mixed|void @@ -55,8 +53,6 @@ public function actWhen($condition, string $action, ...$arguments) /** * Initiate the action if the given condition is false. * - * @param $condition - * @param string $action * @param mixed ...$arguments * * @return mixed|void @@ -75,7 +71,6 @@ public function actUnless($condition, string $action, ...$arguments) /** * Handle the given action. * - * @param string $action * * @return mixed|void * @@ -101,16 +96,17 @@ protected function handle(string $action) /** * Determine if the action has a `failed()` method defined. - * - * @param Actionable $action - * - * @return bool */ protected function actionHasFailedMethod(Actionable $action): bool { return method_exists($action, 'failed'); } + /** + * Determine if the action has the proper interface. + * + * @throws Throwable + */ protected function checkActionForInterface($action): void { throw_unless( @@ -121,11 +117,6 @@ protected function checkActionForInterface($action): void /** * Dispatch appropriate action event. - * - * @param string $event - * @param Actionable $action - * - * @return void */ protected function dispatchEvent(string $event, Actionable $action): void { @@ -139,11 +130,6 @@ protected function dispatchEvent(string $event, Actionable $action): void /** * Check if the given event exists in the action. - * - * @param Actionable $action - * @param string $event - * - * @return bool */ protected function eventExists(Actionable $action, string $event): bool { @@ -154,8 +140,6 @@ protected function eventExists(Actionable $action, string $event): bool /** * Fire failure event and/or call failed action method if they exist. * - * @param Actionable $action - * @param Throwable $exception * * @return mixed * @@ -178,10 +162,6 @@ protected function handleFailure(Actionable $action, Throwable $exception) /** * Check if action has a custom exception. - * - * @param Actionable $action - * - * @return bool */ protected function hasCustomException(Actionable $action): bool { @@ -191,10 +171,6 @@ protected function hasCustomException(Actionable $action): bool /** * Raise the before action event. - * - * @param Actionable $action - * - * @return void */ protected function raiseBeforeActionEvent(Actionable $action): void { @@ -203,10 +179,6 @@ protected function raiseBeforeActionEvent(Actionable $action): void /** * Raise the after action event. - * - * @param Actionable $action - * - * @return void */ protected function raiseAfterActionEvent(Actionable $action): void { diff --git a/src/ActionsServiceProvider.php b/src/ActionsServiceProvider.php index a200c4c..16cd3c9 100644 --- a/src/ActionsServiceProvider.php +++ b/src/ActionsServiceProvider.php @@ -24,8 +24,6 @@ class ActionsServiceProvider extends ServiceProvider /** * Register any application services. - * - * @return void */ public function register(): void { @@ -35,7 +33,6 @@ public function register(): void /** * Bootstrap any package services. * - * @return void * * @throws ReflectionException */ @@ -46,23 +43,31 @@ public function boot(): void $this->bootPublishConfig(); $this->bootAutoDiscoverActions(); + + $this->bootActionMacro(); } /** * Get the services provided by the provider. - * - * @return array */ public function provides(): array { return [Action::class]; } + /** + * Boot macro needed for action functionality. + */ + protected function bootActionMacro(): void + { + Action::macro('getMacro', function (string $name): callable|object { + return static::$macros[$name]; + }); + } + /** * Auto-discover actions classes. * - * @return void - * * @throws ReflectionException */ protected function bootAutoDiscoverActions(): void @@ -95,8 +100,6 @@ protected function bootAutoDiscoverActions(): void /** * Load console commands for actions. - * - * @return void */ protected function bootConsoleCommands(): void { @@ -109,8 +112,6 @@ protected function bootConsoleCommands(): void /** * Publish action configuration file. - * - * @return void */ protected function bootPublishConfig(): void { @@ -121,8 +122,6 @@ protected function bootPublishConfig(): void /** * Register merging of configuration file. - * - * @return void */ protected function registerMergeConfig(): void { diff --git a/src/Exceptions/ActionFailedException.php b/src/Exceptions/ActionFailedException.php index db1dda8..f296c33 100644 --- a/src/Exceptions/ActionFailedException.php +++ b/src/Exceptions/ActionFailedException.php @@ -4,6 +4,4 @@ use Exception; -class ActionFailedException extends Exception -{ -} +class ActionFailedException extends Exception {} diff --git a/src/Exceptions/ActionableInterfaceNotFoundException.php b/src/Exceptions/ActionableInterfaceNotFoundException.php index 8fa27c6..1c40f3a 100644 --- a/src/Exceptions/ActionableInterfaceNotFoundException.php +++ b/src/Exceptions/ActionableInterfaceNotFoundException.php @@ -4,6 +4,4 @@ use Exception; -class ActionableInterfaceNotFoundException extends Exception -{ -} +class ActionableInterfaceNotFoundException extends Exception {} diff --git a/src/Facades/Action.php b/src/Facades/Action.php index eb01d93..562ead6 100644 --- a/src/Facades/Action.php +++ b/src/Facades/Action.php @@ -8,8 +8,6 @@ class Action extends Facade { /** * Get the registered name of the component. - * - * @return string */ protected static function getFacadeAccessor(): string { diff --git a/src/Traits/CanAct.php b/src/Traits/CanAct.php index d9e1f90..c06b45f 100644 --- a/src/Traits/CanAct.php +++ b/src/Traits/CanAct.php @@ -2,30 +2,46 @@ namespace Kirschbaum\Actions\Traits; +use BadMethodCallException; +use Closure; +use Kirschbaum\Actions\Action; + trait CanAct { /** * Handles static method calls by passing them to the Action class. * - * @param string $name - * @param array $arguments * * @return mixed|void */ public static function __callStatic(string $name, array $arguments) { if (in_array($name, ['act', 'actWhen', 'actUnless'])) { - $action = app(get_called_class()); + $action = app(static::class); if ($name === 'act') { - return $action->act(get_called_class(), ...$arguments); + return $action->act(static::class, ...$arguments); } return $action->$name( $arguments[0], - get_called_class(), + static::class, ...array_slice($arguments, 1) ); } + + if (! Action::hasMacro($name)) { + throw new BadMethodCallException(sprintf( + 'Method %s::%s does not exist.', static::class, $name + )); + } + + $macro = Action::getMacro($name); + + if ($macro instanceof Closure) { + $macro = $macro->bindTo(null, Action::class); + } + + return $macro(static::class, ...$arguments); } } diff --git a/src/helpers.php b/src/helpers.php index 7f6559d..1cb3f36 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -6,7 +6,6 @@ /** * Initiate the given action. * - * @param string $action * @param mixed ...$arguments * * @return mixed @@ -23,8 +22,6 @@ function act(string $action, ...$arguments) /** * Initiate the given action if the given condition is true. * - * @param $condition - * @param string $action * @param mixed ...$arguments * * @return mixed @@ -41,8 +38,6 @@ function act_when($condition, string $action, ...$arguments) /** * Initiate the given action if the given condition is false. * - * @param $condition - * @param string $action * @param mixed ...$arguments * * @return mixed diff --git a/tests/Fixtures/Events/AfterEvent.php b/tests/Fixtures/Events/AfterEvent.php index 4e8c6b6..7b0d311 100644 --- a/tests/Fixtures/Events/AfterEvent.php +++ b/tests/Fixtures/Events/AfterEvent.php @@ -18,9 +18,7 @@ class AfterEvent * * @return void */ - public function __construct() - { - } + public function __construct() {} /** * Get the channels the event should broadcast on. diff --git a/tests/Fixtures/Events/BeforeEvent.php b/tests/Fixtures/Events/BeforeEvent.php index eee0273..bcdb3ed 100644 --- a/tests/Fixtures/Events/BeforeEvent.php +++ b/tests/Fixtures/Events/BeforeEvent.php @@ -18,9 +18,7 @@ class BeforeEvent * * @return void */ - public function __construct() - { - } + public function __construct() {} /** * Get the channels the event should broadcast on. diff --git a/tests/Fixtures/Events/FailedEvent.php b/tests/Fixtures/Events/FailedEvent.php index 15d5bb0..326dabe 100644 --- a/tests/Fixtures/Events/FailedEvent.php +++ b/tests/Fixtures/Events/FailedEvent.php @@ -18,9 +18,7 @@ class FailedEvent * * @return void */ - public function __construct() - { - } + public function __construct() {} /** * Get the channels the event should broadcast on. diff --git a/tests/Fixtures/Exceptions/CustomFailedException.php b/tests/Fixtures/Exceptions/CustomFailedException.php index cfb384d..95c8a12 100644 --- a/tests/Fixtures/Exceptions/CustomFailedException.php +++ b/tests/Fixtures/Exceptions/CustomFailedException.php @@ -4,6 +4,4 @@ use Exception; -class CustomFailedException extends Exception -{ -} +class CustomFailedException extends Exception {} diff --git a/tests/TestCase.php b/tests/TestCase.php index ec91e8c..0484afa 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -17,8 +17,6 @@ class TestCase extends OrchestraTestCase * Get package providers. * * @param Application $app - * - * @return array */ protected function getPackageProviders($app): array { diff --git a/tests/Unit/MacroableTest.php b/tests/Unit/MacroableTest.php new file mode 100644 index 0000000..3177d13 --- /dev/null +++ b/tests/Unit/MacroableTest.php @@ -0,0 +1,24 @@ +assertEquals(ActionWithNoEvents::class, $results); + } +}