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. philiprehberger/laravel-operation-result

ActiveLibrary

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

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

v1.0.4(1mo ago)116[1 PRs](https://github.com/philiprehberger/laravel-operation-result/pulls)MITPHPPHP ^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 1mo ago

READMEChangelogDependencies (8)Versions (6)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)[![License](https://camo.githubusercontent.com/2bdbf82b34db3c091922efafcc5e6bc8ca2158fcb08fb6a1f9808c98706e8132/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f7068696c69707265686265726765722f6c61726176656c2d6f7065726174696f6e2d726573756c74)](LICENSE)

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->getErrorCode();     // ?string
$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());
}
```

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()`.

Development
-----------

[](#development)

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

License
-------

[](#license)

MIT

###  Health Score

41

—

FairBetter than 89% of packages

Maintenance89

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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 ~3 days

Total

5

Last Release

53d 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 (16 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

[barryvdh/laravel-ide-helper

Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.

14.9k123.0M687](/packages/barryvdh-laravel-ide-helper)[getsolaris/laravel-make-service

A MVCS pattern create a service command for Laravel 5+

81161.3k](/packages/getsolaris-laravel-make-service)[api-platform/laravel

API Platform support for Laravel

59126.4k6](/packages/api-platform-laravel)

PHPackages © 2026

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