Evo is a Laravel package that leverages PHP 8 features.

Related tags

Laravel evo
Overview
Logo

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads


Evo is a Laravel package that leverages PHP 8 features. It change the way you write Laravel app into something like this:

#[RoutePrefix('users')]
class UserController extends Controller
{
    #[Get]
    public function index(
        #[Query] int $limit,
        #[Query] int $offset,
        #[Query('q')] string $keyword,
    ): UserPaginationResponse
    {
        // your logic goes here
    }

    #[Post]
    public function store(
        #[Body] CreateUserDTO $data
    ): StoreUserResponse
    {
        // your logic goes here
    }
    
    #[Put('{id}')]
    public function update(
        #[Param] int $id,
        #[Body] UpdateUserDTO $data,
    ): UpdateUserResponse
    {
        // your logic goes here
    }
}

Motivation

Defining input and output types in a head of a function will trigger your brain to specifies input and output types before writing the logic. So when it comes to write the logic, you know exactly what you have, where it comes, and what to return.

Also, by defining input and output type in this way, not only you and your teammate would easily read the specifications. Machines too. That is why Evo can provide some amazing features such as auto validation, auto casting, live swagger documentation, mocking API, etc.

Features

Installation

Evo currently is still in the development, it could have some breaking changes before the final release.

You can install the package via composer:

composer require emsifa/evo:dev-main

Usage

Routing

Register Route

To be able to use Laravel Controller in Evo's way, you have to register route with EvoFacade::routes method like below:

// routes/web.php or routes/api.php

use Emsifa\Evo\EvoFacade as Evo;

Evo::routes(App\Http\Controllers\UserController::class);

Then in your UserController, you can attach route attribute such as Get, Post, Put, Patch, Delete like an example below:



namespace App\Http\Controllers;

use Emsifa\Evo\Route\Get;
use Emsifa\Evo\Route\Post;
use Emsifa\Evo\Route\Put;
use Emsifa\Evo\Route\Delete;

class UserController extends Controller
{
    #[Get('users')]
    public function index()
    {
        // ...
    }
    
    #[Post('users')]
    public function store()
    {
        // ...
    }
    
    #[Get('users/{id}')]
    public function show($id)
    {
        // ...
    }
    
    #[Put('users/{id}')]
    public function update($id)
    {
        // ...
    }
    
    #[Delete('users/{id}')]
    public function destroy($id)
    {
        // ...
    }
}

Route Prefixing

If you want to apply route prefix into your controller, you can attach RoutePrefix attribute to your controller class.



namespace App\Http\Controllers;

use Emsifa\Evo\Route\RoutePrefix;
use Emsifa\Evo\Route\Get;
use Emsifa\Evo\Route\Post;
use Emsifa\Evo\Route\Put;
use Emsifa\Evo\Route\Delete;

#[RoutePrefix('users')]
class UserController extends Controller
{
    #[Get]
    public function index()
    {
        // ...
    }
    
    #[Post]
    public function store()
    {
        // ...
    }
    
    #[Get('{id}')]
    public function show($id)
    {
        // ...
    }
    
    #[Put('{id}')]
    public function update($id)
    {
        // ...
    }
    
    #[Delete('{id}')]
    public function destroy($id)
    {
        // ...
    }
}

Applying Middleware

Every route attribute has $middleware parameter that you can set to apply middleware. Here is some examples:



namespace App\Http\Controllers;

use Emsifa\Evo\Route\RoutePrefix;
use Emsifa\Evo\Route\Get;

#[RoutePrefix('users')]
class UserController extends Controller
{
    #[Get("/", middleware: "auth")]
    public function index()
    {
        // ...
    }
    
    #[Post("/", middleware: ["auth", "can:store-post"])]
    public function store()
    {
        // ...
    }
}

If you want to apply same middleware to every routes in a controller, you can attach RouteMiddleware to your controller class like an example below:



namespace App\Http\Controllers;

use Emsifa\Evo\Route\RoutePrefix;
use Emsifa\Evo\Route\RouteMiddleware;
use Emsifa\Evo\Route\Get;

#[RoutePrefix('users')]
#[RouteMiddleware('auth')]
class UserController extends Controller
{
    #[Get]
    public function index()
    {
        // ...
    }
    
    #[Post(middleware: "can:store-post")]
    public function store()
    {
        // ...
    }
}

Accessing Request Value

In Evo, you can access request value by attaching request attributes such as Query, Cookie, Header, Param, Body, etc to your method parameters. Then Evo will automatically inject corresponding value to attached parameter. Evo will also automatically validate and cast the value according to parameter type and definition.

Before using those attributes, make sure you have import their full class names.

use Emsifa\Evo\Http\Param;
use Emsifa\Evo\Http\Query;
use Emsifa\Evo\Http\Header;
use Emsifa\Evo\Http\Cookie;
use Emsifa\Evo\Http\Body;

Query Attribute

Query attribute is used to get a query value from HTTP request.

