Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel View Models Laravel Package

spatie/laravel-view-models

Move complex view-prep logic out of controllers into dedicated Laravel view model classes. Extend Spatie\ViewModels\ViewModel to transform data for views, expose computed properties, and keep templates clean and focused.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require spatie/laravel-view-models
    

    Publish the config (optional):

    php artisan vendor:publish --provider="Spatie\ViewModels\ViewModelsServiceProvider"
    
  2. First Use Case: Create a view model class (e.g., app/ViewModels/PostViewModel.php):

    namespace App\ViewModels;
    
    use Spatie\ViewModels\ViewModel;
    
    class PostViewModel extends ViewModel
    {
        public function __construct(
            public string $title,
            public string $content,
            public int $commentsCount
        ) {}
    }
    
  3. Usage in Controller:

    use App\ViewModels\PostViewModel;
    
    public function show(Post $post)
    {
        return view('posts.show', [
            'viewModel' => PostViewModel::createFrom($post)
                ->withCommentsCount($post->comments()->count())
        ]);
    }
    
  4. View Access:

    <h1>{{ $viewModel->title }}</h1>
    <p>{{ $viewModel->content }}</p>
    <span>Comments: {{ $viewModel->commentsCount }}</span>
    

Where to Look First


Implementation Patterns

Core Workflow

  1. Model-to-ViewModel Transformation: Use createFrom() to instantiate a view model from a model instance, then chain methods to enrich data:

    PostViewModel::createFrom($post)
        ->withCommentsCount($post->comments()->count())
        ->withAuthor($post->author->name)
        ->withTags($post->tags->pluck('name'))
    
  2. Dynamic Properties: Add computed properties via with() or withDynamic():

    public function withDynamic(Post $post)
    {
        return $this->with([
            'isPopular' => $post->views > 1000,
            'formattedDate' => $post->created_at->format('M d, Y'),
        ]);
    }
    
  3. View Model Factories: For complex logic, use a factory class (e.g., PostViewModelFactory):

    public function create(Post $post)
    {
        return new PostViewModel(
            $post->title,
            $post->content,
            $post->comments()->count(),
            $post->author->name,
        );
    }
    
  4. Integration with Controllers:

    • Resource Controllers: Use traits like Spatie\ViewModels\Traits\ResourceController for DRY code.
    • API Responses: Return view models as JSON responses:
      return response()->json($viewModel);
      
  5. Caching: Cache view models in controllers or factories:

    $viewModel = Cache::remember("post.{$post->id}.viewmodel", now()->addHours(1), function () use ($post) {
        return PostViewModel::createFrom($post)->withCommentsCount($post->comments()->count());
    });
    

Advanced Patterns

  • Nested View Models:
    class PostViewModel extends ViewModel
    {
        public function __construct(
            public AuthorViewModel $author,
            public CommentViewModel $latestComment
        ) {}
    }
    
  • View Model Collections:
    $posts = Post::all();
    $viewModels = PostViewModel::collection($posts)
        ->each(fn ($vm) => $vm->withCommentsCount($vm->post->comments()->count()));
    
  • Conditional Data:
    public function withDynamic(Post $post)
    {
        return $this->with([
            'excerpt' => $post->isPublished ? Str::limit($post->content, 200) : null,
        ]);
    }
    

Gotchas and Tips

Common Pitfalls

  1. Over-Fetching Data:

    • Issue: Eager-loading too much data in view models can bloat queries.
    • Fix: Use with() sparingly and lazy-load relationships where possible:
      $viewModel->withCommentsCount($post->comments()->count()); // Eager-load
      $viewModel->withAuthor($post->author); // Load on demand in view model
      
  2. Circular Dependencies:

    • Issue: View models referencing each other (e.g., PostViewModelAuthorViewModelPostViewModel).
    • Fix: Break cycles by flattening data or using IDs:
      class AuthorViewModel extends ViewModel
      {
          public function __construct(
              public string $name,
              public ?int $postId = null // Store ID instead of model
          ) {}
      }
      
  3. Immutable Properties:

    • Issue: View models are immutable by design; modifying properties after creation fails.
    • Fix: Use with() or withDynamic() to add new properties:
      // ❌ Fails: $viewModel->newProperty = 'value';
      // ✅ Works: $viewModel->with(['newProperty' => 'value']);
      
  4. Performance with Large Collections:

    • Issue: Processing large collections of view models can be slow.
    • Fix: Use each() with batch processing or chunking:
      PostViewModel::collection($posts)
          ->each(fn ($vm) => $vm->withCommentsCount($vm->post->comments()->count()))
          ->chunk(100); // Process in batches
      
  5. Testing Quirks:

    • Issue: View models can make unit tests harder to mock.
    • Fix: Mock the factory or use partial view models in tests:
      $viewModel = Mockery::mock(PostViewModel::class)
          ->makePartial()
          ->shouldAllowMockingProtectedMethods();
      

Debugging Tips

  • Log View Model Data: Use dd() or dump() to inspect view model contents:
    dd(PostViewModel::createFrom($post)->withCommentsCount($post->comments()->count()));
    
  • Check for Missing Properties: Laravel will throw UndefinedProperty errors if a property isn’t defined. Use isset() in views:
    @if (isset($viewModel->optionalProperty))
        {{ $viewModel->optionalProperty }}
    @endif
    
  • Verify Factory Calls: Ensure factories are called correctly in controllers:
    // ❌ Wrong: PostViewModel::create($post); // Missing 'from' if using model
    // ✅ Correct: PostViewModel::createFrom($post);
    

Extension Points

  1. Custom View Model Macros: Add reusable methods to the base ViewModel class:

    use Spatie\ViewModels\ViewModel;
    
    ViewModel::macro('withMeta', function (array $meta) {
        return $this->with($meta);
    });
    

    Usage:

    PostViewModel::createFrom($post)->withMeta(['key' => 'value']);
    
  2. View Model Events: Listen for view model creation/modification:

    ViewModel::created(function (ViewModel $viewModel) {
        Log::info("ViewModel created: " . get_class($viewModel));
    });
    
  3. Custom Validation: Validate view model data before passing to views:

    public function withDynamic(Post $post)
    {
        $this->validate([
            'title' => 'required|max:255',
            'content' => 'required',
        ]);
    
        return $this->with([
            'isValid' => true,
        ]);
    }
    
  4. View Model Caching Strategies: Override the default caching behavior in config/view-models.php:

    'cache' => [
        'driver' => 'redis',
        'prefix' => 'view_models',
    ],
    
  5. Integration with Form Requests: Use view models to validate and transform form data:

    public function rules()
    {
        return [
            'title' => 'required|max:255',
            'content' => 'required',
        ];
    }
    
    public function withViewModel()
    {
        return PostViewModel::create(
            $this->validated('title'),
            $this->validated('content'),
            0 // Default comments count
        );
    }
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport