Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/sarfraznawaz2005/actions
Laravel package as alternative to single action controllers with support for web and api in single class.
https://github.com/sarfraznawaz2005/actions
actions cleancode controller laravel single srp
Last synced: 3 months ago
JSON representation
Laravel package as alternative to single action controllers with support for web and api in single class.
- Host: GitHub
- URL: https://github.com/sarfraznawaz2005/actions
- Owner: sarfraznawaz2005
- License: mit
- Created: 2019-11-19T13:26:32.000Z (about 5 years ago)
- Default Branch: master
- Last Pushed: 2021-11-10T12:47:31.000Z (about 3 years ago)
- Last Synced: 2024-10-11T20:14:27.890Z (4 months ago)
- Topics: actions, cleancode, controller, laravel, single, srp
- Language: PHP
- Size: 118 KB
- Stars: 6
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: license.md
Awesome Lists containing this project
README
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](license.md)
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Total Downloads][ico-downloads]][link-downloads]# Laravel Actions
Laravel 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.
Under 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->middleware('foo')` or anything else.
## Table of Contents
- [Why](#why)
- [Requirements](#requirements)
- [Installation](#installation)
- [Example Action Class](#example-action-class)
- [Usage](#usage)
- [Send Web or API Response Automatically](#send-web-or-api-response-automatically)
- [Validation](#validation)
- [Utility Methods and Properties](#utility-methods-and-properties)
- [Creating Actions](#creating-actions)
- [Registering Routes](#registering-routes)
- [Bonus: Creating Plain Classes](#bonus-creating-plain-classes)## Why ##
- Helps follow single responsibility principle (SRP)
- Helps keep controllers and models skinny
- Small dedicated class makes the code easier to test
- Helps avoid code duplication eg different classes for web and api
- Action classes can be callable from multiple places in your app
- Small dedicated classes really pay off in complex apps
- Expressive routes registration like `Route::get('/', HomeAction::class)`
- Allows decorator pattern
## Requirements ##
- PHP >= 7
- Laravel 5, 6## Installation ##
Install via composer
```
composer require sarfraznawaz2005/actions
```That's it.
---
## Example Action Class ##
````php
class PublishPostAction extends Action
{
/**
* Define any validation rules.
*/
protected $rules = [];/**
* Perform the action.
*
* @return mixed
*/
public function __invoke()
{
// code
}
}
````In `__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.
## Usage ##
**As Controller Actions**
Primary usage of action classes is mapping them to routes so they are called automatically when visiting those routes:
````php
// routes/web.phpRoute::get('post', '\App\Http\Actions\PublishPostAction');
// or
Route::get('post', '\\' . PublishPostAction::class);
````> *Note that the initial `\` here is important to ensure the namespace does not become `\App\Http\Controller\App\Http\Actions\PublishPostAction`*
**As Callable Classes**
````php
$action = new PublishPostAction();
$action();
````## Send Web or API Response Automatically ##
If 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:
````php
class TodosListAction extends Action
{
protected $todos;public function __invoke(Todo $todos)
{
$this->todos = $todos->all();
}protected function html()
{
return view('index')->with('todos', $this->todos);
}protected function json()
{
return TodosResource::collection($this->todos);
}
}
````With 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.
Under 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.
You can change this api/json detection mechanism by implementing `isApi()` method, it must return `boolean` value:
````php
class TodosListAction extends Action
{
protected $todos;public function __invoke(Todo $todos)
{
$this->todos = $todos->all();
}protected function html()
{
return view('index')->with('todos', $this->todos);
}protected function json()
{
return TodosResource::collection($this->todos);
}
public function isApi()
{
return request()->wantsJson() && !request()->acceptsHtml();
}}
````**Using Action Classes for API Requests Only**
Simply return `true` from `isApi` method and use `json` method.
**Using Action Classes for Web/Browser Requests Only**
This is default behaviour, you can simply return your HTML/blade views from within `__invoke` or `html` method if you use it.
## Validation ##
You can perform input validation for your `store` and `update` methods, simply use `protected $rules = []` property in your action class:
````php
class TodoStoreAction extends Action
{
protected $rules = [
'title' => 'required|min:5'
];
public function __invoke(Todo $todo)
{
$todo->fill(request()->all());
return $todo->save();
}
}
````In 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.
> **Tip:** Because validation is performed before `__invoke` method is called, using `request()->all()` will always give you valid data in `__invoke` method which is why it's used in above example.
**Custom Validation Messages**
To implement custom validation error messages for your rules, simply use `protected $messages = []` property.
**Ignoring/Filtering Request Data**
If 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.
## Utility Methods and Properties ##
Consider following action which is supposed to save todo/task into database and send appropriate response to web and api:
````php
class TodoStoreAction extends Action
{
protected $rules = [
'title' => 'required|min:2'
];public function __invoke(Todo $todo)
{
return $this->create($todo);
}protected function html($result)
{
if (!$result) {
return back()->withInput()->withErrors($this->errors);
}session()->flash('success', self::MESSAGE_CREATE);
return back();
}protected function json($result)
{
if (!$result) {
return response()->json(['result' => false], Response::HTTP_INTERNAL_SERVER_ERROR);
}return response()->json(['result' => true], Response::HTTP_CREATED);
}
}
````There are few things to notice above that package provides out of the box:
- Inside `__invoke` method, we used `$this->create` method as shorthand/quick way to create a new todo record. Similarly, `$this->update` and `$this->delete` methods can also be used. They all return `boolean` value. They all also accept optional callback:
````php
return $this->create($todo, function ($result) {
if ($result) {
flash(self::MESSAGE_CREATE, 'success');
} else {
flash(self::MESSAGE_FAIL, 'danger');
}
});
````Using these utility methods is not required though.
- 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->create($todo)`) was used in both `html` and `json` methods via `$result` variable whos name can be anything.
- Any validation errors are saved in `$this->errors` variable which can be used as needed.- 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.
> **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.
**Transforming Request Data**
If 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:
`````php
public function transform(Request $request): array
{
return [
'description' => trim(strip_tags($request->description)),
'user_id' => auth()->user->id ?? 0,
];
}
`````The 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.
Behind the scene, we simply merge whatever is returned from this method into original Request data.
## Creating Actions ##
![Screen](https://github.com/sarfraznawaz2005/actions/blob/master/screen.png?raw=true)
- Create an action
```bash
php artisan make:action ShowPost
```> `ShowPost` action will be created
- Create actions for all resource actions (`index`, `show`, `create`, `store`, `edit`, `update`, `destroy`)
```bash
php artisan make:action Post --resource
```> `IndexPost`, `ShowPost`, `CreatePost`, `StorePost`, `EditPost`, `UpdatePost`, `DestroyPost` actions will be created
- Create actions for all API actions (`create`, `edit` excluded)
```bash
php artisan make:action Post --api
```> `IndexPost`, `ShowPost`, `StorePost`, `UpdatePost`, `DestroyPost` actions will be created
- Create actions by the specified actions
```bash
php artisan make:action Post --actions=show,destroy,approve
```> `ShowPost`, `DestroyPost`, `ApprovePost` actions will be created
- Exclude specified actions
```bash
php artisan make:action Post --resource --except=index,show,edit
```> `CreatePost`, `StorePost`, `UpdatePost`, `DestroyPost` actions will be created
- Specify namespace for actions creating (relative path)
```bash
php artisan make:action Post --resource --namespace=Post
```> `IndexPost`, `ShowPost`, `CreatePost`, `StorePost`, `EditPost`, `UpdatePost`, `DestroyPost` actions will be created under `App\Http\Actions\Post` namespace in `app/Http/Actions/Post` directory
- Specify namespace for actions creating (absolute path)
```bash
php artisan make:action ActivateUser --namespace=\\App\\Foo\\Bar
```> `ActivateUser` action will be created under `App\Foo\Bar` namespace in `app/Foo/Bar` directory
- Force create
```bash
php artisan make:action EditPost --force
```> If `EditPost` action already exists, it will be overwritten by the new one
## Registering Routes
Here are several ways to register actions in routes:
#### In separate `actions.php` route file
- Create `routes/actions.php` file (you can choose any name, it's just an example)
- Define the "action" route group in `app/Providers/RouteServiceProvider.php`> ##### With namespace auto prefixing
```php
// app/Providers/RouteServiceProvider.phpprotected function mapActionRoutes()
{
Route::middleware('web')
->namespace('App\Http\Actions')
->group(base_path('routes/actions.php'));
}
``````php
// app/Providers/RouteServiceProvider.phppublic function map()
{
$this->mapApiRoutes();$this->mapWebRoutes();
$this->mapActionRoutes();//
}
``````php
// routes/actions.phpRoute::get('/post/{post}', 'ShowPost');
```> ##### Without namespace auto prefixing
```php
// app/Providers/RouteServiceProvider.phpprotected function mapActionRoutes()
{
Route::middleware('web')
->group(base_path('routes/actions.php'));
}
``````php
// app/Providers/RouteServiceProvider.phppublic function map()
{
$this->mapApiRoutes();$this->mapWebRoutes();
$this->mapActionRoutes();//
}
``````php
// routes/actions.phpuse App\Actions\ShowPost;
Route::get('/post/{post}', ShowPost::class); // pretty sweet, isn't it? 😍
```#### In `web.php` route file
- Change the namespace for "web" group in `RouteServiceProvider.php`
```php
// app/Providers/RouteServiceProvider.phpprotected function mapWebRoutes()
{
Route::middleware('web')
->namespace('App\Http') // pay attention here
->group(base_path('routes/web.php'));
}
```- Put actions and controllers in different route groups in `routes/web.php` file and prepend an appropriate namespace for each of them
```php
// routes/web.phpRoute::group(['namespace' => 'Actions'], function () {
Route::get('/posts/{post}', 'ShowPost');
Route::delete('/posts/{post}', 'DestroyPost');
});Route::group(['namespace' => 'Controllers'], function () {
Route::get('/users', 'UserController@index');
Route::get('/users/{user}', 'UserController@show');
});
```## Bonus: Creating Plain Classes
The package also provides `make:class` console command to create plain classes:
```bash
php artisan make:class FooBar
````FooBar` class will be created under `app/Actions` folder:
````php
namespace App\Actions;class FooBar
{
/**
* Perform the action.
*
* @return mixed
*/
public function execute()
{
//
}
}
````Note 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.
## Credits
- [Sarfraz Ahmed][link-author]
- [All Contributors][link-contributors]## License
Please see the [license file](license.md) for more information.
[ico-version]: https://img.shields.io/packagist/v/sarfraznawaz2005/actions.svg?style=flat-square
[ico-downloads]: https://img.shields.io/packagist/dt/sarfraznawaz2005/actions.svg?style=flat-square[link-packagist]: https://packagist.org/packages/sarfraznawaz2005/actions
[link-downloads]: https://packagist.org/packages/sarfraznawaz2005/actions
[link-author]: https://github.com/sarfraznawaz2005
[link-contributors]: https://github.com/sarfraznawaz2005/actions/graphs/contributors