#[Get]
public function index(#[Query] int $page)
{
    // ...
} 

In example above, Evo will:

  1. Getting request()->query('page') value.
  2. Apply validation to the value to make sure it's numeric.
  3. Cast value to int.
  4. Inject casted value to $page parameter.

If you want to use different query and parameter name, you can set $key parameter to Query attribute like an example below:

#[Get]
public function index(#[Query('p')] int $page)
{
    // ...
} 

In example above Evo will get p query value and inject it to $page parameter.

If you want to make it optional, you can just give default value to the parameter. For example:

#[Get]
public function index(#[Query('q')] ?string $keyword = null)
{
    // ...
} 

Param Attribute

Param attribute is used to get URI parameter value.

#[Get('users/{id}')]
public function index(#[Param] int $id)
{
    // ...
} 

Like Query attribute before, Evo will do validation, type casting, and inject the value to the $id parameter.

Header Attribute

Header attribute is used to get a header value from HTTP request.

#[Get('users/{id}')]
public function index(#[Header('user-agent')] string $userAgent)
{
    // ...
} 

In example above, Evo will get user-agent header value and inject it to the $userAgent parameter.

Cookie Attribute

Cookie attribute is used to get a cookie value from HTTP request.

#[Get('users/{id}')]
public function index(#[Cookie] string $token)
{
    // ...
} 

In example above, Evo will get token cookie value and inject it to the $token parameter.

Body Attribute

Body attribute is used to get value from HTTP request body. To use Body attribute you have to use DTO class as the type of your parameter. You can create DTO class by using evo:make-dto command.

In this example, we will inject request body value as RegisterDTO instance into register method.

First, we have to generate RegisterDTO class with following command:

php artisan evo:make-dto RegisterDTO name:string email:string password:string password_confirmation:string

Then you will get app/DTO/RegisterDTO.php file contains code below:



namespace App\DTO;

use Emsifa\Evo\DTO;

class RegisterDTO extends DTO
{
    public string $name;
    public string $email;
    public string $password;
    public string $password_confirmation;
}

Now you may want to add some extra validations to each properties. You can do that by attaching rules attribute like an example below:



namespace App\DTO;

use Emsifa\Evo\DTO;
use Emsifa\Evo\Rules;

class RegisterDTO extends DTO
{
    #[Rules\Required]
    public string $name;

    #[Rules\Required]
    #[Rules\Email(message: "Incorrect email format")]
    #[Rules\Unique(table: 'users', column: 'email', message: "Email already used by someone")]
    public string $email;

    #[Rules\Required]
    #[Rules\Min(6, message: "Password must have at least 6 characters")]
    public string $password;

    #[Rules\Required]
    #[Rules\SameWith('password', message: "Password confirmation doesn't match with password")]
    public string $password_confirmation;
}

Now behind the scene, Evo will do validation like Laravel code below:

"Email already used by someone", 'password.min' => "Password must have at least 6 characters", 'password_confirmation.same' => "Password confirmation doesn't match with password", ]); ">
$request->validate([
    'name' => 'required',
    'email' => 'required|email|unique:users,email',
    'password' => 'required|min:6',
    'password_confirmation' => 'required|same:password',
], [
    'email.email' => "Incorrect email format",
    'email.unique' => "Email already used by someone",
    'password.min' => "Password must have at least 6 characters",
    'password_confirmation.same' => "Password confirmation doesn't match with password",
]);

After defining validation rules to each properties, you can inject RegisterDTO instance to your method like any other attributes before, but with Body attribute.

#[Post('register')]
public function register(#[Body] RegisterDTO $dto)
{
    // ...
}

And there you go. Same like any other attributes. Evo will validate each properties, resolving RegisterDTO instance, and inject it to $dto parameter.

Uploaded File

You can get uploaded files in two ways below:

  1. Using DTO class and Body attribute.
  2. Using File attribute.
1. Getting uploaded file using DTO and Body attribute

To get uploaded file using DTO, you can just add a property with type Illuminate\Http\UploadedFile.



namespace App\DTO;

use Emsifa\Evo\DTO;
use Emsifa\Evo\Rules;
use Illuminate\Http\UploadedFile;

class UpdateProfileDTO extends DTO
{
    #[Rules\Required]
    public string $name;

    #[Rules\Mimes(['jpeg', 'png'])]
    #[Rules\Image]
    public ?UploadedFile $avatar = null;
}

In example above Evo will inject $avatar property with request()->file('avatar') value.

2. Getting uploaded file using File attribute

If you want directly inject uploaded file instance to your controller method, you can use File attribute like an example below:

#[Post('register')]
public function register(
    #[Body] RegisterDTO $dto,
    #[File(rules: 'required|mimes:jpeg,png|image')] UploadedFile $avatar,
)
{
    // ...
}

LoggedUser Attribute

LoggedUser attribute is used to get current logged user instance from request()->user(). You can use it like any other request attributes before, just make sure if its optional, you have to make it nullable and set default value to null.

public function show(
    #[Param] int $id,
    #[LoggedUser] ?User $user = null,
)
{
    // ...
}

Creating Custom Request Attribute

You can create your own request getter attribute by creating a class implementing Emsifa\Evo\Contracts\RequestGetter interface.

For example we will create JwtToken attribute to retrieve JWT token from header or cookie.

First, create a file app/Http/Attributes/JwtToken.php.

Then you can use code below:

cookie("token"); return $token_from_header ? Str::after($token_from_header, "Bearer ") : $token_from_cookie; } } ">


namespace App\Http\Attributes;

use Attribute;
use Emsifa\Evo\Contracts\RequestGetter;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use ReflectionParameter;
use ReflectionProperty;

#[Attribute(Attribute::TARGET_PARAMETER)]
class JwtToken implements RequestGetter
{
    public function getRequestValue(Request $request, ReflectionParameter | ReflectionProperty $reflection): mixed
    {
        $token_from_header = $request->header("authorization");
        $token_from_cookie = $request->cookie("token");

        return $token_from_header
            ? Str::after($token_from_header, "Bearer ")
            : $token_from_cookie;
    }
}

Now you can use it like any other request attributes like following code:

public function doSomething(#[JwtToken] ?string $token = null)
{
    // ...
}

In most case you may want to validate the value to make sure it is safe for Evo to type cast it and inject it to your parameter. To do that, you can implement Emsifa\Evo\Contracts\RequestValidator like an example below:

cookie("token"); return $token_from_header ? Str::after($token_from_header, "Bearer ") : $token_from_cookie; } public function validateRequest(Request $request, ReflectionProperty | ReflectionParameter $reflection) { $data = [ "token" => $this->getRequestValue($request, $reflection) ]; $rules = [ "token" => "required|string", ]; Validator::make($data, $rules)->validate(); } } ">


namespace App\Http\Attributes;

use Attribute;
use Emsifa\Evo\Contracts\RequestGetter;
use Emsifa\Evo\Contracts\RequestValidator;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Validator;
use ReflectionParameter;
use ReflectionProperty;

#[Attribute(Attribute::TARGET_PARAMETER)]
class JwtToken implements RequestGetter, RequestValidator
{
    public function getRequestValue(Request $request, ReflectionParameter | ReflectionProperty $reflection): mixed
    {
        $token_from_header = $request->header("authorization");
        $token_from_cookie = $request->cookie("token");

        return $token_from_header
            ? Str::after($token_from_header, "Bearer ")
            : $token_from_cookie;
    }

    public function validateRequest(Request $request, ReflectionProperty | ReflectionParameter $reflection)
    {
        $data = [
            "token" => $this->getRequestValue($request, $reflection)
        ];

        $rules = [
            "token" => "required|string",
        ];

        Validator::make($data, $rules)->validate();
    }
}

Now Evo will run validateRequest before type casting it's value and inject it to your parameter.

Using DTO

DTO (Data Transfer Object) is basically an object that contains some declared properties in it. DTO used to carry data between processes. In typical PHP application, we often store data as associative array or as stdClass. The downside of storing data as associative array or stdClass is we don't really know what is inside and what type it is. If we are not carefully to check it, it could cause some security problem to our application.

By replacing them with DTO, we, Text Editor, and IDE know exactly what it is, what properties in it, the type of each properties, etc.

But creating DTO instance with PHP native way sometimes can be quite annoying. You have to create an instance, set the value for each properties one by one, also you have to cast its type properly.

That is why Evo provide Emsifa\Evo\DTO class that comes with some useful utilities.

In the Body Attribute section, we are creating RegisterDTO class that will be injected using Body attribute. Yes, the validation process does comes from Body attribute, but the ability to create RegisterDTO instance with the correct types for its properties is comes from its parent class: Emsifa\Evo\DTO.

Emsifa\Evo\DTO doing type casting by looking for type casters attached to it. If you look at the source of Emsifa\Evo\DTO, you will see something like this:

#[UseCaster('int', IntCaster::class)]
#[UseCaster('float', FloatCaster::class)]
#[UseCaster('string', StringCaster::class)]
#[UseCaster('bool', BoolCaster::class)]
#[UseCaster('array', ArrayCaster::class)]
#[UseCaster(DateTime::class, DateTimeCaster::class)]
#[UseCaster(Collection::class, CollectionCaster::class)]
abstract class DTO implements Arrayable
{
    ...
}

In code above, UseCaster is used to tell the DTO what caster class to be used to cast a type.

In this section we will explore how to generate DTO file, how default casters behave, and how to create and override default caster.

Generating DTO File

We can generate DTO file using evo:make-dto command. Below is the signature of the command:

php artisan evo:make-dto {classname} {...properties}

Argument classname is required and properties are optional.

For example, to create LoginDTO that have string $email and string $password properties. We should run:

php artisan evo:make-dto LoginDTO email:string password:string

It will generate app/DTO/LoginDTO.php with following code below:



namespace App\DTO;

use Emsifa\Evo\DTO;

class LoginDTO extends DTO
{
    public string $email;
    public string $password;
}

You can also generate nested object by giving the name of the object for the type. For example:

php artisan evo:make-dto SaveProfileDTO name:string contact:ContactDTO address:AddressDTO

It will generate 3 files below:

  1. app/DTO/SaveProfileDTO.php:

    
    
    namespace App\DTO;
    
    use Emsifa\Evo\DTO;
    
    class SaveProfileDTO extends DTO
    {
        public string $name;
        public ContactDTO $contact;
        public AddressDTO $contact;
    }
  2. app/DTO/ContactDTO.php (if not exists):

    
    
    namespace App\DTO;
    
    use Emsifa\Evo\DTO;
    
    class ContactDTO extends DTO
    {
    }
  3. app/DTO/AddressDTO.php (if not exists):

    
    
    namespace App\DTO;
    
    use Emsifa\Evo\DTO;
    
    class AddressDTO extends DTO
    {
    }

You can also generate typed array by adding [] sign after type name. For example:

php artisan evo:make-dto CheckoutOrderDTO items:CheckoutItemDTO[]

It will generate:

  1. app/DTO/CheckoutOrderDTO.php:

    
    
    namespace App\DTO;
    
    use Emsifa\Evo\DTO;
    use Emsifa\Evo\Types\ArrayOf;
    
    class CheckoutOrderDTO extends DTO
    {
        #[ArrayOf(CheckoutItemDTO::class)]
        public array $items;
    }
  2. app/DTO/CheckoutItemDTO.php (if not exists):

    
    
    namespace App\DTO;
    
    use Emsifa\Evo\DTO;
    
    class CheckoutItemDTO extends DTO
    {
    }

Default Casters

Here are some examples of Evo's default casters behave:

BoolCaster
Property Original Value Casted Value
bool $property null false
?bool $property null null
bool $property true true
bool $property false false
bool $property "true" true
bool $property "false" false
bool $property 1 true
bool $property 0 false
bool $property "1" true
bool $property "0" false
bool $property 123 CastErrorException
bool $property 123.45 CastErrorException
bool $property "123" CastErrorException
bool $property "123.45" CastErrorException
bool $property "lorem ipsum" CastErrorException
bool $property [] CastErrorException
bool $property [1,2,3] CastErrorException
bool $property stdClass CastErrorException
IntCaster
Property Original Value Casted Value
int $number null 0
?int $number null null
int $number 123 123
int $number "123" 123
int $number 123.45 123
int $number "123.45" 123
int $number "123.99" 123
int $number true CastErrorException
int $number false CastErrorException
int $number "123-ipsum" CastErrorException
int $number "123-ipsum" CastErrorException
int $number [] CastErrorException
int $number stdClass CastErrorException
FloatCaster
Property Original Value Casted Value
float $number null 0
?float $number null null
float $number 123 123.0
float $number "123" 123.0
float $number 123.45 123.45
float $number "123.45" 123.45
float $number "123.99" 123.99
float $number true CastErrorException
float $number false CastErrorException
float $number "123-ipsum" CastErrorException
float $number "123-ipsum" CastErrorException
float $number [] CastErrorException
float $number stdClass CastErrorException
FloatCaster
Property Original Value Casted Value
float $number null 0
?float $number null null
float $number 123 123.0
float $number "123" 123.0
float $number 123.45 123.45
float $number "123.45" 123.45
float $number "123.99" 123.99
float $number true CastErrorException
float $number false CastErrorException
float $number "123-ipsum" CastErrorException
float $number "123-ipsum" CastErrorException
float $number [] CastErrorException
float $number stdClass CastErrorException
StringCaster
Property Original Value Casted Value
string $str null ""
?string $str null null
string $str true "true"
string $str false "false"
string $str "123" "123"
string $str 123 "123"
string $str 123.45 "123.45"
string $str 123.0 "123"
string $str Stringable __toString()
string $str [] CastErrorException
ArrayCaster

ArrayCaster basically only accept array and Illuminate\Contracts\Support\Arrayable value, other than that it throws Emsifa\Evo\Exceptions\CastErrorException.

ArrayCaster will check Emsifa\Evo\Types\ArrayOf attribute to cast its items.

For example if you have property like this:

#[ArrayOf('int')]
public array $numbers;

And inject it with value:

[1, "2", "3.0", null]

Evo will apply int caster to each items, so the result would be:

[1, 2, 3, 0]

In example above, if you inject it with value:

[1, "2", "lorem-ipsum", null]

It will throw Emsifa\Evo\Exceptions\CastErrorException.

But sometimes you may want to treat it differently. Instead of thrown an error, you may want to skip the item, make it null, or just keep as is. That is why the second parameter of ArrayOf comes in handy.

You can use ArrayOf::SKIP_ITEM, ArrayOf::NULL_ITEM, ArrayOf::KEEP_AS_IS as second parameter of ArrayOf attribute.

For example, if you change the property in example above like this:

#[ArrayOf('int', ArrayOf::SKIP_ITEM)]
public array $numbers;

And if you inject it with value:

[1, "2", "lorem-ipsum", "4.2", "5"]

It will give you this result:

[1, 2, 4, 5]

If you change second parameter to ArrayOf::NULL_ITEM, it would give you the result below:

[1, 2, null, 4, 5]

Lastly if you change second parameter to ArrayOf::KEEP_AS_IS, it would give you this result:

[1, 2, "lorem-ipsum", 4, 5]
CollectionCaster

Same like ArrayCaster, but it will apply collect($result) to transform the result into Illuminate\Support\Collection instance.

DateTimeCaster

DateTimeCaster will transform any string that is accepted by date_create() function to DateTime object.

For example:

public DateTime $date;

It would transform string "2010-01-02", "2010-01-02 12:34:56", etc into DateTime instance.

If date_create() returns null, it would throws Emsifa\Evo\Exceptions\CastErrorException.

Creating Custom Type Caster

To create your own type caster, you have to create a class implementing Emsifa\Evo\Contracts\Caster interface.

In this example we will create a boolean caster. Also we will override Evo's default boolean caster with our custom caster.

First, let's create file BoolCaster.php in app/Casters directory. If you don't have Casters directory yet, just create it.

Then let's write this blank caster code:



namespace App\Casters;

use Emsifa\Evo\Contracts\Caster;
use Emsifa\Evo\Exceptions\CastErrorException;
use ReflectionParameter;
use ReflectionProperty;

class BoolCaster implements Caster
{
    public function cast($value, ReflectionProperty | ReflectionParameter $prop): mixed
    {
        // ...
    }
}

Now we will put some logic to our cast method. For example in this custom BoolCaster we want to convert string "1", "true", "on", "yes" to true and "0", "false", "no", "off" to false. Also, we want to make sure if our property is nullable, and the value is null, we should returns null. Else we will throw Emsifa\Evo\Exceptions\CastErrorException.

null, in_array($value, $truthy, true) => true, in_array($value, $falsy, true) => false, default => throw new CastErrorException("Cannot cast boolean from type: " . gettype($value)), }; } } ">
class BoolCaster implements Caster
{
    public function cast($value, ReflectionProperty | ReflectionParameter $prop): mixed
    {
        $nullable = optional($prop->getType())->allowsNull();
        $truthy = [true, "true", 1, "1", "yes", "on"];
        $falsy = [false, "false", 0, "0", "no", "off"];

        return match (true) {
            $nullable && is_null($value) => null,
            in_array($value, $truthy, true) => true,
            in_array($value, $falsy, true) => false,
            default => throw new CastErrorException("Cannot cast boolean from type: " . gettype($value)),
        };
    }
}

