PHPackages                             philiprehberger/laravel-operation-result - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. philiprehberger/laravel-operation-result

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

philiprehberger/laravel-operation-result
========================================

Typed Result pattern for Laravel service-layer operations with named factory methods and specialized result types

v1.1.0(3mo ago)160MITPHPPHP ^8.2CI passing

Since Mar 6Pushed 1mo agoCompare

[ Source](https://github.com/philiprehberger/laravel-operation-result)[ Packagist](https://packagist.org/packages/philiprehberger/laravel-operation-result)[ Docs](https://github.com/philiprehberger/laravel-operation-result)[ RSS](/packages/philiprehberger-laravel-operation-result/feed)WikiDiscussions main Synced 3w ago

READMEChangelogDependencies (16)Versions (9)Used By (0)

Laravel Operation Result
========================

[](#laravel-operation-result)

[![Tests](https://github.com/philiprehberger/laravel-operation-result/actions/workflows/tests.yml/badge.svg)](https://github.com/philiprehberger/laravel-operation-result/actions/workflows/tests.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/47ef754e7ac5595299a5b8e90374b3afc6612311b9bdbd159ef876831134146d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7068696c69707265686265726765722f6c61726176656c2d6f7065726174696f6e2d726573756c742e737667)](https://packagist.org/packages/philiprehberger/laravel-operation-result)[![Last updated](https://camo.githubusercontent.com/f4c9890143512b1bdcb453d672fab892f05be942a1e71373debf0b6357ffca8e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6173742d636f6d6d69742f7068696c69707265686265726765722f6c61726176656c2d6f7065726174696f6e2d726573756c74)](https://github.com/philiprehberger/laravel-operation-result/commits/main)

Typed Result pattern for Laravel service-layer operations with named factory methods and specialized result types.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Laravel 11 or 12

Installation
------------

[](#installation)

```
composer require philiprehberger/laravel-operation-result
```

No service provider registration is needed. The classes are ready to use immediately.

Usage
-----

[](#usage)

### Why Use This?

[](#why-use-this)

Without a result pattern, service methods either throw exceptions for every failure or return ambiguous booleans/nulls that force controllers to guess what went wrong. Result objects make the contract explicit:

```
// Without result objects
public function createClient(array $data): Client
{
    // Throws on validation, throws on DB error, throws on auth — controller catches them all
}

// With result objects
public function createClient(array $data): OperationResult
{
    // Returns a structured result — controller knows exactly what to check
}
```

### Available Result Types

[](#available-result-types)

ClassUse Case`OperationResult`Model CRUD operations (create, update, delete)`BulkActionResult`Operations on multiple items at once`CollectionResult`Service methods returning lists or paginated data`ValidationResult`Data and template validation with errors and warnings`RateLimitResult`API rate limit checks with HTTP header generation`UndoResult`Undo operations tracking restored vs failed itemsAll classes implement `ResultContract` and extend the abstract `Result` base class.

### OperationResult

[](#operationresult)

Use when a service method creates, reads, updates, or deletes an Eloquent model.

#### Service

[](#service)

```
use PhilipRehberger\OperationResult\OperationResult;

class ClientService
{
    public function create(array $data): OperationResult
    {
        if (!auth()->user()->can('create', Client::class)) {
            return OperationResult::unauthorized();
        }

        $validator = Validator::make($data, ['name' => 'required|string|max:255']);

        if ($validator->fails()) {
            return OperationResult::validationFailed('Validation failed', $validator->errors()->toArray());
        }

        $client = Client::create($data);

        return OperationResult::created($client);
    }

    public function update(Client $client, array $data): OperationResult
    {
        $client->update($data);

        return OperationResult::updated($client, 'Client profile updated.');
    }

    public function delete(int $id): OperationResult
    {
        $client = Client::find($id);

        if (!$client) {
            return OperationResult::notFound('Client not found.');
        }

        $client->delete();

        return OperationResult::deleted();
    }
}
```

#### Controller

[](#controller)

```
public function store(StoreClientRequest $request, ClientService $service): JsonResponse
{
    $result = $service->create($request->validated());

    if ($result->failed()) {
        return response()->json($result->toArray(), match ($result->getErrorCode()) {
            'UNAUTHORIZED'       => 403,
            'VALIDATION_FAILED'  => 422,
            default              => 500,
        });
    }

    return response()->json($result->toArray(), 201);
}
```

#### Factory Methods

[](#factory-methods)

MethodDescription`OperationResult::created($model, $message)`Success — model was created`OperationResult::updated($model, $message)`Success — model was updated`OperationResult::deleted($message)`Success — model was deleted`OperationResult::success($model, $message)`Generic success, model optional`OperationResult::failure($message, $errorCode, $data)`Generic failure`OperationResult::notFound($message)`404-style failure, error code `NOT_FOUND``OperationResult::validationFailed($message, $errors)`Validation failure, error code `VALIDATION_FAILED``OperationResult::unauthorized($message)`Auth failure, error code `UNAUTHORIZED`#### Additional Methods

[](#additional-methods)

```
$result->getModel();                   // ?Model
$result->getData();                    // array
$result->withData(['key' => 'value']); // returns new instance with merged data
$result->withMessage('New msg');       // returns new instance with updated message
$result->getOrThrow();                 // Model|array — throws \RuntimeException on failure
$result->getErrorCode();               // ?string
$result->isNotFound();                 // bool
$result->isUnauthorized();             // bool
$result->isValidationFailed();         // bool
$result->toArray();                    // array
```

### BulkActionResult

[](#bulkactionresult)

Use when operating on multiple items at once, such as bulk-deleting, bulk-archiving, or bulk-updating a set of records.

#### Service

[](#service-1)

```
use PhilipRehberger\OperationResult\BulkActionResult;

class BulkClientService
{
    public function archiveMany(array $ids): BulkActionResult
    {
        $processed = 0;
        $details = [];

        foreach ($ids as $id) {
            $client = Client::find($id);

            if (!$client) {
                $details[] = ['id' => $id, 'success' => false, 'error' => 'Not found'];
                continue;
            }

            $client->update(['status' => 'archived']);
            $details[] = ['id' => $id, 'success' => true];
            $processed++;
        }

        $failed = count($ids) - $processed;

        if ($failed > 0 && $processed > 0) {
            return BulkActionResult::partial($processed, $failed, "{$processed} archived, {$failed} failed.", $details);
        }

        if ($failed > 0) {
            return BulkActionResult::failure('No clients were archived.', null, $details);
        }

        $undoToken = Str::uuid()->toString();
        Cache::put("undo:{$undoToken}", $ids, now()->addMinutes(10));

        return BulkActionResult::success($processed, "{$processed} clients archived.", $details, $undoToken);
    }
}
```

#### Controller

[](#controller-1)

```
public function bulkArchive(BulkArchiveRequest $request, BulkClientService $service): JsonResponse
{
    $result = $service->archiveMany($request->input('ids'));

    $status = $result->succeeded() ? 200 : 422;

    return response()->json($result->toArray(), $status);
}
```

#### Factory Methods

[](#factory-methods-1)

MethodDescription`BulkActionResult::success($processed, $message, $details, $undoToken, $undoExpiresAt)`All items processed`BulkActionResult::partial($processed, $failed, $message, $details, $undoToken, $undoExpiresAt)`Mixed results`BulkActionResult::failure($message, $errorCode, $details)`Complete failure#### Additional Methods

[](#additional-methods-1)

```
$result->hasFailures();     // bool — true if any items failed
$result->isComplete();      // bool — true if processed > 0 and failed === 0
$result->getFailedIds();    // array — IDs from details where success === false
$result->getSuccessIds();   // array — IDs from details where success === true
$result->canUndo();         // bool — true if undoToken is set
```

### CollectionResult

[](#collectionresult)

Use when a service method returns a list of items, with or without pagination.

#### Service

[](#service-2)

```
use PhilipRehberger\OperationResult\CollectionResult;

class ProjectService
{
    public function listForClient(int $clientId, int $page = 1, int $perPage = 15): CollectionResult
    {
        $paginator = Project::where('client_id', $clientId)
            ->orderByDesc('created_at')
            ->paginate($perPage, ['*'], 'page', $page);

        if ($paginator->isEmpty()) {
            return CollectionResult::empty('No projects found for this client.');
        }

        return CollectionResult::paginated(
            $paginator->getCollection(),
            total: $paginator->total(),
            page: $page,
            perPage: $perPage
        );
    }

    public function getRecent(): CollectionResult
    {
        try {
            $projects = Project::orderByDesc('updated_at')->limit(10)->get();

            return CollectionResult::withItems($projects, $projects->count());
        } catch (\Exception $e) {
            return CollectionResult::failure('Could not load projects.', 'DB_ERROR');
        }
    }
}
```

#### Controller

[](#controller-2)

```
public function index(Request $request, ProjectService $service): JsonResponse
{
    $result = $service->listForClient(
        $request->user()->client_id,
        $request->integer('page', 1)
    );

    if ($result->failed()) {
        return response()->json(['error' => $result->getMessage()], 500);
    }

    return response()->json($result->toArray());
}
```

#### Factory Methods

[](#factory-methods-2)

MethodDescription`CollectionResult::withItems($items, $total, $message)`Success with a list, no pagination`CollectionResult::paginated($items, $total, $page, $perPage, $message)`Success with pagination metadata`CollectionResult::empty($message)`Success with zero items`CollectionResult::failure($message, $errorCode)`Failure#### Additional Methods

[](#additional-methods-2)

```
$result->getItems();   // Collection|array
$result->getTotal();   // ?int
$result->count();      // int — count of items in this result
$result->isEmpty();    // bool
$result->hasMore();    // bool — true when more pages exist
```

### ValidationResult

[](#validationresult)

Use when a service or class validates data, tracking both hard errors (blocking) and soft warnings (advisory).

#### Service

[](#service-3)

```
use PhilipRehberger\OperationResult\ValidationResult;

class InvoiceTemplateValidator
{
    public function validate(array $templateData): ValidationResult
    {
        $errors = [];
        $warnings = [];

        if (empty($templateData['line_items'])) {
            $errors['line_items'] = 'At least one line item is required.';
        }

        if (!isset($templateData['due_date'])) {
            $warnings['due_date'] = 'No due date set; invoice will have no payment deadline.';
        }

        if (!empty($errors)) {
            return ValidationResult::invalid($errors, $warnings);
        }

        return ValidationResult::valid($warnings);
    }
}
```

#### Controller

[](#controller-3)

```
public function validateTemplate(Request $request, InvoiceTemplateValidator $validator): JsonResponse
{
    $result = $validator->validate($request->all());

    $status = $result->isValid() ? 200 : 422;

    return response()->json($result->toArray(), $status);
}
```

#### Factory Methods

[](#factory-methods-3)

MethodDescription`ValidationResult::valid($warnings)`Passes, optional warnings`ValidationResult::invalid($errors, $warnings)`Fails with errors, optional warnings`ValidationResult::failure($message, $errorCode)`Unexpected failure (not a validation error)#### Additional Methods

[](#additional-methods-3)

```
$result->isValid();       // bool
$result->hasErrors();     // bool
$result->hasWarnings();   // bool
$result->getErrors();     // array
$result->getWarnings();   // array
```

### RateLimitResult

[](#ratelimitresult)

Use when checking or enforcing API rate limits. Provides typed results and generates standard HTTP rate-limit response headers.

#### Service

[](#service-4)

```
use PhilipRehberger\OperationResult\RateLimitResult;

class ApiRateLimiter
{
    public function check(string $apiKey, string $scope): RateLimitResult
    {
        $limit = 1000;
        $window = 3600; // 1 hour
        $cacheKey = "rate_limit:{$apiKey}:{$scope}";
        $resetAt = now()->addHour()->timestamp;

        $current = Cache::increment($cacheKey);

        if ($current === 1) {
            Cache::expire($cacheKey, $window);
        }

        $remaining = max(0, $limit - $current);

        if ($current > $limit) {
            $ttl = Cache::ttl($cacheKey);
            return RateLimitResult::denied($limit, $resetAt, $ttl);
        }

        return RateLimitResult::allowed($limit, $remaining, $resetAt);
    }
}
```

#### Middleware

[](#middleware)

```
public function handle(Request $request, Closure $next): Response
{
    $result = $this->rateLimiter->check($request->header('X-API-Key'), 'default');

    $response = $result->isDenied()
        ? response()->json(['error' => $result->getMessage()], 429)
        : $next($request);

    foreach ($result->getHeaders() as $header => $value) {
        $response->headers->set($header, $value);
    }

    return $response;
}
```

#### Factory Methods

[](#factory-methods-4)

MethodDescription`RateLimitResult::allowed($limit, $remaining, $resetAt)`Request is within limit`RateLimitResult::denied($limit, $resetAt, $retryAfter)`Limit exceeded, error code `RATE_LIMITED`#### Additional Methods

[](#additional-methods-4)

```
$result->isAllowed();   // bool
$result->isDenied();    // bool
$result->getHeaders();  // array — X-RateLimit-* headers, plus Retry-After when denied
```

### UndoResult

[](#undoresult)

Use when reversing a previous bulk operation, tracking how many items were restored successfully vs how many failed.

#### Service

[](#service-5)

```
use PhilipRehberger\OperationResult\UndoResult;

class UndoService
{
    public function undo(string $token): UndoResult
    {
        $ids = Cache::pull("undo:{$token}");

        if (!$ids) {
            return UndoResult::failure('Undo token not found or has expired.', 'TOKEN_EXPIRED');
        }

        $restored = 0;
        $failed = 0;

        foreach ($ids as $id) {
            $client = Client::withTrashed()->find($id);

            if ($client && $client->restore()) {
                $restored++;
            } else {
                $failed++;
            }
        }

        if ($failed > 0 && $restored > 0) {
            return UndoResult::partial($restored, $failed, "{$restored} clients restored, {$failed} could not be undone.");
        }

        if ($failed > 0) {
            return UndoResult::failure('Undo failed for all items.');
        }

        return UndoResult::success($restored);
    }
}
```

#### Controller

[](#controller-4)

```
public function undo(string $token, UndoService $service): JsonResponse
{
    $result = $service->undo($token);

    $status = $result->succeeded() ? 200 : 422;

    return response()->json($result->toArray(), $status);
}
```

#### Factory Methods

[](#factory-methods-5)

MethodDescription`UndoResult::success($restored, $message)`All items restored`UndoResult::partial($restored, $failed, $message)`Mixed results`UndoResult::failure($message, $errorCode)`Complete failure#### Additional Methods

[](#additional-methods-5)

```
$result->hasFailures();  // bool — true if any items could not be restored
```

### The ResultContract Interface

[](#the-resultcontract-interface)

All result types implement `PhilipRehberger\OperationResult\Contracts\ResultContract`:

```
interface ResultContract
{
    public function succeeded(): bool;
    public function failed(): bool;
    public function getMessage(): string;
    public function toArray(): array;
}
```

Use this interface for type hints when you accept any result type:

```
public function logResult(ResultContract $result): void
{
    Log::info($result->getMessage(), $result->toArray());
}
```

### Type-Checking Helpers

[](#type-checking-helpers)

Every result type includes convenience methods for checking common error codes:

```
$result = $service->findClient($id);

if ($result->isNotFound()) {
    abort(404, $result->getMessage());
}

if ($result->isUnauthorized()) {
    abort(403, $result->getMessage());
}

if ($result->isValidationFailed()) {
    return back()->withErrors($result->getData()['errors'] ?? []);
}
```

### Getting Data or Throwing

[](#getting-data-or-throwing)

Use `getOrThrow()` to extract data from a successful result or throw on failure:

```
// Returns the model (OperationResult) or null (base Result) on success
$client = $service->findClient($id)->getOrThrow();

// Throws \RuntimeException with the error message on failure
try {
    $model = $service->create($data)->getOrThrow();
} catch (\RuntimeException $e) {
    Log::error($e->getMessage());
}
```

### Immutable Message Updates

[](#immutable-message-updates)

Use `withMessage()` to create a new result instance with a different message:

```
$result = $service->create($data);

if ($result->succeeded()) {
    $result = $result->withMessage('Client was created and synced to CRM.');
}

return response()->json($result->toArray());
```

API
---

[](#api)

ClassUse Case`OperationResult`Model CRUD operations (create, update, delete)`BulkActionResult`Operations on multiple items at once`CollectionResult`Service methods returning lists or paginated data`ValidationResult`Data and template validation with errors and warnings`RateLimitResult`API rate limit checks with HTTP header generation`UndoResult`Undo operations tracking restored vs failed itemsAll classes implement `ResultContract`: `succeeded()`, `failed()`, `getMessage()`, `toArray()`.

### Base Methods (All Result Types)

[](#base-methods-all-result-types)

Every result class inherits these methods from `Result`:

MethodReturnsDescription`succeeded()``bool`True if the operation succeeded`failed()``bool`True if the operation failed`getMessage()``string`The result message`getErrorCode()``?string`The error code, if any`isNotFound()``bool`True if error code is `NOT_FOUND``isUnauthorized()``bool`True if error code is `UNAUTHORIZED``isValidationFailed()``bool`True if error code is `VALIDATION_FAILED``getOrThrow()``mixed`Returns data on success, throws `\RuntimeException` on failure`withMessage(string $message)``static`Returns a new instance with the updated message`toArray()``array`Array representation of the resultDevelopment
-----------

[](#development)

```
composer install
vendor/bin/phpunit
vendor/bin/pint --test
vendor/bin/phpstan analyse
```

Support
-------

[](#support)

If you find this project useful:

⭐ [Star the repo](https://github.com/philiprehberger/laravel-operation-result)

🐛 [Report issues](https://github.com/philiprehberger/laravel-operation-result/issues?q=is%3Aissue+is%3Aopen+label%3Abug)

💡 [Suggest features](https://github.com/philiprehberger/laravel-operation-result/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)

❤️ [Sponsor development](https://github.com/sponsors/philiprehberger)

🌐 [All Open Source Projects](https://philiprehberger.com/open-source-packages)

💻 [GitHub Profile](https://github.com/philiprehberger)

🔗 [LinkedIn Profile](https://www.linkedin.com/in/philiprehberger)

License
-------

[](#license)

[MIT](LICENSE)

###  Health Score

42

—

FairBetter than 89% of packages

Maintenance87

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 95.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

8

Last Release

96d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/cfd7d24cbbf32400fa13ce0bbe7a31edd2d66a6d4488eafdb3d64c5337bf0435?d=identicon)[philiprehberger](/maintainers/philiprehberger)

---

Top Contributors

[![philiprehberger](https://avatars.githubusercontent.com/u/8218077?v=4)](https://github.com/philiprehberger "philiprehberger (23 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

responselaravelserviceresultpatternoperation

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/philiprehberger-laravel-operation-result/health.svg)

```
[![Health](https://phpackages.com/badges/philiprehberger-laravel-operation-result/health.svg)](https://phpackages.com/packages/philiprehberger-laravel-operation-result)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[wearepixel/laravel-cart

A cart implementation for Laravel

1355.6k](/packages/wearepixel-laravel-cart)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
