labelgrup / laravel-utilities
Utilities to Laravel projects
Package info
github.com/labelgrupnetworks/laravel-utilities
pkg:composer/labelgrup/laravel-utilities
Requires
- php: ^8.0
- ext-curl: *
- ext-zip: *
- laravel/framework: ^9.2|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.18
- phpunit/phpunit: ^9.5
Suggests
- laravel/mcp: Required to use MCP features.
This package is auto-updated.
Last update: 2026-07-30 09:41:20 UTC
README
A comprehensive collection of utilities to improve and streamline Laravel applications. This package provides Artisan commands for scaffolding API-ready components, reusable helper classes, custom exception handling, validation rules, and support for modern API development patterns.
β Requirements
- PHP
^8.0 - Laravel
^9.2|^10.0|^11.0|^12.0|^13.0 - PHP Extensions:
zip,curl
π¦ Installation
composer require labelgrup/laravel-utilities
The package is automatically registered through Laravel's package discovery.
π Table of Contents
βοΈ Commands
π MakeApiRequest
Generates an API request class in app/Http/Requests/Api/ with built-in JSON validation error handling.
php artisan make:api-request {ApiRequestName}
The generated class extends ApiRequest and provides automatic JSON response formatting for validation failures. It's designed to work seamlessly with API endpoints.
Example Usage:
namespace App\Http\Requests\Api; use Labelgrup\LaravelUtilities\Core\Requests\ApiRequest; class CreateUserRequest extends ApiRequest { public function rules() { return [ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users', 'password' => 'required|min:8' ]; } }
π§ MakeUseCase
Generates a Use Case class in app/UseCases/ to encapsulate and organize business logic into reusable, testable actions.
php artisan make:use-case {UseCaseName} {--without-validation}
The generated class extends UseCase, implements UseCaseInterface, and includes built-in exception handling and response management.
Example with Validation:
namespace App\UseCases; use Labelgrup\LaravelUtilities\Core\UseCases\UseCase; use Labelgrup\LaravelUtilities\Core\UseCases\WithValidateInterface; class CreateUserUseCase extends UseCase implements WithValidateInterface { public function __construct( private UserRepository $userRepository ) {} public function action() { // Your business logic here return $this->userRepository->create($this->getData()); } public function validate(): void { $validator = validator($this->getData(), [ 'name' => 'required|string', 'email' => 'required|email|unique:users' ]); if ($validator->fails()) { throw new ValidationException($validator); } } }
Customizing Response:
public string $response_message = 'User created successfully'; public int $success_status_code = 201; // HTTP_CREATED // Or override the handle() method for complete control public function handle(): UseCaseResponse { // Custom logic here return parent::handle(); }
ποΈ Core Classes
ApiRequest
Located in src/Core/Requests/ApiRequest.php, this abstract class extends Laravel's FormRequest and automatically formats validation errors as JSON responses, making it ideal for API controllers.
Features:
- Automatic JSON error responses for validation failures
- Uses
ApiResponse::fail()for consistent error formatting - Returns HTTP 422 (Unprocessable Entity) on validation error
UseCase & UseCaseInterface
Located in src/Core/UseCases/, these classes provide a pattern for organizing business logic.
UseCase Class:
- Abstract base class with built-in exception handling
- Calls
validate()automatically if theWithValidateInterfaceis implemented - Wraps errors in
UseCaseResponsewith proper HTTP status codes - Configurable
success_status_codeandresponse_message
UseCaseInterface:
Requires implementation of the action() method, which contains your business logic.
UseCaseResponse
A standardized response object returned by all Use Cases containing:
success(bool) - Whether the operation succeededmessage(string) - Response messagehttp_code(int) - HTTP status codedata(mixed) - Response payloaderror_code(string|null) - Custom error code for logging/debuggingtrace(array) - Exception trace (only on failures)
β CustomException
Provides a custom exception class that extends Exception and implements Laravel's Renderable interface for fine-grained control over API error responses.
public function __construct( public string $error_code, public string $error_message, public int $http_code = Response::HTTP_INTERNAL_SERVER_ERROR, public ?array $report_data = ['logs' => []], public ?array $response = [], public bool $should_render = true )
Parameters:
error_code: Unique identifier for the exception (e.g.,'USER_NOT_FOUND')error_message: Human-readable error messagehttp_code: HTTP status code (defaults to 500)report_data: Additional context for loggingresponse: Custom response datashould_render: Whether to render the exception
Example Usage:
throw new CustomException( error_code: 'USER_NOT_FOUND', error_message: 'The requested user does not exist', http_code: Response::HTTP_NOT_FOUND );
Automatic Logging: The exception automatically logs request information including method, parameters, and user context when reported.
π§ Helpers
ApiResponse
Provides fluent methods for consistent API response formatting.
Available Methods:
ok(data, code = 200, streamJson = false): JsonResponse|StreamedJsonResponse
Returns a success response with data only.
return ApiResponse::ok(['users' => $users]);
done(message, data = [], code = 200, streamJson = false): JsonResponse|StreamedJsonResponse
Returns a success response with message and optional data.
return ApiResponse::done('Users retrieved successfully', ['users' => $users]);
fail(message, data = [], code = 400, error_code = null, trace = []): JsonResponse|StreamedJsonResponse
Returns a failure response with error information.
return ApiResponse::fail('Validation failed', $errors, Response::HTTP_UNPROCESSABLE_ENTITY);
stream(data): StreamedJsonResponse
Returns a streamed JSON response for large datasets.
return ApiResponse::stream($largeDataset);
ExceptionHandler
Provides a centralized exception rendering system for both legacy (Laravel <11) and current Laravel versions.
Static render() Method:
Handles rendering of various exception types including:
CustomException- Custom error responsesValidationException- Validation error responsesAuthenticationException- Authentication failures (401)UnauthorizedException- Authorization failures (401)NotFoundHttpException- Resource not found (404)HttpException- Generic HTTP exceptions
Usage in Exception Handler:
public function render($request, Throwable $exception) { return ExceptionHandler::render($exception, $request); }
Image
Provides image manipulation utilities.
getExtensionImageFromUrl(url): ?string
Extracts the file extension from an image URL.
$ext = Image::getExtensionImageFromUrl('https://example.com/image.jpg'); // 'jpg'
destroy(src): bool
Deletes an image from storage.
Image::destroy('uploads/profile.jpg'); // true/false
downloadFromUrl(url, fileName): void
Downloads an image from a URL and saves it locally. Uses cURL with proper headers and timeouts.
Image::downloadFromUrl('https://example.com/avatar.png', 'local_avatar.png');
Password
Provides password generation and validation with security checks.
rule(min_size = 12): PasswordRule
Returns a validated password rule with requirements:
- Minimum length (default 12 characters)
- Mixed case letters
- Numbers
- Symbols
- Must not be in known leaked password databases
'password' => [ 'required', 'confirmed', Password::rule(16) // Minimum 16 characters ]
generateSecurePassword(length = 12, max_retries = 5): string
Generates a cryptographically secure random password that is not in breach databases.
Attempts up to max_retries times to generate a password not found in leaked password databases.
$password = Password::generateSecurePassword(16); // length: 16
Text
String manipulation utilities with UTF-8 support.
sanitize(text, divider = '-'): string
Sanitizes text for URL-safe slugs:
- Removes non-alphanumeric characters
- Converts to ASCII transliteration
- Lowercases the result
- Removes duplicate separators
- Returns 'n-a' for empty results
Text::sanitize('HΓ©llo WΓΈrld!'); // 'hello-world' Text::sanitize('User Profile', '_'); // 'user_profile'
Time
Converts seconds to human-readable time format.
parseTimeForHumans(seconds, unitMin = 's'): string
Breaks down seconds into readable time units (weeks, days, hours, minutes, seconds).
unitMinparameter controls the minimum unit to display:'s'- seconds (default)'i'- minutes'h'- hours'd'- days'w'- weeks
Time::parseTimeForHumans(3661); // '1 hour 1 minute 1 second' Time::parseTimeForHumans(86400, 'h'); // '1 day' Time::parseTimeForHumans(604800, 'd'); // '1 week'
Zip
File compression utilities.
create(zipFile, sourcePath): ?string
Creates a ZIP archive from a directory recursively.
Returns the path to the created ZIP file or null on failure.
Zip::create( storage_path('exports/archive.zip'), storage_path('files') );
The method:
- Preserves directory structure
- Includes all files recursively
- Returns the file path on success
π― Validation Rules
SlugRule
Validates that a string conforms to valid slug format ([a-z0-9]+(?:-[a-z0-9]+)*).
A slug must:
- Contain only lowercase letters and numbers
- Allow hyphens as separators
- Start and end with alphanumeric characters
Usage:
use Labelgrup\LaravelUtilities\Rules\SlugRule; 'slug' => ['required', 'string', new SlugRule()] // Valid: product-name, user-profile-123 // Invalid: Product-Name, user_profile, -invalid-
π€ MCP Tools (AI\Mcp)
Framework for building laravel/mcp tools that reuse an app's existing controllers or use cases instead of re-implementing business logic. Requires laravel/mcp (listed under suggest, not require β install it yourself if you use this namespace; the package auto-detects its presence, see Configuration).
Labelgrup\LaravelUtilities\AI\Mcp\
ββ Schemas/
β ββ Attributes/{Schema, OutputSchema} β declare a Tool's input/output schema by class reference
β ββ Interfaces/{SchemaInterface, OutputSchemaInterface}
β ββ Traits/PaginationTrait β generic paginated-output schema (integers only)
ββ Tools/
β ββ Abstracts/{ControllerTool, UseCaseTool} β the two Tool bases
β ββ Attributes/RequestClass β declares a UseCaseTool's validating FormRequest
β ββ DTO/{EndpointDTO, UseCaseDTO}
β ββ Errors/DefaultToolErrorResolver
β ββ Interfaces/{ControllerToolInterface, UseCaseToolInterface, ToolErrorResolverInterface,
β β ToolErrorResponseBuilderInterface, McpScopeAuthorizerInterface}
β ββ Resolvers/{ResolvesToolResponse, ResolvesToolSchemas}
ββ Resources/
ββ ServerToolResolver β reads a Server's $tools without instantiating it
ββ ToolsSchemaCatalogBuilder β builds the tools+schemas catalog for one server
ββ Abstracts/AbstractToolsSchemaCatalogResource β Resource base wiring the two above together
Labelgrup\LaravelUtilities\Commands\
ββ McpToolsSchemaSnapshot β `mcp:tools:schema-snapshot` Artisan command
Tool bases: ControllerTool & UseCaseTool
ControllerTool reuses an existing API controller method. The Tool declares an EndpointDTO (controller class, method, optional FormRequest class, plus scalar route params/Eloquent models for methods with route-model-binding). The base builds and validates that FormRequest from the MCP arguments, calls the controller, and maps the resulting JsonResponse:
use Labelgrup\LaravelUtilities\AI\Mcp\Tools\Abstracts\ControllerTool; use Labelgrup\LaravelUtilities\AI\Mcp\Tools\DTO\EndpointDTO; class SearchProductTool extends ControllerTool { public function endpoint(): EndpointDTO { return new EndpointDTO(SearchEngineController::class, 'searchProducts', SearchProductRequest::class); } public function schema(JsonSchema $schema): array { return [ /* input schema */ ]; } }
request is nullable (no-FormRequest methods are called with no arguments). The controller's own validation, guards and permission checks apply for free β the Tool never bypasses them.
UseCaseTool calls a use case directly, without an HTTP controller in between. The Tool declares a UseCaseDTO (the use case instance + responseToApi() flags). Optionally, a #[RequestClass(SomeFormRequest::class)] class attribute tells handle() which FormRequest's rules() validate the incoming MCP arguments before useCase() runs:
use Labelgrup\LaravelUtilities\AI\Mcp\Tools\Abstracts\UseCaseTool; use Labelgrup\LaravelUtilities\AI\Mcp\Tools\Attributes\RequestClass; use Labelgrup\LaravelUtilities\AI\Mcp\Tools\DTO\UseCaseDTO; #[RequestClass(SearchProductRequest::class)] class SearchProductTool extends UseCaseTool { public function useCase(Request $request): UseCaseDTO { return new UseCaseDTO( use_case: new SearchProductsUseCase( query: $request->get('query'), page: $request->get('page', 1), ), response_simplified: true ); } public function schema(JsonSchema $schema): array { return [ /* input schema */ ]; } }
useCase() runs inside the response wrapper, so its own validation/construction errors are mapped cleanly. When #[RequestClass] is present, handle() validates $request against that FormRequest's rules() via Laravel\Mcp\Request::validate() and merges the validated (and defaulted) data back into $request β so useCase() reads already-validated values via $request->get(...), with useCase()'s own signature never tied to the concrete FormRequest class. A validation failure throws ValidationException before useCase() runs, mapped the same way as any other error below. A Tool without the attribute skips this step entirely β useCase() is free to validate however it likes, or not at all. Note that handle()'s default flow calls $use_case->handle()->responseToApi(...) β the use case's own business exceptions are swallowed into a JsonResponse by UseCase::handle() before ever reaching the error resolver below. A Tool that wants a business exception to reach the error resolver raw must override handle() and call perform()/action() directly instead.
useCase() isn't limited to $request->get(...): $this->resolveRequestClass($request) builds, container-resolves and validates a populated instance of the declared #[RequestClass] FormRequest (mirroring ControllerTool::formRequest() β setContainer(), setRedirector(), validateResolved()), so ->validated(), ->authorize() and any container-dependent getters/rules on that FormRequest all work, and a Tool can call it inside useCase() for typed getters/accessors instead of reading raw array keys off $request. It returns $request unchanged when no #[RequestClass] is declared. This is opt-in and independent of handle()'s automatic validation step β calling both against the same #[RequestClass] re-runs rules() a second time (harmless against already-validated, defaulted data, but redundant work).
A consumer needing a third input shape (e.g. Spatie\LaravelData\Data objects) implements its own sibling of these two β see NAP's DataObjectControllerTool for a worked example β rather than this package trying to cover every possible input shape.
Response & error mapping
Both bases use the ResolvesToolResponse trait, funneling every call through respond(callable):
authorizeScope()runs first β throwsUnauthorizedExceptionif the Tool isn't#[IsReadOnly]and the caller lacks write access (see Scope authorization);- the callable's return is normalised:
stringβ text,arrayβ structured (via the overridabletransformResponse()hook),JsonResponseβ mapped by HTTP status (responseFromJsonResponse()), anything else β'Success'; - any
Throwableβreport($e)+errorResponse($e).
responseFromJsonResponse() maps by status: 5xx β generic internal error; 4xx β error (error/message from the body, sanitized); 204/empty (with no output schema declared) β 'Success'; otherwise β the structured body.
errorResponse() delegates to a configurable resolver (config('laravel-utilities.mcp.errors.resolver'), default DefaultToolErrorResolver β must implement ToolErrorResolverInterface). DefaultToolErrorResolver::resolve():
ValidationExceptionβ flattened field errors.HttpResponseExceptionβ unwraps the underlyingJsonResponse.- Otherwise, if the exception's class is listed in
config('laravel-utilities.mcp.errors.exposed_exceptions')β delegates to the app's ownExceptionHandler::render()(same mapping as the REST API). - Otherwise, if
app.debugistrueβ the raw (sanitized) exception message. - Otherwise β generic
'Tool is temporarily unavailable'(the real error only reaches the log, viareport()).
A consumer can add business exceptions to exposed_exceptions with zero code changes, or swap errors.resolver entirely for a project-specific mapping without forking this package.
Scope authorization
shouldRegister()/authorizeScope() gate writes: a Tool without #[IsReadOnly] requires write access; read-only tools are always listed and callable. Write access itself is resolved via config('laravel-utilities.mcp.scope_authorizer') β a class implementing McpScopeAuthorizerInterface::canWrite(): bool, bound in the consuming app's own container/config. This package has no opinion on how a consumer determines write access (token scopes, roles, anything else) β it only defines the interface and the resolution point. If unset, canWrite() defaults to unrestricted (true).
Input/output schemas
ResolvesToolSchemas provides default schema()/outputSchema() implementations for Tools that declare #[Schema(SomeClass::class)] / #[OutputSchema(SomeClass::class, key: '...', many: false, scalar: false)] instead of hand-writing the array inline. #[Schema] is repeatable (merged in declaration order) for input combining multiple shape sources. A Tool can still override either method directly β an explicit override always wins over the attribute.
PaginationTrait::paginationOutputSchema() provides a generic paginated-output shape (items + data.{current_page,last_page,total_items}, all plain integers). A consumer whose pagination metadata needs a richer numeric type (e.g. formatted/localized numbers) writes its own sibling trait instead of extending this one β see NAP's PaginationGenesisTrait for a worked example.
Tools schema catalog & version snapshot
ServerToolResolver::toolClasses(ServerClass::class) reads a Server subclass's $tools default property via reflection, without instantiating it β Laravel\Mcp\Server::__construct() requires a real Transport, only available inside an actual MCP request. ToolsSchemaCatalogBuilder::build($tool_classes, ?$decorate) turns that list into a catalog (['tools' => [...]], sorted by name) by reusing each Tool's own toArray() β the same shape a real MCP client sees in tools/list β with a fixed key order and all 4 MCP annotation hints always explicit, so two catalogs diff cleanly. Never pre-filter $tool_classes by a per-caller eligibility/scope check before passing it in β that would make the catalog vary by reader.
AbstractToolsSchemaCatalogResource (extends Laravel\Mcp\Server\Resource) wires both together for exposing the catalog live, as an MCP Resource, on any server:
use Labelgrup\LaravelUtilities\AI\Mcp\Resources\Abstracts\AbstractToolsSchemaCatalogResource; class MyServerToolsSchemaCatalogResource extends AbstractToolsSchemaCatalogResource { protected function serverClass(): string { return MyServer::class; } }
Register it on the target server's $resources. A consuming project that wants to enrich each entry (e.g. a domain grouping derived from the tool's namespace, or scope metadata) overrides the protected decorateEntry(array $entry, string $tool_class): array hook β identity by default, so overriding is opt-in and never required.
For a git-diffable history of tool/schema changes across commits, php artisan mcp:tools:schema-snapshot {server?} writes the same catalog to a tracked JSON file per configured server (config('laravel-utilities.mcp.servers'), a slug => ['class' => Server::class, 'schema_snapshot_path' => '...'] map β empty by default, so a consumer must configure it; omit {server} to regenerate all). Each server writes to its own schema_snapshot_path (defaults to storage_path('app/mcp-tools-schema-snapshots') when a server entry omits it β a consuming project typically points each server at a path of its own choosing, e.g. next to that server's own Resources), as {slug}-server-tools-schema-catalog.json.
Configuration
Publish the config with php artisan vendor:publish --tag=laravel-utilities-config (or copy config/laravel-utilities.php from the package). The mcp block:
'mcp' => [ // Auto-detects laravel/mcp; only pays the cost of the rate limiter/config validation when true. 'enabled' => env('LARAVEL_UTILITIES_MCP_ENABLED', class_exists(\Laravel\Mcp\Server::class)), // Must implement McpScopeAuthorizerInterface. Null = unrestricted writes. 'scope_authorizer' => null, // Servers snapshotted by `mcp:tools:schema-snapshot`, keyed by a short slug // (used as both the CLI argument and the snapshot filename). Empty by default. // 'schema_snapshot_path' is optional per entry β defaults to // storage_path('app/mcp-tools-schema-snapshots') when omitted. 'servers' => [ // 'my-server' => [ // 'class' => \App\Mcp\Servers\MyServer::class, // 'schema_snapshot_path' => app_path('Mcp/Resources/snapshots'), // ], ], 'errors' => [ 'resolver' => \Labelgrup\LaravelUtilities\AI\Mcp\Tools\Errors\DefaultToolErrorResolver::class, 'exposed_exceptions' => [ // FQCNs whose ->getMessage() is safe to expose to the MCP client. ], ], 'rate_limit' => [ 'per_minute' => env('MCP_RATE_LIMIT_PER_MINUTE', 60), 'whitelist_ips' => [], ], ],
When mcp.enabled is true, LaravelUtilitiesServiceProvider registers a named mcp rate limiter (RateLimiter::for('mcp', ...), throwing CustomException with AI-MCP-RATELIMIT-0001 on HTTP_TOO_MANY_REQUESTS) and validates the config β a consumer only needs to apply throttle:mcp to its own MCP route(s).
Claude Code skills installer
The package ships two Claude Code skills under skills/Mcp/ (mcp-tool-builder, mcp-tool-reviewer) to help build/review Tools against the conventions above. Publishing them is opt-in β deliberately not a Composer Plugin, so it never runs (or requires trust-gate approval) on every consumer's composer install/update, only when a project actually wants them.
When mcp.enabled is true, LaravelUtilitiesServiceProvider::publishSkills() registers each skills/Mcp/<name> for publishing under the laravel-utilities-skills tag:
php artisan vendor:publish --tag=laravel-utilities-skills
This copies each skill into the consuming project's .claude/skills/<name>. Like any Laravel publishes() call, existing files at the target are left untouched unless --force is passed.
π License
MIT License. See the LICENSE file for details.
π₯ Authors
- Eric RF - erojas@labelgrup.com
- Manel Alonso - malonso@labelgrup.com