PHPackages                             adrosoftware/laravel-data-proxy - 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. [API Development](/categories/api)
4. /
5. adrosoftware/laravel-data-proxy

ActiveLibrary[API Development](/categories/api)

adrosoftware/laravel-data-proxy
===============================

A GraphQL-like declarative data retrieval layer for Laravel with query batching and optimization

0.5.0(1mo ago)0760↓91.7%MITPHPPHP ^8.2CI passing

Since Feb 27Pushed 1mo agoCompare

[ Source](https://github.com/adrorocker/laravel-data-proxy)[ Packagist](https://packagist.org/packages/adrosoftware/laravel-data-proxy)[ RSS](/packages/adrosoftware-laravel-data-proxy/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (6)Dependencies (24)Versions (8)Used By (0)

Laravel Data Proxy
==================

[](#laravel-data-proxy)

A GraphQL-like declarative data retrieval layer for Laravel with automatic query batching and optimization.

Why DataProxy?
--------------

[](#why-dataproxy)

ProblemDataProxy SolutionN+1 queries across multiple data sourcesAutomatic query batching by modelVerbose, scattered query codeDeclarative requirements in one placeComplex eager loading setupGraphQL-like nested shapesNo visibility into query performanceBuilt-in metrics trackingRepetitive data fetching patternsReusable, composable data classesFeatures
--------

[](#features)

- **Declarative Requirements** - Define what data you need, not how to fetch it
- **Automatic Query Batching** - Same-model lookups are combined into single queries
- **Nested Relations** - GraphQL-like shapes with field selection and constraints
- **Schema Agnostic** - No schema assumptions; respects your Eloquent model configurations
- **Memory Efficient** - Lazy DataSet collections with chunking support
- **Built-in Caching** - Per-requirement caching with tags support
- **Presenter Support** - Integrate with any presenter package
- **Pagination** - First-class pagination handling
- **Metrics Tracking** - Monitor query counts, execution time, and memory usage

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

[](#requirements)

- PHP 8.2 or higher
- Laravel 11.x, 12.x, or 13.x

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

[](#installation)

```
composer require adrosoftware/laravel-data-proxy
```

The package auto-registers its service provider and facade with Laravel.

### Publish Configuration (Optional)

[](#publish-configuration-optional)

```
php artisan vendor:publish --tag=dataproxy-config
```

Quick Start
-----------

[](#quick-start)

```
use AdroSoftware\DataProxy\DataProxy;
use AdroSoftware\DataProxy\Requirements;
use AdroSoftware\DataProxy\Shape;
use App\Models\User;
use App\Models\Post;

// Define all your data requirements in one place
$result = DataProxy::make()->fetch(
    Requirements::make()
        // Fetch a single user with their profile and roles
        ->one('user', User::class, $userId,
            Shape::make()
                ->select('id', 'name', 'email')
                ->with('profile')
                ->with('roles', Shape::make()->select('id', 'name'))
        )
        // Fetch their recent published posts
        ->query('posts', Post::class,
            Shape::make()
                ->where('user_id', $userId)
                ->where('published', true)
                ->latest()
                ->limit(10)
                ->with('tags')
        )
        // Get aggregate counts
        ->count('totalPosts', Post::class,
            Shape::make()->where('user_id', $userId)
        )
        // Compute derived values
        ->compute('stats', fn($data) => [
            'posts' => $data['totalPosts'],
            'hasProfile' => $data['user']?->profile !== null,
        ], dependsOn: ['user', 'totalPosts'])
);

// Access your data
echo $result->user->name;
echo $result->user->profile->bio;

foreach ($result->posts as $post) {
    echo $post->title;
}

echo "Total posts: " . $result->totalPosts;
echo "Has profile: " . ($result->stats['hasProfile'] ? 'Yes' : 'No');

// Get performance metrics
$metrics = $result->metrics();
// ['queries' => 3, 'time_ms' => 12.5, 'memory_mb' => 2.1, 'batch_savings' => 2]
```

Core Concepts
-------------

[](#core-concepts)

### Requirements

[](#requirements-1)

The `Requirements` class defines what data you need:

```
Requirements::make()
    ->one('user', User::class, $id)           // Single entity by ID
    ->many('users', User::class, [1, 2, 3])   // Multiple entities by IDs
    ->query('posts', Post::class, $shape)     // Query with constraints
    ->paginate('posts', Post::class, 15, 1)   // Paginated query
    ->count('total', Post::class)             // Count aggregate
    ->sum('views', Post::class, 'view_count') // Sum aggregate
    ->avg('rating', Post::class, 'rating')    // Average aggregate
    ->min('oldest', Post::class, 'created_at') // Min aggregate
    ->max('newest', Post::class, 'created_at') // Max aggregate
    ->raw('custom', 'SELECT ...', $bindings)  // Raw SQL
    ->compute('derived', $callback, $deps)    // Computed value
    ->cache('user', 'user:1', ttl: 3600)      // Cache configuration
```

### Shape

[](#shape)

The `Shape` class defines the structure of data to retrieve:

```
Shape::make()
    // Field selection
    ->select('id', 'title', 'content')

    // Relations with nested shapes
    ->with('author')
    ->with('comments', Shape::make()
        ->select('id', 'post_id', 'body') // Include foreign key when selecting fields
        ->with('author')
        ->latest()
        ->limit(5)
    )

    // Constraints
    ->where('status', 'published')
    ->where('views', '>', 100)
    ->whereIn('category_id', [1, 2, 3])
    ->whereNull('deleted_at')
    ->whereHas('comments')

    // Ordering and pagination
    ->orderBy('created_at', 'desc')
    ->latest()  // Shorthand for orderBy('created_at', 'desc')
    ->limit(10)
    ->offset(20)

    // Custom query scopes (accumulate - multiple calls are supported)
    ->scope(fn($query) => $query->withCount('likes'))
    ->scope(fn($query, $resolved) => $query->whereIn('author_id', $resolved['followedIds']))

    // Output format
    ->asArray()  // Return arrays instead of models
    ->present(PostPresenter::class)  // Apply presenter
```

### Custom Query Scopes

[](#custom-query-scopes)

Apply custom query modifications using scopes. Multiple scopes accumulate and are applied in order:

```
Shape::make()
    // First scope - add aggregates
    ->scope(fn($query) => $query->withCount('likes'))

    // Second scope - add visibility constraints
    ->scope(fn($query, $resolved) => $query->whereIn('author_id', $resolved['followedIds']))

    // Conditional scope using when()
    ->when($excludeIds, fn($shape) => $shape->scope(
        fn($query) => $query->whereNotIn('id', $excludeIds)
    ))
```

Scopes receive `($query, $resolved)` parameters where `$resolved` contains all previously resolved requirements.

To inspect or clear scopes:

```
$shape->getScopes();    // Returns array of callables
$shape->clearScopes();  // Removes all scopes
```

### Result

[](#result)

The `Result` class provides access to resolved data:

```
// Multiple access patterns
$result->user;           // Magic getter
$result->get('user');    // Method access
$result['user'];         // Array access

// Check existence
$result->has('user');

// Get subsets
$result->all();
$result->only(['user', 'posts']);
$result->except(['metrics']);

// Transform values
$result->transform([
    'posts' => fn($posts) => $posts->take(5),
]);

// For API responses
return response()->json($result->toResponse());
// { "data": { "user": {...}, "posts": [...] } }

return response()->json($result->toResponse(includeMetrics: true));
// { "data": {...}, "meta": { "queries": 3, "time_ms": 12.5 } }
```

Using the Facade
----------------

[](#using-the-facade)

```
use AdroSoftware\DataProxy\Laravel\DataProxyFacade as Data;

$result = Data::fetch(
    Requirements::make()
        ->one('user', User::class, 1)
);

// Or with inline builder
$result = Data::query(function ($r) {
    $r->one('user', User::class, auth()->id())
      ->query('posts', Post::class, Shape::make()->limit(10));
});
```

Configuration Presets
---------------------

[](#configuration-presets)

```
// For API use (caching enabled, metrics enabled)
$result = DataProxy::forApi()->fetch($requirements);

// For large exports (no caching, larger chunks, metrics disabled)
$result = DataProxy::forExport()->fetch($requirements);

// For high performance (aggressive caching, metrics disabled)
$result = DataProxy::forPerformance()->fetch($requirements);

// Custom configuration
$result = DataProxy::make()
    ->configure([
        'cache.ttl' => 7200,
        'metrics.enabled' => false,
    ])
    ->fetch($requirements);
```

Creating Data Classes
---------------------

[](#creating-data-classes)

Organize your data requirements into reusable classes:

```
namespace App\Data;

use AdroSoftware\DataProxy\DataProxy;
use AdroSoftware\DataProxy\Requirements;
use AdroSoftware\DataProxy\Shape;
use AdroSoftware\DataProxy\Result;
use App\Models\User;
use App\Models\Post;

class DashboardData
{
    public static function fetch(int $userId): Result
    {
        return DataProxy::make()->fetch(
            Requirements::make()
                ->one('user', User::class, $userId, self::userShape())
                ->query('recentPosts', Post::class, self::recentPostsShape($userId))
                ->count('totalPosts', Post::class, Shape::make()->where('user_id', $userId))
                ->compute('stats', fn($d) => [
                    'posts' => $d['totalPosts'],
                    'hasRecentActivity' => $d['recentPosts']->isNotEmpty(),
                ], ['totalPosts', 'recentPosts'])
        );
    }

    protected static function userShape(): Shape
    {
        return Shape::make()
            ->select('id', 'name', 'email', 'avatar')
            ->with('profile')
            ->with('roles', Shape::make()->select('id', 'name'));
    }

    protected static function recentPostsShape(int $userId): Shape
    {
        return Shape::make()
            ->where('user_id', $userId)
            ->where('published', true)
            ->latest()
            ->limit(5)
            ->with('tags');
    }
}

// Usage
$data = DashboardData::fetch(auth()->id());
return view('dashboard', $data->all());
```

Working with DataSet
--------------------

[](#working-with-dataset)

Query results are returned as lazy `DataSet` collections:

```
$result->posts->each(fn($post) => echo $post->title);
$result->posts->map(fn($post) => $post->title);
$result->posts->filter(fn($post) => $post->views > 100);
$result->posts->pluck('title');
$result->posts->keyBy('id');
$result->posts->groupBy('category_id');
$result->posts->first();
$result->posts->count();
$result->posts->isEmpty();

// Memory-efficient chunking
$result->posts->chunk(100, function ($chunk) {
    // Process chunk
});

// Convert to other formats
$result->posts->all();      // Array
$result->posts->toArray();  // Nested array
$result->posts->collect();  // Laravel Collection
```

Pagination
----------

[](#pagination)

```
$result = DataProxy::make()->fetch(
    Requirements::make()
        ->paginate('posts', Post::class, perPage: 15, page: 1,
            shape: Shape::make()->where('published', true)->latest()
        )
);

foreach ($result->posts as $post) {
    echo $post->title;
}

echo "Page " . $result->posts->currentPage();
echo " of " . $result->posts->lastPage();
echo " - Total: " . $result->posts->total();

if ($result->posts->hasMorePages()) {
    // Show next page link
}
```

Caching
-------

[](#caching)

```
$result = DataProxy::make()->fetch(
    Requirements::make()
        ->query('categories', Category::class, Shape::make()->where('active', true))
        ->cache('categories', 'categories:active', ttl: 3600, tags: ['categories'])
);

// The categories query will be cached for 1 hour
// Invalidate with: Cache::tags(['categories'])->flush()
```

Presenter Integration
---------------------

[](#presenter-integration)

### Using a Closure Adapter

[](#using-a-closure-adapter)

```
use AdroSoftware\DataProxy\Adapters\ClosurePresenterAdapter;

$adapter = new ClosurePresenterAdapter();
$adapter->register(User::class, function ($user) {
    return new class($user) {
        public function __construct(private $user) {}
        public function __get($name) { return $this->user->{$name}; }
        public function fullName(): string {
            return $this->user->first_name . ' ' . $this->user->last_name;
        }
    };
});

$result = DataProxy::make()
    ->withPresenter($adapter)
    ->fetch($requirements);
```

### Using Laravel Model Presenter

[](#using-laravel-model-presenter)

```
use AdroSoftware\DataProxy\Adapters\LaravelModelPresenterAdapter;

$adapter = new LaravelModelPresenterAdapter(
    namespace: 'App\\Presenters\\',
    suffix: 'Presenter'
);

$result = DataProxy::make()
    ->withPresenter($adapter)
    ->fetch(
        Requirements::make()
            ->one('user', User::class, 1,
                Shape::make()->present(UserPresenter::class)
            )
    );

// Presenter methods available
echo $result->user->fullName();
```

Documentation
-------------

[](#documentation)

For detailed documentation, see the `/docs` directory:

- [Getting Started](docs/getting-started.md)
- [Usage Guide](docs/usage.md)
- [Use Cases](docs/use-cases.md)
- [Configuration](docs/configuration.md)
- [API Reference](docs/api-reference.md)

Testing
-------

[](#testing)

```
composer test
```

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance88

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity41

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 ~11 days

Total

6

Last Release

59d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/1ba004d502228eb272341845427581da17399ec8b4315ce16c99b298ec0a414f?d=identicon)[adrorocker](/maintainers/adrorocker)

---

Top Contributors

[![adrorocker](https://avatars.githubusercontent.com/u/1872940?v=4)](https://github.com/adrorocker "adrorocker (30 commits)")

---

Tags

laravelproxygraphqldataquerybatchoptimizationdto

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/adrosoftware-laravel-data-proxy/health.svg)

```
[![Health](https://phpackages.com/badges/adrosoftware-laravel-data-proxy/health.svg)](https://phpackages.com/packages/adrosoftware-laravel-data-proxy)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

59156.3k11](/packages/api-platform-laravel)[simplestats-io/laravel-client

Analytics for Laravel. Track visitors, registrations, and payments. Discover which channels actually drive revenue, not just traffic. Server-side, GDPR compliant, ad-blocker proof.

5019.3k](/packages/simplestats-io-laravel-client)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

245.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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