One command. A complete, production-ready API module.
Model · Migration · Enum · Filter · Repository · Service
· Requests · Resource · Policy · Controller · Tests
php artisan make:module ProductOne command generates 15 files across every layer of your REST API:
| Layer | What is generated |
|---|---|
| Data | Model, driver-aware migration, repository interface, repository |
| Domain | Service, filter class (whitelist-safe column filtering + pagination) |
| HTTP | Controller (Swagger-annotated), store & update form requests, API resource, resource collection |
| Auth | Policy with CRUD gates, wired into every controller action |
| Status | PHP 8.1 backed string enum (active / inactive / pending), auto-cast by the model |
| Tests | Feature test + unit test stubs — pre-skipped so CI is green from commit one |
Then add one route:
Route::apiResource('products', ProductController::class);Your API is running, documented, authorized, and tested.
- Why LaravelBase?
- Features
- Laravel Artisan vs LaravelBase
- Requirements
- Installation
- Quick Start
- Architecture overview
- make:module — the module generator
- Filter + Pagination API
- Driver-aware migrations
- Swagger / OpenAPI docs
- Enums
- Policy
- API Resource
- Generated tests
- Repository auto-binding
- make:repository — deprecated alias
- base:create-database
- Manual setup
- API Reference
- Configuration
- Publishing helpers and traits
- Testing and static analysis
- Troubleshooting
- Changelog
- Contributing
- Security
- License
- Author
Every Laravel API project ends up writing the same boilerplate: a repository, a service, a form request that returns JSON errors, and a handful of response helpers. You write them once, then copy-paste across every project.
LaravelBase eliminates that tax. The package ships pre-written, production-hardened base classes and a generator that scaffolds a complete vertical slice — not just the model, not just the controller, but every layer wired together and ready to ship:
- No
ServiceProvideredits for the common case — interfaces are auto-bound to implementations by naming convention at boot. - No column-injection vulnerabilities —
AbstractFilteraccepts only columns you explicitly whitelist; unknown request parameters are silently ignored. - No documentation debt — generated controllers carry
@OASwagger annotations; installl5-swaggerand your API is self-documented. - No broken CI — generated test stubs are pre-skipped so your pipeline is green from the first commit.
- Your code, your rules — generated files live in
app/; edit, extend, or delete them freely. They are never re-generated unless you pass--force.
The generated code is idiomatic Laravel — no framework-within-a-framework, no magic, no lock-in.
| Feature | Details |
|---|---|
| Module generator | make:module scaffolds 15 components in one command: Model, Migration, Enum, Filter, Interface, Repository, Service, Store/Update Requests, API Resource + Collection, Policy, Controller, Feature test, Unit test |
| Filter + Pagination | AbstractFilter base class: whitelist-safe column filters, LIKE search, sort-by whitelist, per-page clamping — ergonomic $service->filter($request)->paginate() API |
| Driver-aware migrations | Detects MySQL / PostgreSQL / SQLite at runtime; uses json() on MySQL/PG, text() fallback otherwise |
| Swagger / OpenAPI | Generated controllers carry @OA\* PHPDoc annotations (l5-swagger / swagger-php compatible) — optional, gated behind suggest |
| Status Enums | PHP 8.1 backed string enum (Active, Inactive, Pending) with label(), isActive(), values(); model casts status automatically |
| Policy | Full CRUD policy stub with HandlesAuthorization; controller calls $this->authorize() for every action |
| API Resource | JsonResource + ResourceCollection with @OA\Schema; controller returns wrapped resources via ApiResponse |
| Generated tests | Feature + Unit test stubs that pass (skipped) from the first commit; inline TODO instructions |
| Repository pattern | BaseRepository with all, paginate, find, findOrFail, findBy, create, update, delete, query |
| Service layer | BaseService wrapping any repository; extend to add domain logic |
| Contracts-first design | RepositoryInterface / ServiceInterface with full native type hints |
| External validation | BaseRequest returns a standard 422 JSON envelope on failure — no extra code |
| Consistent API responses | Static ApiResponse helper + ApiResponseTrait — identical JSON shape everywhere |
| Image handling | Secure upload / update / delete via ImageUploadTrait (MIME-derived extension) |
| Auto-binding | *RepositoryInterface auto-bound to *Repository by naming convention — no manual registration |
| Laravel 10/11/12/13, PHP 8.1+ | No upper-bound constraint on Laravel version |
| Capability | Laravel Artisan (built-in) | LaravelBase v3 |
|---|---|---|
| Generate Eloquent model | make:model |
Included in make:module by default |
| Generate migration | make:migration (separate command) |
Auto-generated, driver-aware — json() on MySQL/PG, text() with notice on SQLite |
| Repository pattern | Not provided | BaseRepository + RepositoryInterface + generated {Name}Repository + {Name}RepositoryInterface |
| Service layer | Not provided | BaseService + ServiceInterface + generated {Name}Service |
| Controller | make:controller --api (empty method bodies) |
Full CRUD controller with $this->authorize(), typed requests, ApiResponse calls, and Swagger annotations |
| Form Requests | make:request (separate command, manual wiring) |
Store{Name}Request + Update{Name}Request generated and wired; BaseRequest returns JSON 422 envelope automatically |
| JSON API response helper | Not provided | ApiResponse — success / created / error / notFound / paginated with a consistent {status, message, data, meta} envelope |
| Status Enum | Not provided | PHP 8.1 backed string enum with label(), isActive(), values(); model casts status automatically |
| Query filtering + pagination | Not provided | AbstractFilter: whitelisted column filters, LIKE search, global ?search=, sort whitelist, ?per_page= clamped to [1, 100] |
| API Resource + Collection | make:resource (separate command) |
{Name}Resource + {Name}ResourceCollection generated and wired into controller; includes @OA\Schema annotation |
| Policy | make:policy (separate command, manual registration) |
{Name}Policy with HandlesAuthorization, all CRUD gates; controller calls $this->authorize() for every action |
| Swagger / OpenAPI docs | Not provided | All 5 CRUD actions annotated (@OA\Get/Post/Put/Delete); compatible with darkaonline/l5-swagger |
| Repository auto-binding | Not provided | Convention-based: *RepositoryInterface → *Repository resolved at boot; no ServiceProvider edit required |
| Explicit provider binding | Not provided | --provider flag creates/updates RepositoryServiceProvider; idempotent on re-runs |
| Generated tests | Not provided | Feature + Unit test stubs, pre-skipped (CI stays green), with inline TODO instructions |
| Database creation command | Not provided | base:create-database [--connection=] — MySQL and PostgreSQL |
| Image upload helper | Not provided | ImageUploadTrait: MIME-derived extension (prevents extension-spoofing), upload / update / delete |
| Subset generation | N/A | --only= / --except= — generate any combination of the 15 components |
| Per-component skip flags | N/A | --no-model, --no-migration, --no-enum, --no-filter, --no-service, --no-request, --no-resource, --no-policy, --no-controller, --no-test |
| Laravel version support | Current only | 10, 11, 12, 13 |
| PHP version support | Current only | 8.1, 8.2, 8.3, 8.4 |
| CI matrix | N/A | PHP 8.1–8.4 × Laravel 10–13 + prefer-lowest + PHPStan level 5 + make:module smoke test |
| Dependency | Version |
|---|---|
| PHP | ^8.1 |
| Laravel | 10.x and above (10 / 11 / 12 / 13) |
composer require muhammedsalama/laravel-baseThe service provider registers automatically via Laravel's package auto-discovery. No manual configuration required.
Install from GitHub (VCS repository)
"repositories": [
{
"type": "vcs",
"url": "https://github.com/MuhammedMSalama/LaravelBase"
}
]composer require muhammedsalama/laravel-base:^3.0Install from a local path (package development)
"repositories": [
{
"type": "path",
"url": "../laravel-base"
}
],
"require": {
"muhammedsalama/laravel-base": "*"
}Generate a complete Product module with one command:
php artisan make:module ProductThis creates 15 files — the full vertical slice of a REST API module:
app/
Enums/ProductStatus.php
Filters/ProductFilters.php
Interfaces/ProductRepositoryInterface.php
Repositories/ProductRepository.php
Services/ProductService.php
Models/Product.php
Policies/ProductPolicy.php
Http/
Controllers/ProductController.php
Requests/Product/
StoreProductRequest.php
UpdateProductRequest.php
Resources/
ProductResource.php
ProductResourceCollection.php
database/migrations/xxxx_xx_xx_create_products_table.php
tests/
Feature/ProductTest.php
Unit/ProductServiceTest.php
Register the route, define your $fillable, register the policy, and you have a working, documented, authorized CRUD API:
// routes/api.php
Route::apiResource('products', ProductController::class);HTTP Request
│
▼
Controller (thin — delegates, authorizes, transforms)
│ uses ApiResponse::
│ type-hints Form Requests (Store/Update)
│ calls $this->authorize() via Policy
│ returns ApiResource-wrapped data
▼
Filters class (AbstractFilter — request-driven, whitelisted)
│ applies WHERE / LIKE / ORDER BY to the query
│ paginates and returns LengthAwarePaginator
▼
Service (domain logic)
│ extends BaseService
│ exposes filter(Request): {Name}Filters
│ depends on RepositoryInterface
▼
Repository (data access)
│ extends BaseRepository
│ wraps an Eloquent Model
▼
Model (casts status → {Name}Status enum)
│
▼
Database
Validation failures are caught by BaseRequest::failedValidation() before the controller runs, and a 422 ApiResponse envelope is returned automatically.
php artisan make:module {Name} [options]Generates a complete module. All files are valid, idiomatic, and immediately working.
| Component | Generated path | Stub |
|---|---|---|
| Interface | app/Interfaces/{Name}RepositoryInterface.php |
interface.stub |
| Repository | app/Repositories/{Name}Repository.php |
repository.stub |
| Model | app/Models/{Name}.php |
module-model.stub / model.stub |
| Migration | database/migrations/{ts}_create_{table}_table.php |
migration.stub |
| Status Enum | app/Enums/{Name}Status.php |
enum.stub |
| Filters | app/Filters/{Name}Filters.php |
filter.stub |
| Service | app/Services/{Name}Service.php |
module-service.stub / service.stub |
| StoreRequest | app/Http/Requests/{Name}/Store{Name}Request.php |
request.stub |
| UpdateRequest | app/Http/Requests/{Name}/Update{Name}Request.php |
request.stub |
| API Resource | app/Http/Resources/{Name}Resource.php |
resource.stub |
| ResourceCollection | app/Http/Resources/{Name}ResourceCollection.php |
resource-collection.stub |
| Policy | app/Policies/{Name}Policy.php |
policy.stub |
| Controller | app/Http/Controllers/{Name}Controller.php |
module-controller.stub |
| Feature test | tests/Feature/{Name}Test.php |
test-feature.stub |
| Unit test | tests/Unit/{Name}ServiceTest.php |
test-unit.stub |
The module-model.stub (includes enum cast) is used when enum is enabled (the default). The module-service.stub (includes filter() method) is used when filter is enabled. The module-controller.stub (full: Resources + Policy + Swagger) is used when resource, request, filter, and policy are all enabled. Otherwise the simpler controller.stub / controller.plain.stub are selected automatically.
Generate a subset of components using --only (whitelist) or --except (blacklist). Both accept comma-separated component names from the table above.
# Only the data-access layer
php artisan make:module Invoice --only=model,migration,interface,repository,service
# Everything except tests
php artisan make:module Invoice --except=test
# Just the model and migration — quick prototyping
php artisan make:module Order --only=model,migration
# Skip policy and tests — lean module
php artisan make:module Category --except=policy,test--only takes priority over --except. When both are omitted, all components are generated.
| Flag | Skips | Side effect |
|---|---|---|
--no-model |
Model | Model is never overwritten even with --force |
--no-migration |
Migration | |
--no-enum |
Status Enum | Uses plain model.stub (no cast) |
--no-filter |
Filters class | Uses plain service.stub (no filter() method) and controller.stub |
--no-service |
Service | |
--no-request |
Store + Update Requests | Uses controller.plain.stub |
--no-resource |
API Resource + Collection | Falls back to controller.stub |
--no-policy |
Policy | Falls back to controller.stub |
--no-controller |
Controller | |
--no-test |
Feature + Unit test stubs |
| Option | Description |
|---|---|
--model=Foo |
Use Foo as the Eloquent model name (default: same as module name) |
--controller=FooController |
Custom controller class name |
--provider |
Create / update RepositoryServiceProvider with a binding for the interface |
--force |
Overwrite existing files (model is never overwritten regardless) |
Every generated module ships with a {Name}Filters class extending
MuhammedSalama\Base\Filters\AbstractFilter, and the generated {Name}Service exposes a
filter(Request $request) method pre-wired to the repository's query builder.
// {Name}Controller::index() — generated automatically
public function index(Request $request): JsonResponse
{
$this->authorize('viewAny', Product::class);
return ApiResponse::paginated(
$this->service->filter($request)->paginate()
);
}Extend AbstractFilter and declare your whitelist:
// app/Filters/ProductFilters.php
class ProductFilters extends AbstractFilter
{
// column => SQL operator ('=', 'like', '>', '<', '>=', '<=', '!=')
protected array $filters = [
'status' => '=', // ?status=active
'category' => '=',
'name' => 'like', // ?name=phone → WHERE name LIKE '%phone%'
];
// columns the client may ORDER BY
protected array $sortable = ['id', 'name', 'price', 'created_at'];
// columns searched by a single ?search=term
protected array $searchable = ['name', 'description'];
}| Parameter | Behaviour |
|---|---|
?status=active |
Exact match — operator must be '=' in $filters |
?name=phone |
LIKE match — operator must be 'like' in $filters |
?search=keyword |
OR LIKE across all $searchable columns |
?sort_by=name |
ORDER BY — column must be in $sortable |
?sort_dir=desc |
Sort direction; anything other than desc defaults to asc |
?per_page=25 |
Page size; clamped to [1, 100]; default 15 |
Security: only columns declared in $filters or $sortable are ever applied to the query. Unknown request parameters are silently ignored — the filter is safe against column-injection attacks.
// Apply filters and return the Builder for further custom constraints
$builder = $filter->apply()->getQuery();
// Apply filters and paginate (returns LengthAwarePaginator)
$paginator = $filter->paginate($perPage = 15);
// Standard one-liner
return ApiResponse::paginated($this->service->filter($request)->paginate());apply() is idempotent — safe to call multiple times without duplicating WHERE clauses.
make:module reads config('database.default') and the configured driver at runtime — it never hardcodes a driver. The metadata JSON column in the generated migration is emitted as:
| Driver | Generated column |
|---|---|
mysql |
$table->json('metadata')->nullable() |
pgsql |
$table->json('metadata')->nullable() |
sqlite / other |
$table->text('metadata')->nullable() + a printed notice |
The status column always uses $table->string('status') — portable across all drivers. If your project uses SQLite or another driver, a notice is printed at generation time so you can review the migration before running it.
The generated controller carries @OA\... PHPDoc annotations compatible with
darkaonline/l5-swagger +
zircote/swagger-php. These packages are listed
in composer.json under suggest only — they are not required. Your application works
without them; the annotations are inert PHPDoc comments unless you install the generator.
composer require darkaonline/l5-swagger
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"Add a global @OA\Info annotation once — conventionally in app/Http/Controllers/Controller.php:
/**
* @OA\Info(title="My API", version="1.0.0")
*/
class Controller extends BaseController { ... }Generate the docs and open the interactive Swagger UI:
php artisan l5-swagger:generate
# → http://your-app/api/documentation| Method | Annotation | Documents |
|---|---|---|
index |
@OA\Get |
path, per_page, search, sort_by, sort_dir, status params; 200/401/403 |
show |
@OA\Get |
path {id} param; 200/404 |
store |
@OA\Post |
201/422 |
update |
@OA\Put |
path {id}; 200/422 |
destroy |
@OA\Delete |
path {id}; 200/404 |
The generated {Name}Resource carries a @OA\Schema annotation with all declared properties.
The generated {Name}Status is a PHP 8.1 backed string enum with three starter cases and
helper methods:
// app/Enums/ProductStatus.php
enum ProductStatus: string
{
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
public function label(): string { ... } // "Active", "Inactive", "Pending"
public function isActive(): bool { ... } // true when === self::Active
public static function values(): array { ... } // ['active', 'inactive', 'pending']
}The generated model casts status automatically:
// app/Models/Product.php
protected $casts = [
'status' => ProductStatus::class,
];Usage examples:
$product->status; // ProductStatus::Active
$product->status->label(); // "Active"
$product->status->value; // "active"
$product->status->isActive(); // true
ProductStatus::values(); // ['active', 'inactive', 'pending']
// In StoreProductRequest:
'status' => ['required', \Illuminate\Validation\Rule::enum(ProductStatus::class)],The generated {Name}Policy uses HandlesAuthorization and starts with all gates open
(return true). Harden the logic to match your business rules before production.
// Option A — AuthServiceProvider (Laravel 10 / 11):
protected $policies = [
Product::class => ProductPolicy::class,
];
// Option B — AppServiceProvider / boot() (any version):
\Illuminate\Support\Facades\Gate::policy(Product::class, ProductPolicy::class);The generated controller calls $this->authorize() for every action automatically:
$this->authorize('viewAny', Product::class); // index
$this->authorize('view', $product); // show
$this->authorize('create', Product::class); // store
$this->authorize('update', $product); // update
$this->authorize('delete', $product); // destroy{Name}Resource extends JsonResource with a toArray() ready to customise.
{Name}ResourceCollection wraps it for list responses.
The generated controller returns:
// Single resource
return ApiResponse::success(new ProductResource($product));
// Created
return ApiResponse::created(new ProductResource($product));
// Paginated list (via the filter's paginator)
return ApiResponse::paginated($this->service->filter($request)->paginate());Both test stubs are skipped out of the box so your CI is green from the first commit.
Each method has inline TODO instructions.
php artisan test tests/Feature/ProductTest.php # all skipped ✓
php artisan test tests/Unit/ProductServiceTest.php # all skipped ✓Enable the feature tests by:
- Registering the route:
Route::apiResource('products', ProductController::class); - Creating a factory:
php artisan make:factory ProductFactory --model=Product - Removing the
markTestSkipped()calls
By default (auto_bind => true in config/base.php), the package scans
app/Interfaces/*RepositoryInterface.php at boot and binds each to its matching
app/Repositories/*Repository.php — no manual registration required.
To manage bindings explicitly, use --provider when generating:
php artisan make:module Product --provider
# Creates app/Providers/RepositoryServiceProvider.php (once)
# and appends the binding for Product.
# Subsequent --provider runs are idempotent — no duplicate entries.Then register the provider once:
// bootstrap/providers.php (Laravel 11+)
App\Providers\RepositoryServiceProvider::class,
// config/app.php (Laravel 10)
'providers' => [App\Providers\RepositoryServiceProvider::class],Or disable auto-binding entirely and declare bindings in config:
// config/base.php
'auto_bind' => false,
'bindings' => [
\App\Interfaces\ProductRepositoryInterface::class
=> \App\Repositories\ProductRepository::class,
],Deprecated since v3.0.0.
make:repositorywill continue to work indefinitely for backward compatibility, but new projects should usemake:module.
make:repository is a thin wrapper that calls make:module with all new components
suppressed (--no-resource --no-policy --no-test --no-enum --no-filter), so the generated
output is identical to what v2.x produced. All existing options (--model,
--controller, --no-service, --no-controller, --no-request, --no-migration,
--provider, --force) are forwarded transparently.
A deprecation notice is printed at runtime:
⚠ make:repository is deprecated. Please use `php artisan make:module` instead.
# These all still work exactly as before:
php artisan make:repository Product
php artisan make:repository BlogPost --model=Post --no-migration
php artisan make:repository Order --controller=OrderApiControllerCreates the configured database if it does not already exist. Supports MySQL and PostgreSQL.
php artisan base:create-database [--connection=]| Option | Description |
|---|---|
--connection= |
Laravel database connection name (defaults to database.default) |
php artisan base:create-database # default connection
php artisan base:create-database --connection=pgsql- If the database already exists, the command exits successfully without changes.
- MySQL: uses
charsetandcollationfrom the connection config. - PostgreSQL: uses
charsetas theENCODING. - Requires the matching PDO extension and a user with
CREATE DATABASEprivileges.
The generator covers most cases, but here is how to wire a module by hand if needed.
namespace App\Interfaces;
use MuhammedSalama\Base\Interfaces\RepositoryInterface;
interface ProductRepositoryInterface extends RepositoryInterface
{
// Add product-specific query methods here when needed.
}namespace App\Repositories;
use App\Interfaces\ProductRepositoryInterface;
use App\Models\Product;
use MuhammedSalama\Base\Repositories\BaseRepository;
class ProductRepository extends BaseRepository implements ProductRepositoryInterface
{
public function __construct(Product $model)
{
parent::__construct($model);
}
}namespace App\Services;
use App\Filters\ProductFilters;
use App\Interfaces\ProductRepositoryInterface;
use Illuminate\Http\Request;
use MuhammedSalama\Base\Services\BaseService;
class ProductService extends BaseService
{
public function __construct(ProductRepositoryInterface $repository)
{
parent::__construct($repository);
}
public function filter(Request $request): ProductFilters
{
return new ProductFilters($request, $this->repository->query());
}
}namespace App\Http\Requests\Product;
use MuhammedSalama\Base\Requests\BaseRequest;
class StoreProductRequest extends BaseRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'status' => ['required', \Illuminate\Validation\Rule::enum(\App\Enums\ProductStatus::class)],
];
}
}A failed validation response:
{
"status": false,
"message": "Validation error",
"errors": {
"name": ["The name field is required."]
}
}namespace App\Http\Controllers;
use App\Http\Requests\Product\StoreProductRequest;
use App\Http\Requests\Product\UpdateProductRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
use App\Services\ProductService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use MuhammedSalama\Base\Helpers\ApiResponse;
class ProductController extends Controller
{
public function __construct(private ProductService $service) {}
public function index(Request $request): JsonResponse
{
$this->authorize('viewAny', Product::class);
return ApiResponse::paginated($this->service->filter($request)->paginate());
}
public function show(int $id): JsonResponse
{
$product = $this->service->find($id);
$this->authorize('view', $product);
return ApiResponse::success(new ProductResource($product));
}
public function store(StoreProductRequest $request): JsonResponse
{
$this->authorize('create', Product::class);
return ApiResponse::created(new ProductResource($this->service->store($request->validated())));
}
public function update(UpdateProductRequest $request, int $id): JsonResponse
{
$product = $this->service->find($id);
$this->authorize('update', $product);
return ApiResponse::success(new ProductResource($this->service->update($id, $request->validated())));
}
public function destroy(int $id): JsonResponse
{
$product = $this->service->find($id);
$this->authorize('delete', $product);
$this->service->destroy($id);
return ApiResponse::success(null, 'Product deleted successfully.');
}
}MuhammedSalama\Base\Interfaces\RepositoryInterface
| Method | Returns | Notes |
|---|---|---|
all(array $columns, array $relations) |
Collection<int, Model> |
Eager-loads $relations |
paginate(int $perPage, array $columns, array $relations) |
LengthAwarePaginator |
|
find(int|string $id, array $columns, array $relations) |
?Model |
null when absent |
findOrFail(int|string $id, array $columns, array $relations) |
Model |
Throws ModelNotFoundException |
findBy(string $column, mixed $value, array $columns) |
?Model |
First match |
create(array $data) |
Model |
Respects $fillable/$guarded |
update(int|string $id, array $data) |
Model |
Finds, updates, returns model |
delete(int|string $id) |
bool |
Throws if absent |
query() |
Builder |
Fresh query builder for complex queries |
MuhammedSalama\Base\Repositories\BaseRepository — implements RepositoryInterface.
Extend it and inject the Eloquent model via the constructor:
class ProductRepository extends BaseRepository implements ProductRepositoryInterface
{
public function __construct(Product $model)
{
parent::__construct($model);
}
}Additional method not in the interface:
| Method | Returns |
|---|---|
getModel(): Model |
The raw model instance |
MuhammedSalama\Base\Interfaces\ServiceInterface
| Method | Returns | Notes |
|---|---|---|
all(array $columns, array $relations) |
Collection<int, Model> |
|
paginate(int $perPage, array $columns, array $relations) |
LengthAwarePaginator |
|
find(int|string $id, array $columns, array $relations) |
Model |
Always throws on missing |
store(array $data) |
Model |
|
update(int|string $id, array $data) |
Model |
|
destroy(int|string $id) |
bool |
MuhammedSalama\Base\Services\BaseService — implements ServiceInterface.
Extend it and inject the repository interface:
class ProductService extends BaseService
{
public function __construct(ProductRepositoryInterface $repository)
{
parent::__construct($repository);
}
}Additional method not in the interface:
| Method | Returns |
|---|---|
repository(): RepositoryInterface |
The underlying repository (for custom queries) |
MuhammedSalama\Base\Requests\BaseRequest — extends Laravel's FormRequest.
| Member | Default | Notes |
|---|---|---|
authorize() |
true |
Override to add gate/policy checks |
rules() |
— | Define validation rules (abstract-like) |
failedValidation() |
— | Throws HttpResponseException with 422 envelope |
MuhammedSalama\Base\Helpers\ApiResponse — all methods return JsonResponse.
ApiResponse::success($data, 'Message'); // 200
ApiResponse::created($data); // 201
ApiResponse::noContent(); // 204
ApiResponse::error('Message', 400, $errors);
ApiResponse::validation($errors); // 422
ApiResponse::notFound('Message'); // 404
ApiResponse::unauthorized(); // 401
ApiResponse::forbidden(); // 403
ApiResponse::paginated($paginator, 'Message'); // 200 + metaStandard success envelope:
{ "status": true, "message": "Success", "data": { ... } }Standard error envelope:
{ "status": false, "message": "Validation error", "errors": { ... } }Paginated envelope:
{
"status": true, "message": "Success",
"data": [ ... ],
"meta": { "current_page": 1, "last_page": 5, "per_page": 15, "total": 72 }
}MuhammedSalama\Base\Traits\ApiResponseTrait — use inside controllers to call response
methods as $this->success(...) instead of ApiResponse::success(...). Produces the
identical JSON envelope.
| Method | Status |
|---|---|
success($data, $message, $code) |
200 |
created($data, $message) |
201 |
error($message, $code, $errors) |
400 |
validationError($errors, $message) |
422 |
notFound($message) |
404 |
unauthorized($message) |
401 |
forbidden($message) |
403 |
paginated($paginator, $message) |
200 |
MuhammedSalama\Base\Traits\ImageUploadTrait — extensions are derived from MIME type, not
the client-supplied filename, preventing extension-spoofing attacks.
| Method | Returns | Description |
|---|---|---|
uploadImage($request, $input, $path) |
string|null |
Store a single image |
uploadMultiImage($request, $input, $path) |
array<int, string> |
Store multiple images |
updateImage($request, $input, $path, $oldPath) |
string|null |
Replace an image, deletes old file |
deleteImage($path) |
void |
Delete an image |
// Store
$path = $this->uploadImage($request, 'image', 'uploads/products');
// Replace (old file is deleted automatically)
$path = $this->updateImage($request, 'image', 'uploads/products', $product->image);
// Delete
$this->deleteImage($product->image);Always validate uploads in the Form Request first:
'image' => 'nullable|image|mimes:jpg,jpeg,png,webp|max:2048',Publish the config:
php artisan vendor:publish --tag=base-configThis creates config/base.php:
return [
// Auto-bind App\Interfaces\{Name}RepositoryInterface → App\Repositories\{Name}Repository
'auto_bind' => true,
// Explicit bindings always registered regardless of auto_bind
'bindings' => [
// \App\Interfaces\ProductRepositoryInterface::class
// => \App\Repositories\EloquentProductRepository::class,
],
];By default, ApiResponse and the traits are used directly from the package namespace — no copying needed. If you want editable local copies under App\:
php artisan vendor:publish --tag=base-helpers # → app/Helpers/ApiResponse.php
php artisan vendor:publish --tag=base-traits # → app/Traits/ApiResponseTrait.php + ImageUploadTrait.php
php artisan vendor:publish --tag=base-config # → config/base.phpAfter publishing, switch use statements to the App\Helpers / App\Traits namespaces. Published files will not receive automatic updates — treat them as your own code.
composer install
composer test # PHPUnit + Orchestra Testbench (SQLite in-memory)
composer analyse # PHPStan level 5 + LarastanThe CI matrix covers PHP 8.1–8.4 × Laravel 10/11/12/13 on every push and pull request.
Deprecation Notice: Function curl_close() is deprecated during Composer.
These come from Composer running on PHP 8.5+. Run composer self-update or use PHP 8.4.
Your requirements could not be resolved … nette/schema requires php 8.1 - 8.4.
A transitive dependency predates PHP 8.5. Update it: composer update nette/schema --with-all-dependencies.
Auto-binding does not seem to work.
Verify that app/Interfaces/ exists and files match *RepositoryInterface.php. Both the interface class and the implementation must be autoloadable (class_exists() in tinker). Confirm auto_bind => true in config/base.php.
Class … not found after running make:module.
Run composer dump-autoload so the PSR-4 autoloader discovers the newly created files.
Policy gates throw AuthorizationException on every request.
The generated policy starts with all gates open (return true). If you bound the policy but all requests fail, check that your Auth::user() is set (routes are authenticated). If running API tests unauthenticated, call $this->withoutMiddleware() or mock the Gate facade.
See CHANGELOG.md for a full list of changes by version.
Contributions are welcome. Please:
- Fork the repository and create a feature branch.
- Write tests for any change in behaviour.
- Ensure
composer testandcomposer analyseboth pass locally. - Open a pull request against
mainwith a clear description of the change. - CI must be green before merging. The workflow runs
composer testacross PHP 8.1–8.4 and Laravel 10/11/12/13 (with appropriate PHP-version excludes), plus PHPStan static analysis.
If you discover a security vulnerability, please use GitHub private vulnerability reporting or contact the maintainer at devmuhammedsalama@gmail.com. Do not disclose vulnerabilities publicly until they have been addressed.
This package is open-sourced software licensed under the MIT license.
Muhammed Salama — @MuhammedMSalama