Now our BoolCaster is done. To use our BoolCaster, we can attach Emsifa\Evo\DTO\UseCaster attribute to every DTO classes wa want to apply.

In this example we will attach it to LoginDTO class.



namespace App\DTO;

use Emsifa\Evo\DTO;
use Emsifa\Evo\DTO\UseCaster;
use App\Casters\BoolCaster;

#[UseCaster('bool', BoolCaster::class)]
class LoginDTO extends DTO
{
    public string $email;
    public string $password;
    public bool $remember;
}

Now, our BoolCaster will be applied to cast bool $remember property.

If you want to apply it to all of your DTO classes, you can create your own abstract DTO class to be used as a parent to all of your DTO classes.

For example, create your own app/DTO/DTO.php file with code below:



namespace App\DTO;

use Emsifa\Evo\DTO as EvoBaseDTO;
use Emsifa\Evo\DTO\UseCaster;
use App\Casters\BoolCaster;

#[UseCaster('bool', BoolCaster::class)]
abstract class DTO extends EvoBaseDTO
{
}

Then in your DTO classes you just have to extends App\DTO\DTO class instead of Emsifa\Evo\DTO.

Validation

Evo will automatically validate request data to every parameters that is attached with attribute that implements Emsifa\Evo\Contracts\RequestValidator interface. Evo's built-in Query, Header, Cookie, Param, and Body attributes is doing validation because they are implementing that interface.

Validating Query, Header, Cookie, and Param

For Query, Header, Cookie, and Param attributes, by default they choose validation rules by looking for its parameter type. For example:

#[Get]
public function index(
    #[Query] int $limit,
    #[Query] int $offset,
    #[Query] ?string $keyword = null,
)
{
}

Evo will apply required|numeric to $limit and $offset, also apply rule nullable|string rule to $keyword.

But you can also use your own validation rules by giving $rules to attribute. For example:

#[Get]
public function index(
    #[Query(rules: 'required|numeric|min:10')] int $limit,
    #[Query(rules: 'required|numeric|min:0')] int $offset,
    #[Query] ?string $keyword = null,
)
{
}

Now $limit and $offset parameters will use your defined rules instead of using the default rules.

Validating Body

For Body attribute. It will scan its DTO properties, and choose rules for each properties by looking for the property type and finding Illuminate\Contracts\Validation\Rule attributes attached to it.

For example, if you have controller like this:

#[Post('checkout')]
public function checkout(#[Body] CheckoutOrderDTO $dto)
{
    // ...
}

Where CheckoutOrderDTO has properties like this:



namespace App\DTO;

use Emsifa\Evo\DTO;
use Emsifa\Evo\Rules;

class CheckoutOrderDTO extends DTO
{
    #[Rules\Required]
    #[Rules\Numeric]
    public string $phone;

    #[Rules\Required]
    public string $address;

    #[Rules\Required]
    #[Rules\Numeric]
    #[Rules\Size(11)]
    public string $subdistrict_id;

    #[Rules\Required]
    #[Rules\Numeric]
    #[Rules\Size(5)]
    public string $postal_code;

    #[ArrayOf(CheckoutOrderItemDTO::class)]
    public array $items;
}

And CheckoutOrderItemDTO like this:



namespace App\DTO;

use Emsifa\Evo\DTO;
use Emsifa\Evo\Rules;

class CheckoutOrderItemDTO extends DTO
{
    #[Rules\Required]
    #[Rules\Exists('products', 'id')]
    public string $product_id;

    #[Rules\Required]
    #[Rules\Numeric]
    public int $qty;
}

The Body attribute will do this following validation for you:

Validator::make($data, [
    'phone' => 'required|string|numeric',
    'address' => 'required|string',
    'subdistrict_id' => 'required|string|numeric|size:11',
    'postal_code' => 'required|string|numeric|size:5',
    'items' => 'required|array',
    'items.*.product_id' => 'required|string|exists:product,id',
    'items.*.qty' => 'required|numeric',
]);
Create Your Own Validation Rule

Evo's validation rule is basically a class implementing Illuminate\Contracts\Validation\Rule interface and has Attribute attribute attached on it.

For example, we will create Bit rule that only accepts string with "0" and "1" characters.

First, create app/Rules/Bit.php file.

Then write following code below:

message) ?: __("validation.bit") ?: "Invalid bit string"; } } ">


namespace App\Rules;

use Attribute;
use Illuminate\Contracts\Validation\Rule;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Bit implements Rule
{
    public function __construct(protected string $message = '')
    {
    }

    public function passes($attribute, $value)
    {
        return is_string($value) && preg_match("/^(0|1)+$/", $value);
    }

    public function message()
    {
        return __($this->message) ?: __("validation.bit") ?: "Invalid bit string";
    }
}

That's it! now you can attach it to your DTO's property like this:



namespace App\DTO;

use Emsifa\Evo\DTO;
use Emsifa\Evo\Rules;
use App\Rules\Bit;

class MyDTO extends DTO
{
    #[Rules\Required]
    #[Bit]
    public string $bit_string;
}

Using Response

Evo provide custom response class Emsifa\Evo\Http\Response\JsonResponse and Emsifa\Evo\Http\Response\ViewResponse that is inherit from Emsifa\Evo\DTO class. They are all implements Illuminate\Contracts\Support\Responsable, so that they can be transformed to HTTP response.

You can extend them to your response classes then use your response classes as return type of your controller action to get these benefits below:

  • Easier to identify how the response data should be.
  • It prevents you to send wrong data types. Eg: integer as string, null to non-nullable property, etc.
  • It makes Evo knows how to generate your endpoint into OpenAPI.
  • It makes Evo knows how to mock the response.

Generate Response Class

Evo provide evo:make-response command to generate response class. Here is its command signature:

php artisan evo:make-response {classname} {...properties} {--view} {--json-template=} 
  • classname (required): class name to be generated.
  • properties (optional): class properties.
  • --view (optional): by default evo:make-response command will use JsonResponse as parent class, if you want to use ViewResponse, you can add this option.
  • --json-template=: add this if you want to apply UseJsonTemplate attribute in generated response class.

For example, we will generate StoreTodoResponse with command below:

php artisan evo:make-response StoreTodoResponse id:int title:string completed:bool

It will generate app/Http/Responses/StoreTodoResponse class with following code below:



namespace App\Http\Responses;

use Emsifa\Evo\Http\Response\JsonResponse;

class StoreTodoResponse extends JsonResponse
{
    public int $id;
    public string $title;
    public bool $completed;
}

Using Response Class

To use StoreTodoResponse you need to put it as return type, then in your controller you can use fromArray method like an example below:

#[Post]
public function store(#[Body] StoreTodoDTO $dto): StoreTodoResponse
{
    $todo = Todo::create($dto);

    return StoreTodoResponse::fromArray($todo);
}

StoreTodoResponse::fromArray method will create StoreTodoResponse instance by mapping StoreTodoResponse public properties with $todo array values. It also apply type casting for each properties.

Using View Response

In this example we will create view response in Evo's way.

First let's generate our response view with following command:

php artisan evo:make-response EditTodoResponse todo:TodoDTO --view

Since you put TodoDTO type there, it will generate 2 files below:

  1. app/Http/Responses/EditTodoResponse:

    
    
    namespace App\Http\Responses;
    
    use Emsifa\Evo\Http\Response\UseView;
    use Emsifa\Evo\Http\Response\ViewResponse;
    
    #[UseView('edit-todo')]
    class EditTodoResponse extends ViewResponse
    {
        public TodoDTO $todo;
    }
  2. app/Http/Responses/TodoDTO:

    
    
    namespace App\Http\Responses;
    
    use Emsifa\Evo\Http\Response\ResponseDTO;
    
    class TodoDTO extends ResponseDTO
    {
    }

UseView attribute above is used to identify what view file should be rendered, and the data passed to the view is its properties, in this case $todo that is TodoDTO instance.

Now, let's edit TodoDTO with following properties:



namespace App\Http\Responses;

use DateTime;
use Emsifa\Evo\Http\Response\ResponseDTO;

class TodoDTO extends ResponseDTO
{
    public int $id;
    public string $title;
    public bool $is_completed;
    public DateTime $created_at;
    public ?DateTime $updated_at;
}

And here is the example on how to use EditTodoResponse view:

#[Get('{id}')]
public function edit(#[Param] int $id): EditTodoResponse
{
    $todo = Todo::findOrFail($id);

    return EditTodoResponse::fromArray(['todo' => $todo]);
}

Yes, Evo will automatically transform your Todo model into TodoDTO instance. Means it will map id, title, is_completed, created_at, and updated_at value from database into TodoDTO instance properties.

Define Response Status

To define response status in your response class, you can attach Emsifa\Evo\Http\Response\ResponseStatus attribute in your response class like following code below:



namespace App\Http\Responses;

use Emsifa\Evo\Http\Response\JsonResponse;
use Emsifa\Evo\Http\Response\ResponseStatus;

#[ResponseStatus(201)]
class StoreTodoResponse extends JsonResponse
{
    public int $id;
    public string $title;
    public bool $completed;
}

Now, when you return StoreTodoResponse, Evo will set response status to 201.

Using Json Template

Evo provide Emsifa\Evo\Contracts\JsonTemplate class to wrap your JsonResponse instance.

When building RESTful API you may have a convention/standard on how your data formatted.

For example we want our StoreTodoResponse instead of just rendered like this:

{
    "id": 1,
    "title": "Write documentation",
    "completed": false
}

We want it to be wrapped like this:

{
    "status": "ok",
    "data": {
        "id": 1,
        "title": "Write documentation",
        "completed": false
    }
}

To do this, we have to create a JSON response class that implements Emsifa\Evo\Contracts\JsonTemplate as a wrapper to all of your JSON response classes.

Let's create file app/Http/Responses/SuccessJsonTemplate.php with following code:

data = $response->getData(); return $this; } } ">


namespace App\Http\Responses;

use Emsifa\Evo\Contracts\JsonData;
use Emsifa\Evo\Contracts\JsonTemplate as JsonTemplateContract;
use Emsifa\Evo\Http\Response\JsonResponse;

class SuccessJsonTemplate implements JsonTemplateContract
{
    public string $status;
    public JsonData $data;

    /**
     * @param BaseResponse $response
     */
    public function forJsonResponse(JsonResponse $response): static
    {
        $this->status = "ok";
        $this->data = $response->getData();

        return $this;
    }
}

Now you can use this template using Emsifa\Evo\Http\Response\UseJsonTemplate like code below:



namespace App\Http\Responses;

use Emsifa\Evo\Http\Response\JsonResponse;
use Emsifa\Evo\Http\Response\UseJsonTemplate;

#[UseJsonTemplate(SuccessJsonTemplate::class)]
class StoreTodoResponse extends JsonResponse
{
    public int $id;
    public string $title;
    public bool $completed;
}

Now whenever you return StoreTodoResponse in your controller, it will be wrapped with SuccessJsonTemplate data.

Swagger UI

Swagger UI is a web based application to visualize and interacts with API's resources without having any of the implementation logic in place. It reads OpenAPI schema to display and interact with API endpoints.

Evo can reflect your code and generate OpenAPI schema on-the-fly, so you can display Swagger UI with minimal configuration.

In this section we will guide you on how to use Swagger UI with Evo.

Publishing Assets Files

First, you have to publish assets files that will be used by Swagger UI page.

php artisan vendor:publish --tag=evo-assets

Register Swagger Routes

Then, you have to register two routes. First route is for displaying Swagger UI and second route is for rendering OpenAPI specification as JSON.

To do that, you can simply add this line to your routes/web.php:

Evo::swagger('/docs');

Now if you run php artisan route:list, you should see GET /docs and GET /docs/openapi routes there.

You can check it by running your app with php artisan serve, then in your browser, open URL http://localhost:8000/docs.

You should see Swagger UI page with no endpoints yet.

Add an Example Endpoint

Now we will create an example endpoint to be displayed in our Swagger UI.

First, let's create controller with following command:

php artisan make:controller ExampleController

Then, add this to your routes/api.php:

Evo::routes(ExampleController::class);

Now, let's add a route in our ExampleController:



namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Emsifa\Evo\Route\Post;
use Emsifa\Evo\Route\RoutePrefix;

#[RoutePrefix('examples')]
class ExampleController extends Controller
{
    #[Post('post-something')]
    public function postSomething()
    {

    }
}

This time Evo will not render any endpoint to Swagger UI because Evo will only register endpoints that returns Emsifa\Evo\Http\Response\JsonResponse instance.

Let's create DTO and JsonResponse class for our postSomething method by running following commands.

php artisan evo:make-dto PostSomethingDTO number:int message:string
php artisan evo:make-response PostSomethingResponse number:int message:string

Now we can use it in postSomething method like this:



namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Emsifa\Evo\Route\Post;
use Emsifa\Evo\Route\RoutePrefix;
use App\DTO\PostSomethingDTO;
use App\Http\Responses\PostSomethingResponse;

#[RoutePrefix('examples')]
class ExampleController extends Controller
{
    #[Post('post-something')]
    public function postSomething(
        #[Body] PostSomethingDTO $dto
    ): PostSomethingResponse
    {
        return PostSomethingResponse::fromArray($dto);
    }
}

Save it. Now if you back to your browser http://localhost:8000/docs, you will see there is POST /api/examples/post-something operation a.k.a endpoint.

Configuring OpenAPI

To configure OpenAPI, Evo provides some attributes modifier that modify OpenAPI schema. Also Evo have configuration file to modify some information in OpenAPI.

To configure info with configuration file. First you have to publish configuration file by running following command:

php artisan vendor:publish --tag=evo-config

It will create config/evo.php in your project directory. Take a look, and modify it as you want.

Example Attribute

Emsifa\Evo\Swagger\OpenAPI\Example attribute is used to define example value to response or DTO's property.

For example, if you expand our POST /api/examples/post-something in Swagger UI, you will see our response example value like this:

{
    "number": 0,
    "message": "string"
}

If you want to modify it with more realistic example, we can modify PostSomethingResponse into something like this:



namespace App\Http\Responses;

use Emsifa\Evo\Http\Response\JsonResponse;
use Emsifa\Evo\Swagger\OpenApi\Example;

class PostSomethingResponse extends JsonResponse
{
    #[Example(12)]
    public int $number;

    #[Example("Lorem dolor sit amet")]
    public string $message;
}

Now if you refresh Swagger UI page, you will see that it shows our defined example value.

Summary Attribute

Emsifa\Evo\Swagger\OpenApi\Summary attribute is used to define summary to your endpoint operation.

You can attach it to your controller method like this:

#[RoutePrefix('examples')]
class ExampleController extends Controller
{
    #[Post('post-something')]
    #[Summary('Post Something')]
    public function postSomething(
        #[Body] PostSomethingDTO $dto
    ): PostSomethingResponse
    {
        return PostSomethingResponse::fromArray($dto);
    }
}

Now if you refresh Swagger UI, you will see "Post Something" in POST /api/examples/post-something endpoint.

Description Attribute

Emsifa\Evo\Swagger\OpenApi\Description attribute is used to define description for operation, schema (properties), response class, or DTO class.

For example we will add description to PostSomethingResponse and its properties like following code below:



namespace App\Http\Responses;

use Emsifa\Evo\Http\Response\JsonResponse;
use Emsifa\Evo\Swagger\OpenApi\Example;
use Emsifa\Evo\Swagger\OpenApi\Description;

#[Description("Post something succeed")]
class PostSomethingResponse extends JsonResponse
{
    #[Description("A random number")]
    #[Example(12)]
    public int $number;

    #[Description("A message to stored")]
    #[Example("Lorem dolor sit amet")]
    public string $message;
}

Now if you refresh Swagger UI, you will see those description shows there.

Create Custom OpenAPI Modifiers

To use create custom OpenAPI modifiers, Evo provides some interfaces that you can use to your custom attribute.

Those interfaces are:

  • Emsifa\Evo\Contracts\OpenApiPathModifier
  • Emsifa\Evo\Contracts\OpenApiSchemaModifier
  • Emsifa\Evo\Contracts\OpenApiResponseModifier
  • Emsifa\Evo\Contracts\OpenApiOperationModifier
  • Emsifa\Evo\Contracts\OpenApiParameterModifier
  • Emsifa\Evo\Contracts\OpenApiRequestBodyModifier

For example, you can take a look to Description attribute:



namespace Emsifa\Evo\Swagger\OpenApi;

use Attribute;
use Emsifa\Evo\Contracts\OpenApiOperationModifier;
use Emsifa\Evo\Contracts\OpenApiParameterModifier;
use Emsifa\Evo\Contracts\OpenApiRequestBodyModifier;
use Emsifa\Evo\Contracts\OpenApiResponseModifier;
use Emsifa\Evo\Swagger\OpenApi\Schemas\Operation;
use Emsifa\Evo\Swagger\OpenApi\Schemas\Parameter;
use Emsifa\Evo\Swagger\OpenApi\Schemas\RequestBody;
use Emsifa\Evo\Swagger\OpenApi\Schemas\Response;

#[Attribute]
class Description implements
    OpenApiRequestBodyModifier,
    OpenApiParameterModifier,
    OpenApiOperationModifier,
    OpenApiResponseModifier
{
    public function __construct(protected string $description)
    {
    }

    public function modifyOpenApiRequestBody(RequestBody $body, mixed $reflection = null)
    {
        $body->description = $this->description;
    }

    public function modifyOpenApiParameter(Parameter $parameter)
    {
        $parameter->description = $this->description;
    }

    public function modifyOpenApiOperation(Operation $operation)
    {
        $operation->description = $this->description;
    }

    public function modifyOpenApiResponse(Response $response)
    {
        $response->description = $this->description;
    }
}

