PHP code example of devespresso / laravel-api-kit

1. Go to this page and download the library: Download devespresso/laravel-api-kit library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

devespresso / laravel-api-kit example snippets


'pagination' => [
    'with_pages' => false, // false = simplePaginate() | true = paginate() (

'paths' => [
    'models'          => 'App\\Models\\',
    'transformers'    => 'App\\Transformers\\',
    'repositories'    => 'App\\Repositories\\',
    'controllers'     => 'App\\Http\\Controllers\\',
    'requests'        => 'App\\Http\\Requests\\',
    'authorisation'   => 'App\\Services\\Authorisation\\',
    'filter_services' => 'App\\Services\\Filters\\',
],

'auto_select' => true,

'auto_eager_load' => true,

'enable_explicit_filtering' => false,

// Invokable class that returns the current user's role (string or int, or null)
'role_resolver' => App\Support\RoleResolver::class,

// String roles — ordered lowest to highest
'roles' => ['moderator', 'editor', 'admin'],

// OR — numeric roles, no list needed
'numeric_roles' => false,

'transformers' => [
    'prefixes' => [
        'hidden_attributes'  => '!', // selected from DB but excluded from the JSON response
        'custom_attributes'  => '@', // computed via a transformer method, not read from the DB
        'accessor_attributes'=> '~', // Laravel model accessor — not selected from DB, but 

use Devespresso\LaravelApiKit\Traits\EnableDatabaseFiltering;

class Post extends Model
{
    use EnableDatabaseFiltering;

    protected $defaultFilterService = PostFilterService::class; // optional

    protected $searchableColumns = ['title', 'body']; // used by the search scope
}

$posts = Post::filter($request->validated(), $request->user());

Post::filter(
    data:  $request->validated(),  // drives filter methods and sorting
    user:  $request->user(),       // available via $this->user and getEffectiveRoles()
    query: $query,                 // pre-scoped Builder — base constraints before filters run
    extras: $extras,               // arbitrary context — read via $this->getExtraProperty('key')
);

// Only show posts belonging to the current team — enforced before filters run
$query = Post::where('team_id', $team->id);

$posts = Post::filter($request->validated(), $request->user(), query: $query);

$posts = Post::filter(
    $request->validated(),
    $request->user(),
    extras: ['team' => $team]
);

// Inside PostFilterService — read the extra value via getExtraProperty():
public function setConditions(): void
{
    $team = $this->getExtraProperty('team');
    $this->query->where('visibility', $team->default_visibility);
}

public function setConditions(): void
{
    // Scope results to the authenticated user
    $this->query->where('user_id', $this->user->id);
}

public function status(string $value): void
{
    // Only admins can filter by draft status
    if ($value === 'draft' && !in_array('admin', $this->getEffectiveRoles())) {
        return;
    }

    $this->query->where('status', $value);
}

use Devespresso\LaravelApiKit\Services\Filters\BaseFilterService;

class PostFilterService extends BaseFilterService
{
    // Columns users are allowed to sort by
    protected $sortColumns = ['created_at', 'updated_at', 'id', 'title'];

    // Alias => real column name mappings for sort
    protected $customSortColumns = ['date' => 'created_at'];

    // Default sort when no 'sort' key is in the request
    protected $defaultSortingColumn = ['created_at,desc'];

    // Methods that cannot be triggered by request data
    protected $guardedMethods = ['sensitiveMethod'];

    // Methods restricted by role
    protected $roleMethods = ['admin' => [' function onlyPublished(bool $value): void
    {
        $this->query->where('published', true);
    }

    // Only callable by users with the 'admin' role
    public function 

$this->getDataValue('key', $default); // get a value from request data
$this->dataHasValue('key', 'value');  // check if key equals a specific value
$this->dataHasKeys(['key1', 'key2']); // check all keys are present
$this->getExtraProperty('tenant_id'); // get a value from $extras
$this->with(['comments', 'tags']);    // eager load relations
$this->withCount(['comments']);       // eager load relation counts
$this->disableConditions();           // skip setConditions() for the next filter() call — useful in admin or internal contexts
$this->setSelect(['id', 'title']);    // override the auto-selected columns; has no effect when auto_select is disabled

protected function applyTeamScope(): void
{
    // safe — cannot be triggered from request data
    $this->query->where('team_id', $this->user->team_id);
}

protected $guardedMethods = ['internalScope', 'sensitiveMethod'];

protected $roleMethods = [
    'moderator' => [''admin'     => ['

'roles'         => ['moderator', 'editor', 'admin'], // ordered lowest to highest
'role_resolver' => App\Support\RoleResolver::class,

// app/Support/RoleResolver.php
class RoleResolver
{
    public function __invoke(?Authenticatable $user): mixed
    {
        return $user?->role; // e.g. 'admin', 'editor', 'moderator', or null
    }
}

// app/Services/Filters/PostFilterService.php
class PostFilterService extends BaseFilterService
{
    protected $roleMethods = [
        'moderator' => ['    public function    $this->query->where('published', false);
    }

    // Admin only
    public function 

$posts = Post::filter($request->validated(), $request->user());

'role_resolver' => App\Support\RoleResolver::class,
'numeric_roles' => true,

protected $roleMethods = [
    1 => ['3 => ['

protected function getEffectiveRoles(): array
{
    // custom logic — return the expanded set of roles yourself
    return $this->user?->getAllGrantedRoles() ?? [];
}

protected $autoApply = ['onlyPublished' => true];

public function onlyPublished(bool $value): void
{
    $this->query->where('published', true);
}

'enable_explicit_filtering' => true,

$posts = Post::filter(
    $request->validated(),
    $request->user(),
    explicitFilters: ['status', 'author_id']
);

// Inside a controller or repository — restrict to safe filters for this endpoint
$posts = Post::filter(
    $request->validated(),
    $request->user(),
    explicitFilters: ['status', 'category_id', 'published']
);

protected $defaultSortingColumn = ['created_at,desc', 'id,desc'];

protected $sortColumns = ['created_at', 'updated_at', 'id', 'title'];

// 'date' in the request maps to 'created_at' on the query
protected $customSortColumns = ['date' => 'created_at'];

protected $rawSort = ['status_order' => 'sortByStatus'];

protected function sortByStatus(): string
{
    return "FIELD(status, 'active', 'pending', 'closed')";
}

use Devespresso\LaravelApiKit\Transformers\BaseTransformer;

class PostTransformer extends BaseTransformer
{
    protected $formats = [
        // Always ormer method)
            '~reading_time',       // accessor attribute (Laravel model accessor, not a DB column)
            '!internal_notes',     // hidden (excluded from output)
            'author' => [          // nested relation
                'id',
                'name',
                '!password',       // hidden within the relation
            ],
        ],

        // Merged with * on the show route
        'show' => [
            'body',
            'created_at',
        ],

        // Returned as-is on index — does NOT merge with *
        '_index' => [
            'id',
            'title',
        ],
    ];

    // Rename output keys
    protected $renames = [
        '*' => ['created_at' => 'createdAt'],     // global rename
        'author.name' => 'authorName',             // path-specific rename
    ];

    // Format attribute values
    protected $formatters = [
        '*' => ['status' => 'formatStatus'],       // global formatter
        'author.name' => ['toUpper'],              // path-specific formatter
    ];

    // Computed attributes resolved via methods (used with the '@' prefix)
    protected $customAttributes = [
        'word_count' => 'getWordCount',
    ];

    // Default values when an attribute is null
    protected $defaults = [
        '*' => ['status' => 'draft'],              // global scalar default
        'author.bio' => 'getBioDefault',           // path-specific method default
    ];

    // Conditionally hide attributes based on the current user/context
    protected $guarded = [
        '*' => ['salary' => 'isNotAdmin'],         // global guard
        'user.secret' => 'isNotOwner',             // path-specific guard
    ];

    // Custom attribute methods (called with the model)
    public function getWordCount($model): int
    {
        return str_word_count($model->body ?? '');
    }

    // Formatter methods
    public function formatStatus($value): string
    {
        return ucfirst($value);
    }

    // Guard methods (return true to hide, false to show)
    public function isNotAdmin($model): bool
    {
        return !auth()->user()?->isAdmin();
    }
}

'versioning' => [
    'enabled'  => true,
    'driver'   => 'route_prefix',  // 'route_prefix' | 'header'
    'header'   => 'X-Api-Version', // used when driver = 'header'
    'versions' => ['v2', 'v3'],    // ordered — v3 builds on v2
],

class PostTransformer extends BaseTransformer
{
    // Declare the highest version this transformer explicitly supports.
    // Any version within this boundary that has no method will throw.
    // Leave null to silently skip missing version methods.
    protected ?string $latestVersion = 'v3';

    // The starting point — used by all versions.
    // Use this method instead of $formats when versioning is enabled.
    protected function baseFormat(): array
    {
        return [
            '*'    => ['id', 'title', 'status'],
            'show' => ['email', 'created_at'],
        ];
    }

    // v2 builds on base
    protected function v2Format(): array
    {
        return [
            'append' => [
                '*'    => ['avatar'],
                'show' => ['phone'],
            ],
            'remove' => [
                '*' => ['status'],  // removed in v2
            ],
        ];
    }

    // v3 builds on v2
    protected function v3Format(): array
    {
        return [
            'append' => [
                '*' => ['verified_at'],
            ],
        ];
    }
}

protected function v2Format(): array
{
    return [
        'append' => [
            '*' => [
                'author' => ['bio'],        // adds bio inside the author relation
            ],
        ],
        'remove' => [
            '*' => [
                'author' => ['email'],      // removes email from inside author
            ],
        ],
    ];
}

protected function v2Format(): array
{
    return [
        'merge'   => false,
        'formats' => [
            '*'    => ['id', 'avatar'],   // completely replaces base
            'show' => ['phone'],
        ],
    ];
}

class PostTransformer extends BaseTransformer
{
    // Always-on base renames
    protected $renames = [
        '*' => ['created_at' => 'createdAt'],
    ];

    // Always-on base formatters
    protected $formatters = [
        '*' => ['title' => 'ucwords'],
    ];

    protected function baseFormat(): array
    {
        return ['*' => ['id', 'title', 'status', 'created_at']];
    }

    protected function v2Format(): array
    {
        return [
            'append'  => ['*' => ['name', 'avatar']],

            // Layered on top of $renames — base rename is preserved
            'renames' => [
                '*'           => ['name' => 'fullName'],   // global rename
                'author.name' => 'authorName',             // dot-notation path rename
            ],

            // Layered on top of $formatters
            'formatters' => [
                '*' => ['status' => 'toUpper'],
            ],

            // Layered on top of $guarded
            'guarded' => [
                '*' => ['salary' => 'isNotAdmin'],
            ],

            // Layered on top of $defaults
            'defaults' => [
                '*'          => ['avatar' => 'https://example.com/default.png'],
                'author.bio' => 'getDefaultBio',
            ],

            // Layered on top of $customAttributes
            'customAttributes' => [
                'greeting' => 'getGreeting',
            ],
        ];
    }

    protected function v3Format(): array
    {
        return [
            // Adds to v2's renames — all three renames are active for v3
            'renames' => [
                '*' => ['status' => 'userStatus'],
            ],
        ];
    }
}

protected ?string $latestVersion = 'v3';

'driver' => 'header',
'header' => 'X-Api-Version',  // GET /posts with X-Api-Version: v2

protected $wrapper = 'post';
// produces: {"post": {...}} instead of {"data": {...}}

class PostTransformer extends BaseTransformer
{
    protected $formats = [
        '*' => [
            'id',
            'title',
            'status',
            'user_id',         // foreign key — must be m attribute — excluded from SELECT, resolved via transformer method
            '~reading_time',   // accessor attribute — excluded from SELECT, resolved via model accessor
            '!team_id',        // hidden from output — but still SELECTed (useful for auth checks)
            'author' => [      // relation — auto eager-loaded
                'id',
                'name',
                '!email',      // hidden from output — but still SELECTed
            ],
        ],
    ];
}

// @word_count and ~reading_time are omitted from SELECT — they are resolved
// after the query via the transformer method and model accessor respectively.
Post::select('posts.id', 'posts.title', 'posts.status', 'posts.user_id', 'posts.team_id')
    ->with(['author' => fn ($q) => $q->select('users.id', 'users.name', 'users.email')])
    ->where('team_id', $user->team_id)
    ->where('published', true)
    ->simplePaginate($perPage);

use Devespresso\LaravelApiKit\Repositories\BaseRepository;

class PostRepository extends BaseRepository
{
    // Optional: override auto-resolved model
    protected $model = Post::class;

    // Hooks
    protected function beforeCreate(array &$attributes): void
    {
        $attributes['slug'] = Str::slug($attributes['title']);
    }

    protected function afterCreated(Model $model, array $attributes): void
    {
        event(new PostCreated($model));
    }

    protected function beforeUpdate(?Model $model, array &$attributes): void
    {
        if (isset($attributes['title'])) {
            $attributes['slug'] = Str::slug($attributes['title']);
        }
    }

    protected function afterUpdated(Model $model, array $attributes): void
    {
        Cache::forget("post:{$model->id}");
    }

    protected function beforeDelete(Model $model): void
    {
        // runs before delete
    }

    protected function afterDeleted(Model $model): void
    {
        // runs after delete
    }
}

$repo->index($data, $user);                                          // filtered, paginated list
$repo->index($data, $user, explicitFilters: ['status', 'name']);      // with explicit filter allowlist
$repo->get($id);                     // single record
$repo->create($attributes);          // create with hooks
$repo->update($model, $attributes);  // update with hooks
$repo->delete($model);               // delete with hooks

// Skip all hooks
$repo->withoutHooks()->delete($model);

// Skip specific hooks only
$repo->withoutHooks('afterCreated')->create($attributes);
$repo->withoutHooks('beforeUpdate', 'afterUpdated')->update($model, $attributes);

use Devespresso\LaravelApiKit\Controllers\ApiController;

class PostController extends ApiController
{
    public function index(PostRequest $request): JsonResponse
    {
        return $this->setData(
            $this->repository->index($request->validated(), $request->user())
        )->respond();
    }

    public function store(PostRequest $request): JsonResponse
    {
        return $this->setData(
            $this->repository->create($request->validated())
        )->respondCreated();
    }

    public function destroy(Post $post): JsonResponse
    {
        $this->repository->delete($post);

        return $this->respondNoContent();
    }
}

return $this->setData($post)->respondCreated();
return $this->respondNoContent();

// Uses the default 'data' wrapper
return $this->setRawData(['total' => 100, 'active' => 42])->respond();

// Custom key
return $this->setRawData(['total' => 100], 'stats')->respond();

$this->appendTo(['id' => 1, 'name' => 'Alice']);
$this->appendTo(['id' => 2, 'name' => 'Bob']);

return $this->respond();
// "data": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]

$this->appendTo($post, 'posts');
$this->appendTo($stats, 'meta');

return $this->respond();

// Bulk set
$this->setMeta(['permissions' => ['edit', 'delete'], 'roles' => ['admin']]);

// Incremental — chainable
$this->addMeta('permissions', ['edit', 'delete'])
     ->addMeta('roles', ['admin']);

// Merge extra keys into the response
return $this->setData($post)->respond(['extra' => 'value']);

// Override a key set by setData()
return $this->setData($post)->respond(['post' => $customPayload], override: true);

    return $this->setData($post, 'post')->respond();
    // produces: {"post": {...}} instead of the transformer default
    

    return $this->setData($post, format: 'show')->respond();
    // uses the 'show' format from PostTransformer
    

return $this->setTransformer(SummaryTransformer::class)
    ->setData($post)
    ->respond();

return $this->setCode(404, 'Post not found')->respond();
// {"code": 404, "status": "error", "message": "Post not found"}

class PostController extends ApiController
{
    protected bool $autoResolveRepository = false;
    protected bool $autoResolveTransformer = false;
}

use Devespresso\LaravelApiKit\Requests\BaseRequest;

class PostRequest extends BaseRequest
{
    protected function actionsRules(): array
    {
        return [
            'store' => [
                'title' => [',
            ],
        ];
    }

    // Optional per-action authorization
    protected function storeAuth(): bool
    {
        return $this->user()->can('create', Post::class);
    }
}

use Devespresso\LaravelApiKit\Services\Authorisation\BaseAuthorisationService;

class PostAuthorisationService extends BaseAuthorisationService
{
    protected $mainProperty = 'post';
}

// In a controller or service:
$auth = (new PostAuthorisationService())
    ->setUser($user)
    ->setProperties(['post' => $post])
    ->doesItBelongToUser()         // asserts post->user_id === $user->id
    ->

$auth = (new PostAuthorisationService())
    ->skipExceptions()
    ->setUser($user)
    ->setProperties(['post' => $post])
    ->doesItBelongToUser();

if (!$auth->isValid()) {
    return response()->json(['errors' => $auth->getErrors()], 403);
}
bash
php artisan vendor:publish --provider="Devespresso\LaravelApiKit\LaravelApiKitServiceProvider"
bash
php artisan devespresso:api-kit:scaffold Post
bash
# Only generate specific components
php artisan devespresso:api-kit:scaffold Post --only=model,repository,transformer

# Skip specific components
php artisan devespresso:api-kit:scaffold Post --except=model

# Overwrite existing files
php artisan devespresso:api-kit:scaffold Post --force
bash
php artisan vendor:publish --provider="Devespresso\LaravelApiKit\LaravelApiKitServiceProvider"