Matthew Hodge
Full Stack Developer

Using Scoped Relationships in Laravel Eloquent

When working with Laravel Eloquent, you’ll often need to filter or extend your relationships to suit your app’s needs. Scoped relationships let you add custom constraints to your relationships, making your models more expressive and your queries more reusable.

Let’s walk through some concrete examples to help you understand and use scoped relationships in your own projects!

What is a Scoped Relationship?

A scoped relationship is simply a relationship method on your model that applies extra query constraints. For example, you might want to easily fetch only the “featured” posts for a user, not just all posts.

Example: User and Posts

Let’s say you have a User model and a Post model. A user can have many posts:

// app/Models/User.php

public function posts(): HasMany
{
    return $this->hasMany(Post::class)->latest();
}

Now, let’s say you want a quick way to get just the featured posts for a user. You can add a new relationship method that scopes the query:

public function featuredPosts(): HasMany
{
    return $this->posts()->where('featured', true);
}

Now you can do:

$featuredPosts = $user->featuredPosts;

Creating Models via Scoped Relationships

But what if you want to create a new featured post using this relationship? By default, the featured attribute won’t be set automatically:

$post = $user->featuredPosts()->create(['title' => 'My Post']);
// $post->featured is NOT true

Introducing withAttributes

Laravel has a withAttributes function which lets you specify default attributes for models created via the relationship:

public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true]);
}

Now, when you create a post via this relationship:

$post = $user->featuredPosts()->create(['title' => 'Featured Post']);
// $post->featured is TRUE!

Customizing Query vs. Creation

By default, withAttributes also adds a where clause to the query. If you only want the default attribute on creation, not as a query constraint, pass asConditions: false:

public function featuredPosts(): HasMany
{
    return $this->posts()->withAttributes(['featured' => true], asConditions: false);
}

When Should You Use Scoped Relationships?

  • When you want to DRY up your code and avoid repeating common query constraints.
  • When you want to make your model API more expressive (e.g., $user->publishedPosts, $user->archivedPosts).
  • When you want to provide sensible defaults for related model creation.

Real-World Use Case

Suppose you’re building a blog platform. You want to let users quickly fetch or create “draft” or “published” posts:

public function draftPosts(): HasMany
{
    return $this->posts()->withAttributes(['status' => 'draft']);
}

public function publishedPosts(): HasMany
{
    return $this->posts()->withAttributes(['status' => 'published']);
}

Now you can easily fetch or create posts in either state:

$draft = $user->draftPosts()->create(['title' => 'My Draft']);
$published = $user->publishedPosts()->create(['title' => 'My Article']);

Conclusion

Scoped relationships are a powerful way to make your Eloquent models more expressive and your code more maintainable. Try adding them to your own models to simplify your queries and model creation!


Laravel's tap helper is a powerful utility that allows you to work with a value and return it without breaking the chain of operations. Let's explore how it works and how to use it effectively.

The TAP Function

At its core, the tap helper is a simple yet powerful function:

if (! function_exists('tap')) {
    /**
     * Call the given Closure with the given value then return the value.
     *
     * @template TValue
     *
     * @param  TValue  $value
     * @param  (callable(TValue): mixed)|null  $callback
     * @return ($callback is null ? \Illuminate\Support\HigherOrderTapProxy : TValue)
     */
    function tap($value, $callback = null)
    {
        if (is_null($callback)) {
            return new HigherOrderTapProxy($value);
        }

        $callback($value);

        return $value;
    }
}

The function does two key things:

  1. If no callback is provided, it returns a HigherOrderTapProxy instance
  2. If a callback is provided, it executes the callback with the value and returns the original value

Basic Usage

The tap helper allows you to "tap" into a chain of methods to perform operations on an object while still returning the original value.

$user = tap(User::first(), function($user) {
    $user->update(['last_login' => now()]);
});

Why Use TAP?

Without tap, you might write:

$user = User::first();
$user->update(['last_login' => now()]);
return $user;

With tap, it's more concise:

return tap(User::first(), function($user) {
    $user->update(['last_login' => now()]);
});

Real-World Examples

Example 1: File Operations

use Illuminate\Support\Facades\Storage;

$path = tap(Storage::put('path/to/file.txt', 'contents'), function() {
    Cache::forget('file-cache');
    Log::info('File was updated');
});
$post = tap(Post::create([
    'title' => 'My Post',
    'content' => 'Post content'
]), function($post) {
    $post->tags()->attach([1, 2, 3]);
    $post->user->notify(new PostCreated($post));
});

Example 3: Collection Manipulation

$processedUsers = tap(User::all(), function($users) {
    $users->each->notify(new WelcomeNotification);
    Cache::put('users', $users);
});

Using TAP with Arrow Functions

PHP 7.4+ allows for more concise syntax:

$user = tap(User::first(), fn($user) => $user->update(['last_login' => now()]));

The tap() Method on Collections

Laravel collections include a tap method:

collect([1, 2, 3])
    ->tap(function($collection) {
        Log::info('Count: ' . $collection->count());
    })
    ->filter(fn($value) => $value > 1)
    ->tap(function($collection) {
        Log::info('Filtered Count: ' . $collection->count());
    });

// Count: 3  
// Filtered Count: 2  

Testing with TAP

TAP is particularly useful in tests:

public function test_user_creation()
{
    $user = tap(User::factory()->create(), function($user) {
        $this->assertDatabaseHas('users', [
            'id' => $user->id,
            'email' => $user->email
        ]);
    });

    $this->actingAs($user);
}

Common Use Cases

  1. Logging Operations:
$response = tap($service->process(), function($result) {
    Log::info('Process completed', ['result' => $result]);
});
  1. Cache Operations:
$posts = tap(Post::all(), function($posts) {
    Cache::put('all_posts', $posts, now()->addDay());
});
  1. File Handling:
$file = tap(new UploadedFile(), function($file) {
    Storage::disk('public')->put('path/to/file', $file);
    event(new FileUploaded($file));
});

Best Practices

  1. Use tap when you need to perform side effects without breaking the chain
  2. Consider tap for cleaner testing code
  3. Combine with arrow functions for more concise syntax
  4. Use collection's tap method when working with collections

Conclusion

The tap helper is a powerful tool for writing cleaner, more maintainable code in Laravel. It's particularly useful for performing side effects while maintaining a fluent interface in your code.

For more information, check out the Laravel documentation.


I have recently been doing some updates to a website I created and had the need to alter the diffForHumans output that carbon provides to a laravel models created at time. Found this pretty interesting and thought I would share.

The system is a listing portal where you are either wanting to buy something other people have listed or alternatively you can list an item you would like to sell.

Difference for humans
Listing::first()->created_at
// => Illuminate\Support\Carbon @1667281103 {#4822
//      date: 2022-11-01 05:38:23.0 UTC (+00:00),
//    }

Listing::first()->created_at->diffForHumans();
// => "10 minutes ago"

Listing::first()->created_at->diffForHumans([
    'parts' => 1
]);
// => "10 minutes ago"

>>> Listing::first()->created_at->diffForHumans([
    'parts' => 2
]);
// => "10 minutes 36 seconds ago"

Listing::first()->created_at->diffForHumans([
    'parts' => 2,
    'join' => ' and '
]);
// => "10 minutes and 36 seconds ago"

I hope you find the above as usefull as I have. To find out more head over to nesbot carbon difference for humans and see how you can use it in your project.