Mocking API

Mocking API is a feature that allows your controller to send response with fake data.

When developing REST API with a team consisting of Back-end and Front-end Developers. Creating an API implementation could takes time. Sometimes it makes Front-end developer have to wait Back-end Developer to finish the implementation. This will make development time less efficient.

To make development time more efficient. We can use Mocking API so Front-end Developer can consume our API without waiting for real implementation to be done.

To do that, in Evo you can simply attach Emsifa\Evo\Http\Response\Mock attribute to your controller method. So when user call that endpoint, Evo will prevent its controller to be executed, instead Evo will read its return type, create return type instance and fill its properties with fake data, finally Evo will respond that fake instance.

Using Mock Attribute

For example, we will use ExampleController in Swagger UI section before.

To respond mock on postSomething method, we need to attach Emsifa\Evo\Http\Response\Mock like an example below:



namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Emsifa\Evo\Route\Post;
use Emsifa\Evo\Route\RoutePrefix;
use Emsifa\Evo\Http\Response\Mock;
use App\DTO\PostSomethingDTO;
use App\Http\Responses\PostSomethingResponse;

#[RoutePrefix('examples')]
class ExampleController extends Controller
{
    #[Post('post-something')]
    #[Mock]
    public function postSomething(
        #[Body] PostSomethingDTO $dto
    ): PostSomethingResponse
    {
        return PostSomethingResponse::fromArray($dto);
    }
}

That's it!

Now to try this, open your Swagger UI page. Expand POST /api/examples/post-something endpoint, click Try it out, then click execute.

You should see it responded with random data.

Using Specific Faker

Behind the scene Evo uses Faker to generate mock data. By default Evo choosing faker to be used is by looking the data type and the name of each property.

For example, if you have $name property, Evo will use $faker->name(). For $title property, Evo will use $faker->title(). For float $latitude, Evo will use $faker->latitude(). If the property name doesn't have related faker formatter available, Evo will choose faker formatter by it's data type.

If you want to use another faker instead of default choosed faker. You can use Emsifa\Evo\DTO\UseFaker to define what formatter you want to use.

For example, we will use paragraph(5) formatter to $message property.

Here is the code:



namespace App\Http\Responses;

use Emsifa\Evo\DTO\UseFaker;
use Emsifa\Evo\Http\Response\JsonResponse;

#[Description("Post something succeed")]
class PostSomethingResponse extends JsonResponse
{
    public int $number;

    #[UseFaker("paragraph", 5)]
    public string $message;
}

Now when you execute your endpoint again, it will respond message with paragraph contains 5 sentences.

Create Custom Faker

If you want to use your own custom faker formatter, you can create a class implementing Emsifa\Evo\Contracts\ValueFaker.

For example, we will create MealsFaker that will resulting a random meals name.

First, create app/Fakers/MealsFaker.php file.

Fill it with code below:



namespace App\Fakers;

use Emsifa\Evo\Contracts\ValueFaker;
use Faker\Generator;
use ReflectionProperty;

class MealsFaker implements ValueFaker
{
    public function generateFakeValue(Generator $faker, ReflectionProperty $property): mixed
    {
        return $faker->randomElement([
            "Kebab",
            "Ramen",
            "Fried Chicken",
            "Pizza",
            "Sandwich"
        ]);
    }
}

Now to use it, we will add new $meal property to our PostSomethingResponse, so our PostSomethingResponse.php would be like this:



namespace App\Http\Responses;

use Emsifa\Evo\DTO\UseFaker;
use Emsifa\Evo\Http\Response\JsonResponse;
use Emsifa\Fakers\MealsFaker;

#[Description("Post something succeed")]
class PostSomethingResponse extends JsonResponse
{
    public int $number;

    #[UseFaker("paragraph", 5)]
    public string $message;

    #[UseFaker(MealsFaker::class)]
    public string $meal;
}

Now if you execute your endpoint again, you will get meal data that is could be "Kebab", "Ramen", "Fried Chicken", "Pizza", or "Sandwich".

FakesCount Attribute

FakesCount attribute is used to define how many fake items you want to be generated into your array property.

For example, we will add $numbers property to PostSomethingResponse that is attached with FakesCount attribute:



namespace App\Http\Responses;

use Emsifa\Evo\DTO\UseFaker;
use Emsifa\Evo\DTO\FakesCount;
use Emsifa\Evo\Types\ArrayOf;
use Emsifa\Evo\Http\Response\JsonResponse;
use Emsifa\Fakers\MealsFaker;

#[Description("Post something succeed")]
class PostSomethingResponse extends JsonResponse
{
    public int $number;

    #[UseFaker("paragraph", 5)]
    public string $message;

    #[UseFaker(MealsFaker::class)]
    public string $meal;
    
    #[ArrayOf('int')]
    #[FakesCount(5)]
    public array $numbers;
}

It will generate 5 random numbers when you execute your mock API.

Ignoring Mock

In local development environment, you may not want your API to be mocked. Instead of remove Mock attribute temporarily, you can just add IGNORE_MOCK=true in your .env file.

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.

