{"id":16294141,"url":"https://github.com/sarfraznawaz2005/actions","last_synced_at":"2025-03-20T04:30:25.550Z","repository":{"id":62540344,"uuid":"222702266","full_name":"sarfraznawaz2005/actions","owner":"sarfraznawaz2005","description":"Laravel package as alternative to single action controllers with support for web and api in single class.","archived":false,"fork":false,"pushed_at":"2021-11-10T12:47:31.000Z","size":121,"stargazers_count":6,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-28T23:01:48.518Z","etag":null,"topics":["actions","cleancode","controller","laravel","single","srp"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sarfraznawaz2005.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"license.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-11-19T13:26:32.000Z","updated_at":"2021-11-10T12:46:31.000Z","dependencies_parsed_at":"2022-11-02T15:33:48.525Z","dependency_job_id":null,"html_url":"https://github.com/sarfraznawaz2005/actions","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarfraznawaz2005%2Factions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarfraznawaz2005%2Factions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarfraznawaz2005%2Factions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sarfraznawaz2005%2Factions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sarfraznawaz2005","download_url":"https://codeload.github.com/sarfraznawaz2005/actions/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244047647,"owners_count":20389206,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actions","cleancode","controller","laravel","single","srp"],"created_at":"2024-10-10T20:14:19.047Z","updated_at":"2025-03-20T04:30:25.278Z","avatar_url":"https://github.com/sarfraznawaz2005.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](license.md)\n[![Latest Version on Packagist][ico-version]][link-packagist]\n[![Total Downloads][ico-downloads]][link-downloads]\n\n# Laravel Actions\n\nLaravel package as an alternative to [single action controllers](https://laravel.com/docs/master/controllers#single-action-controllers) with support for web/html and api in single class. You can use *single* class called *Action* to send appropriate web or api response *automatically*. It also provides easy way to validate request data.\n\nUnder the hood, action classes are normal Laravel controllers but with single public `__invoke` method. This means you can do anything that you do with controllers normally like calling `$this-\u003emiddleware('foo')` or anything else.\n\n\n## Table of Contents\n\n- [Why](#why)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Example Action Class](#example-action-class)\n- [Usage](#usage)\n- [Send Web or API Response Automatically](#send-web-or-api-response-automatically)\n- [Validation](#validation)\n- [Utility Methods and Properties](#utility-methods-and-properties)\n- [Creating Actions](#creating-actions)\n- [Registering Routes](#registering-routes)\n- [Bonus: Creating Plain Classes](#bonus-creating-plain-classes)\n\n## Why ##\n\n - Helps follow single responsibility principle (SRP)\n - Helps keep controllers and models skinny\n - Small dedicated class makes the code easier to test\n - Helps avoid code duplication eg different classes for web and api\n - Action classes can be callable from multiple places in your app\n - Small dedicated classes really pay off in complex apps\n - Expressive routes registration like `Route::get('/', HomeAction::class)`\n - Allows decorator pattern \n \n\n## Requirements ##\n\n - PHP \u003e= 7\n - Laravel 5, 6\n\n## Installation ##\n\nInstall via composer\n\n```\ncomposer require sarfraznawaz2005/actions\n```\n\n\nThat's it.\n\n---\n\n## Example Action Class ##\n\n````php\nclass PublishPostAction extends Action\n{\n    /**\n     * Define any validation rules.\n     */\n    protected $rules = [];\n\n    /**\n     * Perform the action.\n     *\n     * @return mixed\n     */\n    public function __invoke()\n    {\n        // code\n    }\n}\n````\n\nIn `__invoke()` method, you write actual logic of the action. Actions are invokable classes that use `__invoke` magic function turning them into a `Callable` which allows them to be called as function.\n\n## Usage ##\n\n**As Controller Actions**\n\nPrimary usage of action classes is mapping them to routes so they are called automatically when visiting those routes:\n\n````php\n// routes/web.php\n\nRoute::get('post', '\\App\\Http\\Actions\\PublishPostAction');\n\n// or\n\nRoute::get('post', '\\\\' . PublishPostAction::class);\n````\n\n\u003e \u003csup\u003e*Note that the initial `\\` here is important to ensure the namespace does not become `\\App\\Http\\Controller\\App\\Http\\Actions\\PublishPostAction`*\u003c/sup\u003e\n\n**As Callable Classes**\n\n````php\n$action = new PublishPostAction();\n$action();\n````\n\n## Send Web or API Response Automatically ##\n\nIf you need to serve both web and api responses from same/single action class, you need to define `html()` and `json()` method in your action class:\n\n````php\nclass TodosListAction extends Action\n{\n    protected $todos;\n\n    public function __invoke(Todo $todos)\n    {\n        $this-\u003etodos = $todos-\u003eall();\n    }\n\n    protected function html()\n    {\n        return view('index')-\u003ewith('todos', $this-\u003etodos);\n    }\n\n    protected function json()\n    {\n        return TodosResource::collection($this-\u003etodos);\n    }\n}\n````\n\nWith these two methods present, the package will *automatically* send appropriate response. Browsers will receive output from `html()` method and other devices will receive output from `json()` method.\n\nUnder the hood, we check if `Accept: application/json` header is present in request and if so it sends output from your `json()` method otherwise from `html()` method. \n\nYou can change this api/json detection mechanism by implementing `isApi()` method, it must return `boolean` value:\n\n````php\nclass TodosListAction extends Action\n{\n    protected $todos;\n\n    public function __invoke(Todo $todos)\n    {\n        $this-\u003etodos = $todos-\u003eall();\n    }\n\n    protected function html()\n    {\n        return view('index')-\u003ewith('todos', $this-\u003etodos);\n    }\n\n    protected function json()\n    {\n        return TodosResource::collection($this-\u003etodos);\n    }\n        \n    public function isApi()\n    {\n        return request()-\u003ewantsJson() \u0026\u0026 !request()-\u003eacceptsHtml();\n    }\n\n}\n````\n\n**Using Action Classes for API Requests Only**\n\nSimply return `true` from `isApi` method and use `json` method.\n\n**Using Action Classes for Web/Browser Requests Only**\n\nThis is default behaviour, you can simply return your HTML/blade views from within `__invoke` or `html` method if you use it.\n\n## Validation ##\n\nYou can perform input validation for your `store` and `update` methods, simply use `protected $rules = []` property in your action class:\n\n````php\nclass TodoStoreAction extends Action\n{\n    protected $rules = [\n        'title' =\u003e 'required|min:5'\n    ];\n    \n    public function __invoke(Todo $todo)\n    {\n        $todo-\u003efill(request()-\u003eall());\n    \n        return $todo-\u003esave();\n    }\n}\n````\n\nIn this case, validation will be performed before `__invoke` method is called and if it fails, you will be automatically redirected back to previous form page with `$errors` filled with validation errors.\n\n\u003e **Tip:** Because validation is performed before `__invoke` method is called, using `request()-\u003eall()` will always give you valid data in `__invoke` method which is why it's used in above example.\n\n**Custom Validation Messages**\n\nTo implement custom validation error messages for your rules, simply use `protected $messages = []` property.\n\n**Ignoring/Filtering Request Data**\n\nIf you want to remove some request data before it is validated/persisted, you can use the `protected $ignored = ['id'];`. In this case, `id` will be removed from the request eg in other words it will be as if it was not posted in the request.\n\n## Utility Methods and Properties ##\n\nConsider following action which is supposed to save todo/task into database and send appropriate response to web and api:\n\n````php\nclass TodoStoreAction extends Action\n{\n    protected $rules = [\n        'title' =\u003e 'required|min:2'\n    ];\n\n    public function __invoke(Todo $todo)\n    {\n        return $this-\u003ecreate($todo);\n    }\n\n    protected function html($result)\n    {\n        if (!$result) {\n            return back()-\u003ewithInput()-\u003ewithErrors($this-\u003eerrors);\n        }\n\n        session()-\u003eflash('success', self::MESSAGE_CREATE);\n        return back();\n    }\n\n    protected function json($result)\n    {\n        if (!$result) {\n            return response()-\u003ejson(['result' =\u003e false], Response::HTTP_INTERNAL_SERVER_ERROR);\n        }\n\n        return response()-\u003ejson(['result' =\u003e true], Response::HTTP_CREATED);\n    }\n}\n````\n\nThere are few things to notice above that package provides out of the box:\n\n - Inside `__invoke` method, we used `$this-\u003ecreate` method as shorthand/quick way to create a new todo record. Similarly, `$this-\u003eupdate` and `$this-\u003edelete` methods can also be used. They all return `boolean` value. They all also accept optional callback:\n\n````php\nreturn $this-\u003ecreate($todo, function ($result) {\n    if ($result) {\n        flash(self::MESSAGE_CREATE, 'success');\n    } else {\n        flash(self::MESSAGE_FAIL, 'danger');\n    }\n});\n````\n\n\nUsing these utility methods is not required though.\n \n - If you return something from `__invoke` method, it can be read later from `html` and `json` methods as first parameter. In this case, boolean result of todo creation (`return $this-\u003ecreate($todo)`) was used in both `html` and `json` methods via `$result` variable whos name can be anything.\n \n - Any validation errors are saved in `$this-\u003eerrors` variable which can be used as needed.\n\n - In `html()` method, we have used `self::MESSAGE_CREATE` which comes from parent action class. Similar, `self::MESSAGE_UPDATE`, `self::MESSAGE_DELETE` and `self::MESSAGE_FAIL` can also be used.\n\n\u003e **Tip:** You can choose to not use any utility methods/properties/validations offered by this package which is completely fine. Remember, action classes are normal Laravel controllers you can use however you like.\n\n**Transforming Request Data**\n\nIf you want to transform request data *before* validation is performed and *before* `__invoke()` method is called, you can define `transform` method in your action class which must return an array:\n\n`````php\npublic function transform(Request $request): array\n{\n    return [\n        'description' =\u003e trim(strip_tags($request-\u003edescription)),\n        'user_id' =\u003e auth()-\u003euser-\u003eid ?? 0,\n    ];\n}\n`````\n\nThe transform method can be used to both *modify* existing request variables as well as *adding* new variables to request data. In above example, we modify `description` to trim any whitespace and remove any html tags. We also add `user_id` to request data which wasn't in it before.\n\nBehind the scene, we simply merge whatever is returned from this method into original Request data.\n\n## Creating Actions ##\n\n![Screen](https://github.com/sarfraznawaz2005/actions/blob/master/screen.png?raw=true)\n\n- Create an action\n\n```bash\nphp artisan make:action ShowPost\n```\n\n\u003e `ShowPost` action will be created\n\n- Create actions for all resource actions (`index`, `show`, `create`, `store`, `edit`, `update`, `destroy`)\n\n```bash\nphp artisan make:action Post --resource\n```\n\n\u003e `IndexPost`, `ShowPost`, `CreatePost`, `StorePost`, `EditPost`, `UpdatePost`, `DestroyPost` actions will be created\n\n- Create actions for all API actions (`create`, `edit` excluded)\n\n```bash\nphp artisan make:action Post --api\n```\n\n\u003e `IndexPost`, `ShowPost`, `StorePost`, `UpdatePost`, `DestroyPost` actions will be created\n\n- Create actions by the specified actions\n\n```bash\nphp artisan make:action Post --actions=show,destroy,approve\n```\n\n\u003e `ShowPost`, `DestroyPost`, `ApprovePost` actions will be created\n\n- Exclude specified actions\n\n```bash\nphp artisan make:action Post --resource --except=index,show,edit\n```\n\n\u003e `CreatePost`, `StorePost`, `UpdatePost`, `DestroyPost` actions will be created\n\n- Specify namespace for actions creating (relative path)\n\n```bash\nphp artisan make:action Post --resource --namespace=Post\n```\n\n\u003e `IndexPost`, `ShowPost`, `CreatePost`, `StorePost`, `EditPost`, `UpdatePost`, `DestroyPost` actions will be created under `App\\Http\\Actions\\Post` namespace in `app/Http/Actions/Post` directory\n\n- Specify namespace for actions creating (absolute path)\n\n```bash\nphp artisan make:action ActivateUser --namespace=\\\\App\\\\Foo\\\\Bar\n```\n\n\u003e `ActivateUser` action will be created under `App\\Foo\\Bar` namespace in `app/Foo/Bar` directory\n\n- Force create\n\n```bash\nphp artisan make:action EditPost --force\n```\n\n\u003e If `EditPost` action already exists, it will be overwritten by the new one\n\n\n## Registering Routes \n\nHere are several ways to register actions in routes:\n\n#### In separate `actions.php` route file\n\n- Create `routes/actions.php` file (you can choose any name, it's just an example)\n- Define the \"action\" route group in `app/Providers/RouteServiceProvider.php`\n\n\u003e ##### With namespace auto prefixing\n\n```php\n// app/Providers/RouteServiceProvider.php\n\nprotected function mapActionRoutes()\n{\n    Route::middleware('web')\n         -\u003enamespace('App\\Http\\Actions')\n         -\u003egroup(base_path('routes/actions.php'));\n}\n```\n\n```php\n// app/Providers/RouteServiceProvider.php\n\npublic function map()\n{\n    $this-\u003emapApiRoutes();\n\n    $this-\u003emapWebRoutes();\n    \n    $this-\u003emapActionRoutes();\n\n    //\n}\n```\n\n```php\n// routes/actions.php\n\nRoute::get('/post/{post}', 'ShowPost');\n```\n\n\u003e ##### Without namespace auto prefixing\n\n```php\n// app/Providers/RouteServiceProvider.php\n\nprotected function mapActionRoutes()\n{\n    Route::middleware('web')\n         -\u003egroup(base_path('routes/actions.php'));\n}\n```\n\n```php\n// app/Providers/RouteServiceProvider.php\n\npublic function map()\n{\n    $this-\u003emapApiRoutes();\n\n    $this-\u003emapWebRoutes();\n    \n    $this-\u003emapActionRoutes();\n\n    //\n}\n```\n\n```php\n// routes/actions.php\n\nuse App\\Actions\\ShowPost;\n\nRoute::get('/post/{post}', ShowPost::class); // pretty sweet, isn't it? 😍\n```\n\n#### In `web.php` route file\n\n- Change the namespace for \"web\" group in `RouteServiceProvider.php`\n\n```php\n// app/Providers/RouteServiceProvider.php\n\nprotected function mapWebRoutes()\n{\n    Route::middleware('web')\n         -\u003enamespace('App\\Http') // pay attention here\n         -\u003egroup(base_path('routes/web.php'));\n}\n```\n\n- Put actions and controllers in different route groups in `routes/web.php` file and prepend an appropriate namespace for each of them\n\n```php\n// routes/web.php\n\nRoute::group(['namespace' =\u003e 'Actions'], function () {\n    Route::get('/posts/{post}', 'ShowPost');\n    Route::delete('/posts/{post}', 'DestroyPost');\n});\n\nRoute::group(['namespace' =\u003e 'Controllers'], function () {\n    Route::get('/users', 'UserController@index');\n    Route::get('/users/{user}', 'UserController@show');\n});\n```\n\n## Bonus: Creating Plain Classes\n\nThe package also provides `make:class` console command to create plain classes:\n\n```bash\nphp artisan make:class FooBar\n```\n\n`FooBar` class will be created under `app/Actions` folder:\n\n````php\nnamespace App\\Actions;\n\nclass FooBar\n{\n    /**\n     * Perform the action.\n     *\n     * @return mixed\n     */\n    public function execute()\n    {\n        //\n    }\n}\n````\n\n\nNote that these are plain old PHP classes you can use for any purpose. *Ideally*, they should not be dependent on Laravel framework or any other framework and should have single public method as api such as `execute` and any more private/protected methods needed for that class to work. This will allow you to use them across different projects and frameworks. You can also think of them as service classes.\n\n\n## Credits\n\n- [Sarfraz Ahmed][link-author]\n- [All Contributors][link-contributors]\n\n## License\n\nPlease see the [license file](license.md) for more information.\n\n[ico-version]: https://img.shields.io/packagist/v/sarfraznawaz2005/actions.svg?style=flat-square\n[ico-downloads]: https://img.shields.io/packagist/dt/sarfraznawaz2005/actions.svg?style=flat-square\n\n[link-packagist]: https://packagist.org/packages/sarfraznawaz2005/actions\n[link-downloads]: https://packagist.org/packages/sarfraznawaz2005/actions\n[link-author]: https://github.com/sarfraznawaz2005\n[link-contributors]: https://github.com/sarfraznawaz2005/actions/graphs/contributors\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsarfraznawaz2005%2Factions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsarfraznawaz2005%2Factions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsarfraznawaz2005%2Factions/lists"}