The essential news about content management systems and mobile technology. Powered by Perfect Publisher and XT Search for Algolia.
The News Site publishes posts to the following channels: Facebook, Instagram, Twitter, Telegram, Web Push, Tumblr, and Blogger.
When building your Laravel applications, you'll likely have to
write queries that have constraints which are used in multiple
places throughout your application. Maybe you're building a
multi-tenant application and you're having to keep adding a
where
constraint to your queries to filter by the
user's team. Or, maybe you're building a blog and you're having to
keep adding a where
constraint to your queries to
filter by whether the blog post is published or not.
In Laravel, we can make use of query scopes to help us keep these constraints tidy and reusable in a single place.
In this article, we're going to take a look at local query scopes and global query scopes. We'll learn about the difference between the two, how to create your own, and how to write tests for them.
By the end of the article, you should feel confident using query scopes in your Laravel applications.
Query scopes allow you to define constraints in your Eloquent
queries in a reusable way. They are typically defined as methods on
your Laravel models, or as a class that implements the
Illuminate\Database\Eloquent\Scope
interface.
Not only are they great for defining reusable logic in a single place, but they can also make your code more readable by hiding complex query constraints behind a simple method call.
Query scopes come in two different types:
If you've ever used Laravel's built-in "soft delete"
functionality, you may have already used query scopes without
realising it. Laravel makes use of local query scopes to provide
you with methods such as withTrashed
and
onlyTrashed
on your models. It also uses a global
query scope to automatically add a
whereNull('deleted_at')
constraint to all queries on
the model so that soft-deleted records aren't returned in queries
by default.
Let's take a look at how we can create and use local query scopes and global query scopes in our Laravel applications.
Local query scopes are defined as methods on your Eloquent model and allow you to define constraints that can be manually applied to your model queries.
Let's imagine we are building a blogging application that has an admin panel. In the admin panel, we have two pages: one for listing published blog posts and another for listing unpublished blog posts.
We'll imagine the blog posts are accessed using an
\App\Models\Article
model and that the database table
has a nullable published_at
column that stores the
date and time the blog post is to be published. If the
published_at
column is in the past, the blog post is
considered published. If the published_at
column is in
the future or null
, the blog post is considered
unpublished.
To get the published blog posts, we could write a query like this:
use App\Models\Article;
$publishedPosts = Article::query()
->where('published_at', '<=', now())
->get();
To get the unpublished blog posts, we could write a query like this:
use App\Models\Article;
use Illuminate\Contracts\Database\Eloquent\Builder;
$unpublishedPosts = Article::query()
->where(function (Builder $query): void {
$query->whereNull('published_at')
->orWhere('published_at', '>', now());
})
->get();
The queries above aren't particularly complex. However, let's
imagine we are using them in multiple places throughout our
application. As the number of occurrences grows, it becomes more
likely that we'll make a mistake or forget to update the query in
one place. For instance, a developer might accidentally use
>=
instead of <=
when querying for
published blog posts. Or, the logic for determining if a blog post
is published might change, and we'll need to update all the
queries.
This is where query scopes can be extremely useful. So let's
tidy up our queries by creating local query scopes on the
\App\Models\Article
model.
Local query scopes are defined by creating a method that starts
with the word scope
and ends with the intended name of
the scope. For example, a method called scopePublished
will create a published
scope on the model. The method
should accept an
Illuminate\Contracts\Database\Eloquent\Builder
instance and return an
Illuminate\Contracts\Database\Eloquent\Builder
instance.
We'll add both of the scopes to the
\App\Models\Article
model:
declare(strict_types=1);
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class Article extends Model
{
public function scopePublished(Builder $query): Builder
{
return $query->where('published_at', '<=', now());
}
public function scopeNotPublished(Builder $query): Builder
{
return $query->where(function (Builder $query): Builder {
return $query->whereNull('published_at')
->orWhere('published_at', '>', now());
});
}
// ...
}
As we can see in the example above, we've moved our
where
constraints from our previous queries into two
separate methods: scopePublished
and
scopeNotPublished
. We can now use these scopes in our
queries like this:
use App\Models\Article;
$publishedPosts = Article::query()
->published()
->get();
$unpublishedPosts = Article::query()
->notPublished()
->get();
In my personal opinion, I find these queries much easier to read and understand. It also means that if we need to write any queries in the future with the same constraint, we can reuse these scopes.
Global query scopes perform a similar function to local query scopes. But rather than manually being applied on a query-by-query basis, they're automatically applied to all queries on the model.
As we mentioned earlier, Laravel's built-in "soft delete"
functionality makes use of the
Illuminate\Database\Eloquent\SoftDeletingScope
global
query scope. This scope automatically adds a
whereNull('deleted_at')
constraint to all queries on
the model. You can check out the source code on GitHub here if you're interested
in seeing how it works under the hood.
For example, imagine you're building a multi-tenant blogging application that has an admin panel. You'd only want to allow users to view articles that belonged to their team. So, you might write a query like this:
use App\Models\Article;
$articles = Article::query()
->where('team_id', Auth::user()->team_id)
->get();
This query is fine, but it's easy to forget to add the
where
constraint. If you were writing another query
and forgot to add the constraint, you'd end up with a bug in your
application that would allow users to interact with articles that
didn't belong to their team. Of course, we don't want that to
happen!
To prevent this, we can create a global scope that we can apply
automatically to all our App\Model\Article
model
queries.
Let's create a global query scope that filters all queries by
the team_id
column.
Please note, that we're keeping the example simple for the purposes of this article. In a real-world application, you'd likely want to use a more robust approach that handles things like the user not being authenticated, or the user belonging to multiple teams. But for now, let's keep it simple so we can focus on the concept of global query scopes.
We'll start by running the following Artisan command in our terminal:
php artisan make:scope TeamScope
This should have created a new
app/Models/Scopes/TeamScope.php
file. We'll make some
updates to this file and then look at the finished code:
declare(strict_types=1);
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Facades\Auth;
final readonly class TeamScope implements Scope
{
/**
* Apply the scope to a given Eloquent query builder.
*/
public function apply(Builder $builder, Model $model): void
{
$builder->where('team_id', Auth::user()->team_id);
}
}
In the code example above, we can see that we've got a new class
that implements the Illuminate\Database\Eloquent\Scope
interface and has a single method called apply
. This
is the method where we define the constraints we want to apply to
the queries on the model.
Our global scope is now ready to be used. We can add it to any models where we want to scope the queries down to the user's team.
Let's apply it to the \App\Models\Article
model.
There are several ways to apply a global scope to a model. The
first way is to use the
Illuminate\Database\Eloquent\Attributes\ScopedBy
attribute on the model:
declare(strict_types=1);
namespace App\Models;
use App\Models\Scopes\TeamScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Model;
#[ScopedBy(TeamScope::class)]
final class Article extends Model
{
// ...
}
Another way is to use the addGlobalScope
method in
the booted
method of the model:
declare(strict_types=1);
namespace App\Models;
use App\Models\Scopes\TeamScope;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
final class Article extends Model
{
use HasFactory;
protected static function booted(): void
{
static::addGlobalScope(new TeamScope());
}
// ...
}
Both of these approaches will apply the where('team_id',
Auth::user()->team_id)
constraint to all queries on the
\App\Models\Article
model.
This means you can now write queries without having to worry
about filtering by the team_id
column:
use App\Models\Article;
$articles = Article::query()->get();
If we assume the user is part of a team with the
team_id
of 1
, the following SQL would be
generated for the query above:
select * from `articles` where `team_id` = 1
That's pretty cool, right!?
Another way to define and apply a global query scope is to use an anonymous global scope.
Let's update our \App\Models\Article
model to use
an anonymous global scope:
declare(strict_types=1);
namespace App\Models;
use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
final class Article extends Model
{
protected static function booted(): void
{
static::addGlobalScope('team_scope', static function (Builder $builder): void {
$builder->where('team_id', Auth::user()->team_id);
});
}
// ...
}
In the code example above, we've used the
addGlobalScope
method to define an anonymous global
scope in the model's booted
method. The
addGlobalScope
method takes two arguments:
Just like the other approaches, this will apply the
where('team_id', Auth::user()->team_id)
constraint
to all queries on the \App\Models\Article
model.
In my experience, anonymous global scopes are less common than defining a global scope in a separate class. But it's good to know they're available to use if you need them.
There may be times when you want to write a query that doesn't use a global query scope that's been applied to a model. For example, you might be building a report or analytics query that needs to include all records, regardless of the global query scopes.
If this is the case, you can use one of two methods to ignore global scopes.
The first method is withoutGlobalScopes
. This
method allows you to ignore all global scopes on the model if no
arguments are passed to it:
use App\Models\Article;
$articles = Article::query()->withoutGlobalScopes()->get();
Or, if you'd prefer to only ignore a given set of global scopes,
you can the scope names to the withoutGlobalScopes
method:
use App\Models\Article;
use App\Models\Scopes\TeamScope;
$articles = Article::query()
->withoutGlobalScopes([
TeamScope::class,
'another_scope',
])->get();
In the example above, we're ignoring the
App\Models\Scopes\TeamScope
and another imaginary
anonymous global scope called another_scope
.
Alternatively, if you'd prefer to ignore a single global scope,
you can use the withoutGlobalScope
method:
use App\Models\Article;
use App\Models\Scopes\TeamScope;
$articles = Article::query()->withoutGlobalScope(TeamScope::class)->get();
It's important to remember that global query scopes are only
applied to queries made through your models. If you're writing a
database query using the Illuminate\Support\Facades\DB
facade, the global query scopes won't be applied.
For example, let's say you write this query that you'd expect would only grab the articles belonging to the logged-in user's team:
use Illuminate\Support\Facades\DB;
$articles = DB::table('articles')->get();
In the query above, the App\Models\Scopes\TeamScope
global query scope won't be applied even if the scope is defined on
the App\Models\Article
model. So, you'll need to make
sure you're manually applying the constraint in your database
queries.
Now that we've learned about how to create and use query scopes, we'll take a look at how we can write tests for them.
There are several ways to test query scopes, and the method you choose may depend on your personal preference or the contents of the scope you're writing. For instance, you may want to write more unit-style tests for the scopes. Or, you may want to write more integration-style tests that test the scope in the context of being used in something like a controller.
Personally, I like to use a mixture of the two so that I can have confidence the scopes are adding the correct constraints, and that the scopes are actually being used in the queries.
Let's take our example published
and
notPublished
scopes from earlier and write some tests
for them. We'll want to write two different tests (one for each
scope):
published
scope only
returns articles that have been published.notPublished
scope only
returns articles that haven't been published.Let's take a look at the tests and then discuss what's being done:
declare(strict_types=1);
namespace Tests\Feature\Models\Article;
use App\Models\Article;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
final class ScopesTest extends TestCase
{
use LazilyRefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Create two published articles.
$this->publishedArticles = Article::factory()
->count(2)
->create([
'published_at' => now()->subDay(),
]);
// Create an unpublished article that hasn't
// been scheduled to publish.
$this->unscheduledArticle = Article::factory()
->create([
'published_at' => null,
]);
// Create an unpublished article that has been
// scheduled to publish.
$this->scheduledArticle = Article::factory()
->create([
'published_at' => now()->addDay(),
]);
}
#[Test]
public function only_published_articles_are_returned(): void
{
$articles = Article::query()->published()->get();
$this->assertCount(2, $articles);
$this->assertTrue($articles->contains($this->publishedArticles->first()));
$this->assertTrue($articles->contains($this->publishedArticles->last()));
}
#[Test]
public function only_not_published_articles_are_returned(): void
{
$articles = Article::query()->notPublished()->get();
$this->assertCount(2, $articles);
$this->assertTrue($articles->contains($this->unscheduledArticle));
$this->assertTrue($articles->contains($this->scheduledArticle));
}
}
We can see in the test file above, we're first creating some
data in the setUp
method. We're creating two published
articles, one unscheduled article, and one scheduled article.
There is then a test
(only_published_articles_are_returned
) that checks the
published
scope only returns the published articles.
And there is another test
(only_not_published_articles_are_returned
) that checks
the notPublished
scope only returns the articles that
haven't been published.
By doing this, we can now have confidence that our query scopes are applying the constraints as expected.
As we mentioned, another way of testing query scopes is to test
them in the context of being used in a controller. Whereas an
isolated test for the scope can help to assert that a scope is
adding the correct constraints to a query, it doesn't actually test
that the scope is being used as intended in the application. For
instance, you may have forgotten to add the published
scope to a query in a controller method.
These types of mistakes can be caught by writing tests that assert the correct data is returned when the scope is used in a controller method.
Let's take our example of having a multi-tenant blogging application and write a test for a controller method that lists articles. We'll assume we have a very simple controller method like so:
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
final class ArticleController extends Controller
{
public function index()
{
return view('articles.index', [
'articles' => Article::all(),
]);
}
}
We'll assume that the App\Models\Article
model has
our App\Models\Scopes\TeamScope
applied to it.
We'll want to assert that only the articles belonging to the user's team are returned. The test case may look something like this:
declare(strict_types=1);
namespace Tests\Feature\Controllers\ArticleController;
use App\Models\Article;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
final class IndexTest extends TestCase
{
use LazilyRefreshDatabase;
#[Test]
public function only_articles_belonging_to_the_team_are_returned(): void
{
// Create two new teams.
$teamOne = Team::factory()->create();
$teamTwo = Team::factory()->create();
// Create a user that belongs to team one.
$user = User::factory()->for($teamOne)->create();
// Create 3 articles for team one.
$articlesForTeamOne = Article::factory()
->for($teamOne)
->count(3)
->create();
// Create 2 articles for team two.
Article::factory()
->for($teamTwo)
->count(2)
->create();
// Act as the user and make a request to the controller method. We'll
// assert that only the articles belonging to team one are returned.
$this->actingAs($user)
->get('/articles')
->assertOk()
->assertViewIs('articles.index')
->assertViewHas(
key: 'articles',
value: fn (Collection $articles): bool => $articles->pluck('id')->all()
=== $articlesForTeamOne->pluck('id')->all()
);
}
}
In the test above, we're creating two teams. We're then creating a user that belongs to team one. We're creating 3 articles for team one and 2 articles for team two. We're then acting as the user and making a request to the controller method that lists the articles. The controller method should only be returning the 3 articles that belong to team one, so we're asserting that only those articles are returned by comparing the IDs of the articles.
This means we can then have confidence that the global query scope is being used as intended in the controller method.
In this article, we learned about local query scopes and global query scopes. We learned about the difference between the two, how to create your own and use them, and how to write tests for them.
Hopefully, you should now feel confident using query scopes in your Laravel applications.
The post Learn to master Query Scopes in Laravel appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.
Read more https://laravel-news.com/query-scopes
The first release of PHP 8.4 is now available and ready for testing! PHP 8.4 is scheduled to be officially released on November 21, 2024. Before the release, it will feature six months of pre-release phases, going from Alphas to Betas to Release Candidates to the official release.
NOTE: Please DO NOT use this version in production. It is an early test version.
To get started testing this new Alpha, you can download the source, or if you are a Laravel Herd user, v1.9.1 includes the ability to install it directly from the PHP tab.
The PHP team is asking everyone to please carefully test this version and report any issues using the PHP bug tracking system. The next release will be Alpha 2, planned for July, 18 2024.
The post PHP 8.4 Alpha 1 is now out! appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.
Read more https://laravel-news.com/php-84-alpha-1
The Laravel team released v11.15 this week, which includes
improvements to the make:mail
command, support for
setting mime types on attachments with Resend, database migration
updates, and more.
Caleb White contributed integrating relation generics into the Laravel framework:
Generics provide better auto-completion and intellisense in the ide without having to rely on Larastan to add generics to the classes through the use of stubs. Having generics in the framework also makes it easier for third party packages to define the inner types on their custom relations.
Caleb has been contributing to Larastan and is now integrating this into the framework, improving static analysis in Laravel! See Pull Request #51851 for more details.
make:mail
Christoph Rumpel contributed an update to the
make:mail
command that prompts the user for the type
of view they'd like to create:
<script async src="https://laravel-news.com//www.instagram.com/embed.js"></script>View this post on Instagram
Router
TappableMuhammed Sari added the Tappable
trait to the Router
class, allowing you do write
something like the following:
class RouteRegistrar
{
public function __invoke(Router $router)
{
$router->post('redacted', WebhookController::class)
->name('redacted');
}
}
$router
->tap(new Redacted1Webhooks\RouteRegistrar())
->tap(new Redacted2Webhooks\RouteRegistrar())
// ...
;
// In tests...
protected function defineRoutes($router)
{
$router->tap(new \RedactedWebhooks\RouteRegistrar());
}
Hafez Divandari contributed updates to database migrations around SQLite and other quality-of-life improvements. In summary, Pull Request #51373 introduces the following updates:
Jayan Ratna contributed support for setting mime types
on attachments inside a Resend mailable class. This PR adds the
withMime()
method, which is demonstrated in the pull
request as follows:
public function attachments(): array
{
return [
Attachment::fromPath('/path/to/file')
->as('name.pdf')
->withMime('application/pdf'),
];
}
You can see the complete list of new features and updates below and the diff between 11.14.0 and 11.15.0 on GitHub. The following release notes are directly from the changelog:
HttpResponseException
by @hafezdivandari in https://github.com/laravel/framework/pull/51986testMultiplyIsLazy
to ensure
LazyCollection's multiply
method's lazy behaviour by
@lmottasin in https://github.com/laravel/framework/pull/52020MultipleInstanceManager
to have
studly creators by @cosmastech in
https://github.com/laravel/framework/pull/52030$config
property to
MultipleInstanceManager
by @cosmastech in
https://github.com/laravel/framework/pull/52028Router
Tappable
by
@mabdullahsari in https://github.com/laravel/framework/pull/52051block
method
for LockTimeoutException
by @siarheipashkevich in https://github.com/laravel/framework/pull/52063The post Generics Added to Eloquent Builder in Laravel 11.15 appeared first on Laravel News.
Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.
Read more https://laravel-news.com/laravel-11-15-0
When Automattic launched the .blog domain extension back in 2016, our in-house team built a shiny new website to go with it. The vibe of my.blog very much matched the era, featuring a simple layout, a generic color palette, and a “just the basics” approach to the content and overall messaging.
Eight years later, it was time for a refresh.
Our special projects team recently revisited my.blog, giving it a totally new aesthetic and approach, as well as some cool new features. In this post, we’re going to give you a peek behind the curtain about how this new site came to be. The fresh design beautifully showcases what’s possible with WordPress.
Take a look around my.blog and don’t forget to grab a .blog domain while you’re at it!
The first step for our team was to nail down a design motif. For any website project, starting here sets the tone for the entire site. What are the colors, styling, and typographical elements that speak to what you’re trying to get across?
Our design team presented two beautiful options, each with their pros and cons:
The fluid layout of the lighter version, codenamed Lemon Softness, really stood out, whereas the tile or card layout of the darker version, codenamed Starfield, looked nice, but felt like more of the same on today’s web.
On the other hand, the dark aesthetic of Starfield spoke a little more to our future-facing sensibilities.
Ultimately, we combined them. The layout of Lemon Softness is what you’ll see on my.blog today (including a few very cool scrolling animations), while the Starfield inspiration shows up in Dark Mode toggle (the moon/sun icon on the top right). Also check out the “glass morphism” effect in the header at the top of the site, which blurs the imagery just a bit so that you can clearly read and navigate the menu.
Of course, it’s not all about the design. Along with the bold new look, our team included a few features that will inspire bloggers of all stripes and encourage getting back into the habit. After all, blogging is actually thriving in our social media world.
First, we wanted to ensure that “dotbloggers” (folks who use a .blog domain) were highlighted right on the front page. This carousel pulls posts from the “dotblogger” blog category:
Just a bit further down on the page, we decided to feature .blog sites; this carousel is actually populated dynamically from a .blog domain database.
Finally, the team wanted to emphasize how powerful blogging can be. Just as “the death of the novel” has been overblown for at least 100 years, “the death of blogging” is a regular fixture of online discourse. The reality is that more people than ever before are blogging and seeing the value of having a corner of the internet that’s all theirs. To this end, we spent time crafting a manifesto that speaks to this:
We hold this truth at the center of our mission and vision: content is valuable. When you publish online—whatever you publish—you are declaring your willingness to create and add something to our world.
Owning your domain means that you’re not at the whims of an ever-changing algorithm. It’s a claim of ownership and independence. A .blog domain is more than that—it’s a signal and a badge of authenticity that declares your particular corner of the internet as a space for stories, expressions, thoughts, and ideas. You’re an original.
A .blog domain is more than just the dynamic pillar of your online presence (though it is that too). It’s a stage for all of the utterly unique ways you share your story—in words, photographs, videos, podcasts, artwork, and more.
Beyond the glitz and glamor, the .blog website also needs to provide practical resources for both registrars and dotbloggers. On the registrar side, our team made it especially convenient to access promotional content and visual marketing assets. When it comes to bloggers looking for a domain, the search page provides real-time results for what’s available and where you can purchase it.
As a reminder, you can also buy a .blog domain directly from WordPress.com/domains—for just $2.20 for the first year, or for free if you purchase any annual paid plan.
No need to take our word on the great job our special projects team did with the new my.blog website. Head over, click around, grab your own memorable domain name, and start (or restart) blogging today.
Read more https://wordpress.com/blog/2024/07/09/my-dot-blog/