Comments
  • Strange behaviour with string property validation

    Strange behaviour with string property validation

    For example we have DTO like this:

    class SomethingDto extends Dto
    {
        public string $message;
    }
    

    When we send data below:

    {
        "message": ""
    }
    

    It results an error "Message must be a string".

    Expectation

    It should not error because an empty string is a string.

    bug 
    opened by emsifa 1
  • Fix ControllerDispatcher on cached routes

    Fix ControllerDispatcher on cached routes

    In this PR we modify EvoServiceProvider::registeringPackage method, check if routes are cached, we bind Illuminate\Routing\Contracts\ControllerDispatcher to Emsifa\Evo\ControllerDispatcher.

    The drawbacks with this approach is some features on Laravel built-in ControllerDispatcher, such as model binding may not work. So later we have to note this in the documentation.

    Close #180

    opened by emsifa 1
  • Add prefix and middleware parameter to Evo::routes

    Add prefix and middleware parameter to Evo::routes

    This PR add optional parameters string $prefix and string|array|null $middleware to Evo::routes method. So we can register routes using same controller with different path and middleware.

    opened by emsifa 1
  • Feature: HashMap type

    Feature: HashMap type

    HashMap type used to tell OpenApi generator to put additionalProperties to attached property.

    See:

    https://swagger.io/docs/specification/data-models/dictionaries/

    enhancement 
    opened by emsifa 1
  • Feature: Toggle Ignore Mock

    Feature: Toggle Ignore Mock

    Currently when we put Mock attribute to a method, any environment including local environment will response mocked response. Which cause developers should remove the Mock attribute to test their code.

    So, that is why I think we need an envinronment value to indicate that the environment should ignores Mock attribute.

    enhancement 
    opened by emsifa 1
  • Nullable string in DTO still asks for a string

    Nullable string in DTO still asks for a string

    When we have DTO:

    class SomethingDto extends Dto
    {
        public ?string $message;
    }
    

    And we try to fill with request data:

    {
        "message": null
    }
    

    It results an error "The message must be a string.".

    When I dump $rules in Emsifa\Evo\Http\Body::validateRequest method, it doesn't have nullable rule.

    opened by emsifa 0
  • Presence verifier has not been set when trying to use Exists rule in child Dto

    Presence verifier has not been set when trying to use Exists rule in child Dto

    This happens when we use Exists rule in child Dto, eg:

    class StoreUserDto extends Dto
    {
        public string $name;
        public string $username;
        #[ArrayOf(SocialDto::class)]
        public array $socials;
    }
    
    class SocialDto extends Dto
    {
        #[Required]
        #[Exists('social_types', 'id')]
        public int $social_type_id;
    
        #[Required]
        public string $username;
    }
    

    Error detail:

    {
      "message": "Presence verifier has not been set.",
      "exception": "RuntimeException",
      "file": "project_path\\vendor\\emsifa\\evo\\src\\Rules\\Concerns\\PresenceVerifier.php",
      "line": 21,
      "trace": [
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Validation\\Concerns\\ValidatesAttributes.php",
          "line": 724,
          "function": "getPresenceVerifier",
          "class": "Emsifa\\Evo\\Rules\\Exists",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Validation\\Concerns\\ValidatesAttributes.php",
          "line": 708,
          "function": "getExistCount",
          "class": "Emsifa\\Evo\\Rules\\Exists",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\emsifa\\evo\\src\\Rules\\Exists.php",
          "line": 29,
          "function": "validateExists",
          "class": "Emsifa\\Evo\\Rules\\Exists",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Validation\\Validator.php",
          "line": 793,
          "function": "passes",
          "class": "Emsifa\\Evo\\Rules\\Exists",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Validation\\Validator.php",
          "line": 597,
          "function": "validateUsingCustomRule",
          "class": "Illuminate\\Validation\\Validator",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Validation\\Validator.php",
          "line": 412,
          "function": "validateAttribute",
          "class": "Illuminate\\Validation\\Validator",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Validation\\Validator.php",
          "line": 443,
          "function": "passes",
          "class": "Illuminate\\Validation\\Validator",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Validation\\Validator.php",
          "line": 485,
          "function": "fails",
          "class": "Illuminate\\Validation\\Validator",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\emsifa\\evo\\src\\Http\\Body.php",
          "line": 96,
          "function": "validate",
          "class": "Illuminate\\Validation\\Validator",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\emsifa\\evo\\src\\ControllerDispatcher.php",
          "line": 212,
          "function": "validateRequest",
          "class": "Emsifa\\Evo\\Http\\Body",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\emsifa\\evo\\src\\ControllerDispatcher.php",
          "line": 192,
          "function": "getParameterValue",
          "class": "Emsifa\\Evo\\ControllerDispatcher",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\emsifa\\evo\\src\\ControllerDispatcher.php",
          "line": 41,
          "function": "resolveParameters",
          "class": "Emsifa\\Evo\\ControllerDispatcher",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Route.php",
          "line": 261,
          "function": "dispatch",
          "class": "Emsifa\\Evo\\ControllerDispatcher",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Route.php",
          "line": 204,
          "function": "runController",
          "class": "Illuminate\\Routing\\Route",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php",
          "line": 695,
          "function": "run",
          "class": "Illuminate\\Routing\\Route",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 128,
          "function": "Illuminate\\Routing\\{closure}",
          "class": "Illuminate\\Routing\\Router",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Middleware\\SubstituteBindings.php",
          "line": 50,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Routing\\Middleware\\SubstituteBindings",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Middleware\\ThrottleRequests.php",
          "line": 127,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Middleware\\ThrottleRequests.php",
          "line": 103,
          "function": "handleRequest",
          "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Middleware\\ThrottleRequests.php",
          "line": 55,
          "function": "handleRequestUsingNamedLimiter",
          "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Auth\\Middleware\\Authenticate.php",
          "line": 44,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Auth\\Middleware\\Authenticate",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\sanctum\\src\\Http\\Middleware\\EnsureFrontendRequestsAreStateful.php",
          "line": 33,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 128,
          "function": "Laravel\\Sanctum\\Http\\Middleware\\{closure}",
          "class": "Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 103,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\sanctum\\src\\Http\\Middleware\\EnsureFrontendRequestsAreStateful.php",
          "line": 34,
          "function": "then",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 103,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php",
          "line": 697,
          "function": "then",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php",
          "line": 672,
          "function": "runRouteWithinStack",
          "class": "Illuminate\\Routing\\Router",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php",
          "line": 636,
          "function": "runRoute",
          "class": "Illuminate\\Routing\\Router",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Router.php",
          "line": 625,
          "function": "dispatchToRoute",
          "class": "Illuminate\\Routing\\Router",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Kernel.php",
          "line": 166,
          "function": "dispatch",
          "class": "Illuminate\\Routing\\Router",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 128,
          "function": "Illuminate\\Foundation\\Http\\{closure}",
          "class": "Illuminate\\Foundation\\Http\\Kernel",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\fruitcake\\laravel-cors\\src\\HandleCors.php",
          "line": 52,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Fruitcake\\Cors\\HandleCors",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest.php",
          "line": 21,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull.php",
          "line": 31,
          "function": "handle",
          "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest.php",
          "line": 21,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\TrimStrings.php",
          "line": 40,
          "function": "handle",
          "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Foundation\\Http\\Middleware\\TrimStrings",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize.php",
          "line": 27,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance.php",
          "line": 86,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\fruitcake\\laravel-cors\\src\\HandleCors.php",
          "line": 52,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Fruitcake\\Cors\\HandleCors",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Http\\Middleware\\TrustProxies.php",
          "line": 39,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 167,
          "function": "handle",
          "class": "Illuminate\\Http\\Middleware\\TrustProxies",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Pipeline\\Pipeline.php",
          "line": 103,
          "function": "Illuminate\\Pipeline\\{closure}",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Kernel.php",
          "line": 141,
          "function": "then",
          "class": "Illuminate\\Pipeline\\Pipeline",
          "type": "->"
        },
        {
          "file": "project_path\\vendor\\laravel\\framework\\src\\Illuminate\\Foundation\\Http\\Kernel.php",
          "line": 110,
          "function": "sendRequestThroughRouter",
          "class": "Illuminate\\Foundation\\Http\\Kernel",
          "type": "->"
        },
        {
          "file": "project_path\\public\\index.php",
          "line": 52,
          "function": "handle",
          "class": "Illuminate\\Foundation\\Http\\Kernel",
          "type": "->"
        },
        {
          "file": "project_path\\server.php",
          "line": 21,
          "function": "require_once"
        }
      ]
    }
    
    bug 
    opened by emsifa 0
  • OpenAPI generate wrong security schema

    OpenAPI generate wrong security schema

    configs/evo.php:

    'security_schemes' => [
        'sanctum' => [
            'type' => 'http',
            'scheme' => 'bearer',
        ],
    ],
    

    Controller:

    #[Get('something')]
    #[UseGuards('sanctum')]
    public function getSomething(): GetSomethingResponse
    {
    
    }
    

    Generated security on GET /someting operation:

    {
        "0": {
            "sanctum": [ ]
        },
        "sanctum": {
            "key": "sanctum",
            "names": [ ]
        }
    }
    

    Expectation:

    Generated security should be like this:

    [
        { "sanctum": [] }
    ]
    
    opened by emsifa 0
  • ResponseStatus doesn't set on error response from UseErrorResponse

    ResponseStatus doesn't set on error response from UseErrorResponse

    Controller:

    #[UseErrorResponse(MyErrorResponse::class)]
    class ExampleController extends Controller
    {
        #[Post('do-something')]
        public function doSomething()
        {
            throw new CustomException("There is something wrong.");
        }
    
        #[Post('do-another-thing')]
        public function doAnotherThing()
        {
            throw new \RuntimeException("Whoops runtime error");
        }
    }
    

    MyErrorResponse.php

    #[ResponseStatus(500)]
    class MyErrorResponse extends JsonResponse
    {
        public string $message;
    }
    

    CustomException.php

    #[ResponseStatus(400)]
    class CustomException extends Exception
    {
    }
    

    In example above, when we call POST /do-something or POST /do-another-thing, it send response with status 200.

    Expectation

    When we call POST /do-something it should response status 400 since it throws CustomException that has ResponseStatus(400) attribute on it. And when we call POST /do-another-thing it should response status 500 because RuntimeException doesn't has ResponseStatus attribute on it, so it use the ResponseStatus(500) attached on MyErrorResponse class.

    bug 
    opened by emsifa 0
  • Error generating OpenAPI with circular schema

    Error generating OpenAPI with circular schema

    For example we have response:

    class FooResponse extends JsonResponse
    {
        #[ArrayOf(CategoryDto::class)]
        public array $categories;
    }
    

    CategoryDto:

    class CategoryDto extends ResponseDto
    {
        public int $id;
        public string $name;
    
        #[ArrayOf(CategoryDto::class)]
        public array $children;
    }
    

    It throws error 500 with blank response

    bug 
    opened by emsifa 0
  • When ArrayOf item is nullable, validation still validate class schema

    When ArrayOf item is nullable, validation still validate class schema

    TaskDto (parent DTO):

    class TaskDto extends Dto
    {
        public string $title;
    
        #[ArrayOf(TodoDto::class, ArrayOf::NULL_ITEM)]
        public array $todos;
    }
    

    TodoDto:

    class TodoDto extends Dto
    {
        #[Required]
        public string $label;
        
        #[Required]
        public string $description;
        
        public ?string $dueDate;
    }
    

    Request body (JSON):

    {
        "title": "Lorem ipsum dolor sit amet",
        "todos": [
             null,
            {"label": "foobar", "description": "Lorem ipsum dolor"}
         ]
    }
    

    It will validate first todos while todos[0] is null, and currently there is no way we can avoid validation on it. I think the only way we can avoid it is by filtering validation data in the Body object, before validating, but it will decrease performance significanly since we have to scan the whole data just to filter them.

    bug 
    opened by emsifa 0
  • Create attribute to transform property name like `json` tag in golang.

    Create attribute to transform property name like `json` tag in golang.

    It is common to use snake_case in PHP/Laravel project, especially for database column names. In other side, Javascript use camelCase for variable/property name. If we build API without convert naming case, our code will looks inconsistent. But, converting naming case manually need extra work and will add more complexity to our code. In Laravel, we can use Transformer or Resource class to handle response, but how about the request?

    That is why we may need an attribute to handle this elegantly.

    For example, we can create From (for dto) and To (for response) attributes like following code below:

    Request:

    class StorePostDto extends Dto
    {
        public string $title;
        public string $body;
    
        #[From('categoryId')]
        public int $category_id;
    }
    

    Response:

    class StorePostResponse extends JsonResponse
    {
        public string $title;
        public string $body;
    
        #[To('categoryId')]
        public int $category_id;
    }
    

    Controller:

    public function store(#[Body] StorePostDto $dto): StorePostResponse
    {
        $post = Post::create($dto->toArray());
        return StorePostResponse::fromArray($post);
    }
    

    Example above, will transform request value below:

    {
        "title": "Lorem Ipsum",
        "body": "Lorem ipsum dolor sit amet",
        "categoryId": 12
    }
    

    Into PHP StorePostDto object:

    $dto->title = "Lorem Ipsum";
    $dto->body = "Lorem ipsum dolor sit amet";
    $dto->category_id = 12; // <- notice this
    

    That is why, in Post::create method, we don't have to convert categoryId to category_id manually.

    Also, for the response, we convert value from Post instance (result of Post::create) below:

    $post->id = 1;
    $post->title = "Lorem Ipsum";
    $post->body = "Lorem ipsum dolor sit amet";
    $post->category_id = 12; // <- notice this
    

    Into JSON response:

    {
        "id": 1,
        "title": "Lorem Ipsum",
        "body": "Lorem ipsum dolor sit amet",
        "categoryId": 12
    }
    

    Then in our JS code, we can still use camelCase consistently, also in PHP we can still use snake_case consistently.

    proposal 
    opened by emsifa 0
Owner
Muhammad Syifa
Curious being
Muhammad Syifa
Laravel Soulbscription - This package provides a straightforward interface to handle subscriptions and features consumption.

About This package provides a straightforward interface to handle subscriptions and features consumption. Installation You can

Lucas Vinicius 269 Jan 1, 2023
Localization Helper - Package for convenient work with Laravel's localization features and fast language files generation

Localization Helper Package for convenient work with Laravel's localization features and fast language files generation. Installation Via Composer $ c

Galymzhan Begimov 0 Jul 13, 2019
Laravel breeze is a PHP Laravel library that provides Authentication features such as Login page , Register, Reset Password and creating all Sessions Required.

About Laravel breeze To give you a head start building your new Laravel application, we are happy to offer authentication and application starter kits

null 3 Jul 30, 2022
An api with chart js in laravel 8 features

About Laravel Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experie

jehdaileg 2 Dec 1, 2021
Advance features / Topics related to Laravel

Laravel Advance Topics Notes Includes : v1.0.0 - Service Container v1.1.0 - View Composer v1.2.0 - Polymorphic Relationships v1.3.0 - Custom Facade Im

Aadhar Gaur 1 Jan 16, 2022
Easily integrate single-database multi tenant features into your Laravel application

Laravel Tenant Aware Easily integrate single-database multi tenant features into your Laravel application. Installation You can install the package vi

H-FARM Innovation 9 Dec 21, 2022
Adds a service worker to Magento2 to enable PWA features

Monsoon PWA Adds icons, a web manifest file and a service-worker file to make magento 2 a PWA. Main Features Supports Magento Blank and Luma as well a

Monsoon Consulting 20 Nov 17, 2022
Livewire DataTables components for back-end. Modular, easy to use, with tons of features.

Livewire DataTables Livewire DataTables components for back-end. Modular, easy to use, with tons of features. Inspired by Caleb's Livewire Screencasts

Amir Rami 8 Jul 27, 2022
Eloquent Befriended brings social media-like features like following, blocking and filtering content based on following or blocked models.

Laravel Befriended Eloquent Befriended brings social media-like features like following, blocking and filtering content based on following or blocked

Renoki Co. 720 Jan 3, 2023
Worlds (soon to be) most advanced Anime site! Featuring Administration features and everything you need for users and yourself. The successor of aniZero.

/**********************************************************************\ | _____ H33Tx & xHENAI __ 31.01.2022| |

HENAI.eu 40 Jan 3, 2023
Jetstrap is a lightweight laravel 8 package that focuses on the VIEW side of Jetstream / Breeze package installed in your Laravel application

A Laravel 8 package to easily switch TailwindCSS resources generated by Laravel Jetstream and Breeze to Bootstrap 4.

null 686 Dec 28, 2022
A Laravel chat package. You can use this package to create a chat/messaging Laravel application.

Chat Create a Chat application for your multiple Models Table of Contents Click to expand Introduction Installation Usage Adding the ability to partic

Tinashe Musonza 931 Dec 24, 2022
This package provides extended support for our spatie/enum package in Laravel.

Laravel support for spatie/enum This package provides extended support for our spatie/enum package in Laravel. Installation You can install the packag

Spatie 264 Dec 23, 2022
Testbench Component is the de-facto package that has been designed to help you write tests for your Laravel package

Laravel Testing Helper for Packages Development Testbench Component is the de-facto package that has been designed to help you write tests for your La

Orchestra Platform 1.9k Dec 29, 2022
🥳🔐 This package is a Laravel package that checks if an email address is a spammer

This package is a Laravel package that checks if an email address is a spammer. It verifies your signups and form submissions to confirm that they are legitimate.

Endurance, the Martian 15 Dec 19, 2022
GeoLocation-Package - This package helps you to know the current language of the user, the country from which he is browsing, the currency of his country, and also whether he is using it vpn

GeoLocation in PHP (API) ?? ?? ?? This package helps you to know a lot of information about the current user by his ip address ?? ?? ?? This package h

Abdullah Karam 4 Dec 8, 2022
List of 77 languages for Laravel Framework 4, 5, 6, 7 and 8, Laravel Jetstream , Laravel Fortify, Laravel Breeze, Laravel Cashier, Laravel Nova and Laravel Spark.

Laravel Lang In this repository, you can find the lang files for the Laravel Framework 4/5/6/7/8, Laravel Jetstream , Laravel Fortify, Laravel Cashier

Laravel Lang 6.9k Jan 2, 2023
A package that uses blade templates to control how markdown is converted to HTML inside Laravel, as well as providing support for markdown files to Laravel views.

Install Install via composer. $ composer require olliecodes/laravel-etched-blade Once installed you'll want to publish the config. $ php artisan vendo

Ollie Codes 19 Jul 5, 2021
A light weight laravel package that facilitates dealing with arabic concepts using a set of classes and methods to make laravel speaks arabic

A light weight laravel package that facilitates dealing with arabic concepts using a set of classes and methods to make laravel speaks arabic! concepts like , Hijri Dates & Arabic strings and so on ..

Adnane Kadri 49 Jun 22, 2022