It started with some very basic components, but I managed to build some more complicated ones and even adopted Inertia's SPA philosophy. Honestly, I was a bit frightened to release it, but I'm glad I did! I released it as v0.1 as it was quite experimental, and I still needed to build real apps with it. After four or five months, I tagged v1.0 and worked on it for months. Some people liked it and started building apps with it, which is honorable. I positioned it somewhere between Inertia and Livewire, though it was always hard to compare it to those. Mind, this was before Livewire 3.
Maintaining a large package like Splade is also demanding. Feature requests, bug reports and questions come in all the time. Most can be fixed and addressed, but occasionally some request would pop up where I would think: This is would be easy in Inertia/Vue. The goal of Splade was to use Blade as much as possible, and use Vue where necessary. It shouldn't make things harder.
Two major things are bothering me with Splade's current architecture: customization and expandability. Of course, there are other concerns, but these two things keep me up at night.
Let's start with customization. People primarily want to customize the components that come with Splade, not the JavaScript part under the hood that manages SPA navigation, lazy loading, etc. There are many renderless components where Splade doesn't provide any template or markup, but it is about the components that do provide a template. Splade Components consist of a Blade Component and a Vue Component (with only a script, not a template). Essentially, we render the Blade part and pass it as a template to Vue. You see where this is going. To customize the template, you have to publish the Blade view from the vendor directory and then forever keep up with the upstream changes. Maybe even more demanding: The Blade view uses data and methods from the corresponding Vue Component. One should understand and maintain that link as well. And if you want to customize the behavior of the Vue Component, it gets even more complicated.
Splade doesn't offer an architecture to expand it with your own components. Funny enough, it doesn't need to because anybody could do the Blade-in-Vue trick; you don't need Splade for that. But it would be convenient if Splade offered a way to help, for me as well! There's no mechanism to keep the Blade and Vue parts in sync. I have to look in the Vue script to see what a method is called and what it does to use it in the Blade view. It's not a great experience, and it's prone to errors.
At first sight, the customization problem seems more straightforward to solve than the other. We could introduce more slots to the templates and replace the Tailwind classes with generic classes (and provide a Tailwind implementation). There are many more clever ways of improving this, but you get the point. For expandability, we could introduce a bunch of helpers to improve the binding and syncing of the Blade and Vue parts. All this might take away some pain points, but the bottom line is that it's still a lot of work to maintain a customized Splade Component. This two-part component architecture will always differ from regular single-file Vue Components or Blade Components sprinkled with Alpine.js. This whole 'sprinkled with Vue' idea is wonderful until it's not.
My next big experiment was to bring the Vue script block to Blade and keep it all in a single file. Hugely inspired by Laravel Volt, I started working on Splade Core. This would be the missing helper that fundamentally brings Blade and Vue together, not as an implementation of components but as a tool to build components. With Splade Core, you start with a <script setup>
block in your template and then write your regular HTML template below it. I got it working pretty fast and even got some cool features working, like two-way binding of props between PHP and Vue and even calling PHP methods from Vue. At this point, it began to feel more like Livewire than Inertia, but I had no problem with that. So, I started implementing some real-world components from Splade v1, like the Form Components and SPA navigation.
Splade Core works beautifully in most cases. It's exactly what I was looking for, and I was motivated to rebuild all Splade v1 features upon Splade Core. But as I got to the more complex components and situations, I saw the downside. It got really, really complex, and not everything is even working correctly. Particularly, the concept of slots is hard to tackle as Blade and Vue handle them differently.
I'm parsing JavaScript in PHP, messing around with the Blade compiler, automatically generating Vue components, building a Vite plugin to keep everything in sync, providing a tricky skeleton for others to develop and release components, and this list goes on and on. Each feature is well tested, but this beast is unmaintainable and super hard to debug. I can't go on like this. I'm wasting valuable hours, and people probably wouldn't get on the 'Splade Core'-train, even I don't know what's going on some of the time.
Splade Core doesn't have a future. I marked it experimental from the beginning, and that's where I'll leave it. I don't know how to complete and stabilize it without spending more days/weeks/months on it. Another thing I have to admit: I'm not enjoying building it anymore. It's become a burden instead of moving Splade forward. As you can imagine, leaving Splade Core behind was a tough decision. I already invested so much time in it, but investing even more is not worth it.
Where does this leave Splade? I've thought of a few options. As Splade Core is dropped, so is Splade v2. I could rethink v3 with a new architecture, another variant of Splade Core, but I fear I fall into the same trap. There's no guarantee it will work out this time. So, there will be no v2 or v3 in the near future.
Another option is to pick up Splade v1. This won't solve any fundamental problems; I can't deny they exist. Splade v1 is not dead, but it's clearly not the way forward. There are 120+ open issues in the repository, and though fixing them would be great, the basics will stay the same. Addressing them would be great (not all are bugs), but it still doesn't feel like working on Splade's future. I may have taken this whole idea of Blade-in-Vue way too far. It feels strange to keep working on an architecture with this many concerns.
Last week, I took the opportunity to reflect on Splade and its future. I hope you see where I struggle. Besides determining a direction for v1 and a possible reimagined future Splade, this week was also an opportunity to reinvestigate Inertia and Livewire. I'm constantly working with Inertia, but I only briefly worked with Livewire 3. Especially with Inertia, I see a chance to improve the development experience.
Remember that open-source app I started this post with? What if I took some of Splade's philosophies back to Inertia? I'm not talking about forking Inertia or rebuilding it. What about a set of independent, optional packages you can pull into your Inertia project to bring you some of Splade's ideas? Tuned explicitly for Laravel and Vue. The open-source app could be used as a way to demonstrate these.
Maybe it's needless to say, but Livewire has massive momentum. Its release was highly anticipated, and I also feel Filament contributes to that. I've built two Filament + Livewire 3 projects over the last months to get a feel for what it's like. I totally understand the appeal. One was a relatively simple site that needed some interactivity and a backend to manage the content. I absolutely enjoyed the experience. The amount of plugins for Filament is impressive! Also, the things that Livewire 3 now does are mind-blowing. Some of Splade's features were really hard to build; I can't imagine what it must be like to build them at the scale of Livewire.
On Laracon EU, I talked with Povilas about this momentum and how Inertia sometimes seems forgotten. I asked on Twitter where all the Inertia people are, and the response was what I expected. Inertia is such a thin layer between Laravel and Vue that it's not about debugging and learning Inertia. You learn to work with Livewire, but learning Inertia is mostly about Laravel and Vue. This reignited the spark in me why I loved Inertia in the first place: I love both Laravel and Vue. I love Blade as well, but as Taylor said himself, Livewire is what Blade should have been. It's missing the interactivity by default.
Let's bust the myth that Inertia is dead. I must admit I had this gut feeling about Inertia for some time as well, even wondering whether it had a future, but now I know better. It's not dead. Yes, some repositories have tons of issues and PRs, and some features never got merged, but it's growing at a similar rate as Livewire. Maybe it's increasing even faster, as new releases of Livewire are much more common than new Inertia releases. It's not about who grows faster or is more popular, it's about stating that Inertia is alive. In this case, the release frequency should not play a role in choosing one of the other.
I'm surprised you made it this far, but here's a short list of what I've found out and decided this week:
The elephant in the room might be: what's the future of Splade? The truth is, I'm not sure yet, but I narrowed it down. I'll waste no more time on Splade Core or complete rewrites of Splade. This massively frees my mind to think about Splade v1. I can't give any guarantees. I can not promise I'll address and maintain all issues for years to come; the user base is not large enough, and I make practically no money from it. As I said, I need more time to figure out the future of Splade v1.
]]>$directoryContents = Process::run('ls -la')->output();
While the Laravel ecosystem has a package called Envoy that makes it easy to use the Blade template engine to define and run tasks, Laravel Task Runner takes a different approach. This new package allows you to write tasks as if they were Blade Components, providing a dedicated class and template view. It is built on top of the new Process API, enabling you to run tasks directly from your code without installing a separate binary like Envoy. Plus, it supports running tasks locally and on remote servers through an SSH connection.
Let's take a look at a simplified deploy script. With the new package, you can create a task with the Artisan CLI:
php artisan make:task DeployApp
This command generates a DeployApp.php
class in the app/Tasks
directory and a deploy-app.blade.php
template file in the resources/views/tasks
directory. Just like Blade components, public properties and methods are accessible in the template. For instance, you can make the repository branch and migration options variable:
use ProtoneMedia\LaravelTaskRunner\Task;
class DeployApp extends Task
{
public function __construct(public string $branch) { }
public function databaseConnection()
{
return 'mysql';
}
}
And then use them in the template:
cd /var/www/html
git pull origin {{ $branch }}
php artisan migrate --database={{ $databaseConnection() }}
Tasks can easily be dispatched:
DeployApp::dispatch();
If you want to pass constructor parameters, you may use the static make()
method:
DeployApp::make('dev-new-feature')->dispatch();
In addition to running tasks locally, you can also dispatch them on a remote server and run them in the background. You can configure one or more remote connections in the config file.
DeployApp::inBackgronud()->onConnection('web')->dispatch();
Just like Laravel’s Process implementation, you can easily fake tasks and make assertions in your test suite:
TaskRunner::fake();
$this->post('/api/composer/global-update');
TaskRunner::assertDispatched(ComposerGlobalUpdate::class);
Here's another example of faking task output:
TaskRunner::fake([
ComposerGlobalUpdate::class => 'Updating dependencies'
]);
$this->post('/api/composer/global-update');
TaskRunner::assertDispatched(ComposerGlobalUpdate::class, function (PendingTask $task) {
return $task->shouldRunInBackground()
&& $task->shouldRunOnConnection('production');
});
The package is fully open-source and available at Github, where you'll also find the full documentation.
]]>I want to highlight a few new features, starting with handling existing file uploads with Filepond and Spatie's Media Libary. In addition, there's now a client-side Event Bus and Global Data Store, there are translation files for over 20 languages, the confirmation dialog may now ask for a user's password, and there's a new Rehydrate component. This one allows you to refresh sections of Blade templates. Most existing components got additional features as well, like submitting forms in the background, and the Defer component got much more powerful.
Today is another step in improving the Splade ecosystem by releasing the Jetstream Starter Kit for Splade. While there was support for Breeze, many people have been asking about Jetstream. Luckily, the wait is over. The Starter Kit is mostly the same as the first-party variant. It's got all the authentication features like two-factor authentication and session management, as well as API support and team management. Just like the Breeze starter kit, we aim to keep it similar to the upstream as much as possible.
Next up... the Splade Demo app. Stay tuned!
]]>So, what is Laravel Splade? It consists of two major concepts. The first one is being able to create a SPA with Blade templates. No need for React or Vue templates, no Inertia, no Livewire, just plain old Blade templates. Under the hood, it uses Vue 3's render engine, but that's all managed for you. The second concept is making Blade templates interactive. Splade comes with more than 20 components. Some focus on one specific feature (like toggling, two-way data binding, session flash data, etc.), and some are somewhat bigger (modals, slideovers, table and form components). Some components come with a default Tailwind styling, but you can fully customize it, and you don't even have to use Tailwind.
Here's a video that shows the basics and some components of Laravel Splade:
After the first release, we added many features. For example, it now supports Server-side Rendering (SSR) and SEO handling, there's a super-rich Table component (like DataTables), and it's got fantastic Form components with support for model binding, validation, and 3rd-party libraries. The Table component is based on another Table package we introduced last year, and it integrates beautifully with one of Spatie's most fabulous packages: Laravel Query Builder. Check out the Table component introduction video:
Over the last six weeks, we began using Splade in our real-life projects. We started two new projects with Splade, and we're currently moving one giant app over to Splade. The benefits are enormous. Links and form submissions without full page reload, giving the user a smooth experience. We removed numerous custom Vue components, and integrating Modals has become much cleaner. The Form components can clean up your templates, as you never have to worry about validation messages and 'old' data ever again. Take a look at the Form components:
We will work on improving Splade and the documentation, tagging the next major version in the coming weeks.
]]>Storage::fake()
method. This method brilliantly fakes an entire filesystem disk, whether a local disk or an external disk like an S3 service. As a result, you don't have to rewrite a single thing in your code. For end-to-end tests with Laravel Dusk, there are helper methods as well. For example, an attach
method easily attaches a file to an input element.
While this covers most of the testing tools you'll ever need, I've found myself in need of interacting and testing with a real, live S3 service. In my case, I implemented S3's multipart upload feature and wanted a complete end-to-end test. I want to ensure that everything, from attaching a file to the input element to handling the multipart upload, is guaranteed to work.
I've stumbled upon MinIO, an open-source storage suite that runs on all major platforms. It's super lightweight and still fully S3-compatible. There's hardly a thing you have to configure to make it sing with Laravel. You need to point the endpoint
and url
configuration key to the MinIO server and set the use_path_style_endpoint
configuration key to true.
AWS_ACCESS_KEY_ID=user
AWS_SECRET_ACCESS_KEY=password
AWS_DEFAULT_REGION=eu-west-1
AWS_BUCKET=bucket-name
AWS_URL=https://127.0.0.1:9000
AWS_ENDPOINT=https://127.0.0.1:9000
AWS_USE_PATH_STYLE_ENDPOINT=true
To make the testing part even more effortless, I've created a little package that automatically starts and configures a MinIO server for each test. It works fantastic on GitHub Actions as well. You can choose to start a new server for each test or use the same server for the entire test suite. You only have to add a trait to your test, and add the bootUsesMinIOServer
method:
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use ProtoneMedia\LaravelMinioTestingTools\UsesMinioServer;
use Tests\DuskTestCase;
class UploadVideoTest extends DuskTestCase
{
use DatabaseMigrations;
use UsesMinioServer;
protected function setUp(): void
{
parent::setUp();
$this->bootUsesMinIOServer();
}
/** @test */
public function it_can_upload_a_video_using_multipart_upload()
{
}
}
In the README.md of the repository, you'll find how to configure GitHub Actions to use MinIO for your test suite.
]]>This blog post follows up on the first overview of the new Blade, Requests, Routing and Validation features in Laravel 8 since the original release in September 2020. Enjoy!
I got most code examples and explanations from the PRs and official documentation.
The collect
method now allows you to get a subset.
$request->collect(['email', 'roles'])->all();
// [
// 'email' => 'test@example.com',
// 'roles' => [1, 2, 3]
// ];
For convenience, you may attach the can
middleware to your route using the can
method:
// Before:
Route::put('/post/{post}', UpdatePost::class)->middleware('can:update,post');
// After:
Route::put('/post/{post}', UpdatePost::class)->can('update', 'post');
Retrieve input from the request as a Carbon instance.
// Before:
if ($birthday = $request->input('birthday')) {
$birthday = Carbon::parse($birthday);
}
// After:
$birthday = $request->date('birthday');
// With format and timezone:
$elapsed = $request->date('elapsed', '!H:i', 'Europe/Madrid');
Merge new input into the request's input, but only when that key is missing from the request.
// Before:
if ($request->missing('boolean_setting')) {
$request->merge(['boolean_setting' => 0]);
}
// After:
$request->mergeIfMissing(['boolean_setting' => 0])
If a group of routes all utilize the same controller, you may now use the controller
method to define the common controller for all of the routes within the group.
// Before:
Route::prefix('placements')->as('placements.')->group(function () {
Route::get('', [PlacementController::class, 'index'])->name('index');
Route::get('/bills', [PlacementController::class, 'bills'])->name('bills');
Route::get('/bills/{bill}/invoice/pdf', [PlacementController::class, 'invoice'])->name('pdf.invoice');
});
// After:
Route::controller(PlacementController::class)->prefix('placements')->as('placements.')->group(function () {
Route::get('', 'index')->name('index');
Route::get('/bills', 'bills')->name('bills');
Route::get('/bills/{bill}/invoice/pdf', 'invoice')->name('pdf.invoice');
});
For years we had the @json
Blade directive to avoid manually calling json_encode
, but now there's a replacement to handle more cases: @js
. It handles objects, arrays and strings, else, falls back to json_encode
.
<div x-data="@js($data)"></div>
This new method allows you to call render
and get the string with all the Blade compiled returned.
Blade::render('Hello, {{ $name }}', ['name' => 'Claire']);
// Returns 'Hello, Claire'
This PR adds the ability to provide multiple possible date formats to the date_format
validation rule, allowing developers to use one request input for multiple possible date format types.
Validator::validate([
'dates' => ['2021-12-01', '12-01'],
], [
'dates.*' => 'date_format:Y-m-d,m-d',
]);
This PR adds a new Enum validation rule that will make sure the provided data has a corresponding case in the enum.
Validator::validate([
'status' => 'pending'
], [
'status' => [new Enum(ServerStatus::class)]
]);
Reverse off the accepted
and accepted_if
rules. The field under validation must be "no"
, "off"
, 0
, or false
.
Validator::validate([
'foo' => 'off'
], [
'foo' => 'declined'
]);
A new validation rule to ensure that the attribute is a valid MAC address.
Validator::validate([
'mac' => '01-23-45-67-89-ab'
], [
'mac' => 'mac_address'
]);
If the field under validation is present, no fields in anotherfield can be present, even if empty.
Validator::validate([
'users' => [
['email' => 'foo', 'emails' => 'foo'],
['emails' => 'foo'],
],
], [
'users.*.email' => 'prohibits:users.*.emails',
]);
Specify the default validation rules for passwords in a single location of your application.
Password::defaults(function () {
$rule = Password::min(8);
return $this->app->isProduction()
? $rule->mixedCase()->uncompromised()
: $rule;
});
]]>
Following last week's blog post about Jobs and Queues, here's an overview of new features and improvements when it comes to Blade, Requests, Routing and Validation.
I got most code examples and explanations from the PRs and official documentation.
Quickly inspect the request input.
public function index(Request $request)
{
// before:
dd($request->all());
// after:
$request->dd();
$request->dd(['name', 'age']);
$request->dd('name', 'age');
}
Typically, a 404 HTTP response will be generated if an implicitly bound resource model is not found. With the missing
method you may change this behavior:
Route::get('/locations/{location:slug}', [LocationsController::class, 'show'])
->name('locations.view')
->missing(fn($request) => Redirect::route('locations.index', null, 301));
This allows you to specify a single route that allows soft deleted models when resolving implicit model bindings:
Route::post('/user/{user}', function (ImplicitBindingModel $user) {
return $user;
})->middleware(['web'])->withTrashed();
Get the full URL for the request without the given query string parameters.
// When the current URL is
// https://example.com/?color=red&shape=square&size=small
request()->fullUrlWithoutQuery('color');
// https://example.com/?shape=square&size=small
request()->fullUrlWithoutQuery(['color', 'size']);
// https://example.com/?shape=square
Allows to determine if a given attribute is present on a component.
@if ($attributes->has('class'))
<div>Class Attribute Present</div>
@endif
Inspired by Vue's :class
syntax, this PR adds a class()
method to the ComponentAttributeBag
class. It merges the given classes into the attribute bag.
<div {{ $attributes->class(['p-4', 'bg-red' => $hasError]) }}>
class
directive as well:<div @class(['p-4', 'bg-red' => $hasError])>
This PR adds a new Blade::stringable()
method that allows the user to add intercepting closures for any class. The returned value will be outputted in Blade.
// AppServiceProvider
Blade::stringable(Money::class, fn($object) => $object->formatTo('en_GB'));
Blade::stringable(Carbon::class, fn($object) => $object->format('d/m/Y')));
<dl>
<dt>Total</dt>
<dd>{{ $product->total }}</dd> <!-- This is a money object, but will be outputted as an en_GB formatted string -->
<dt>Created at</dt>
<dd>{{ $product->created_at }}</dd> <!-- This is a Carbon object, but will be outputted as English date format -->
</dl>
You may use the @aware
directive to access data from a parent component inside a child component. For example, imagine a parent <x-menu>
and child <x-menu.item>
:
<x-menu color="purple">
<x-menu.item>...</x-menu.item>
<x-menu.item>...</x-menu.item>
</x-menu>
By using the @aware
directive, you can make the color
prop available inside
<!-- /resources/views/components/menu/item.blade.php -->
@aware(['color' => 'gray'])
<li {{ $attributes->merge(['class' => 'text-'.$color.'-800']) }}>
{{ $slot }}
</li>
The field under validation must be empty or not present if the anotherfield field is equal to any value.
Validator::validate([
'is_deceased' => false,
'date_of_death' => '2021-01-01'
], [
'date_of_death' => 'prohibited_unless:is_deceased,true'
]);
The field under validation must be empty or not present.
Validator::validate([
'name' => 'hello-world',
'key' => 'random-key',
], [
'name' => 'required|max:255',
'key' => 'prohibited',
]);
To ensure that passwords have an adequate level of complexity, you may use Laravel's Password
rule object:
Validator::validate([
'password' => '123123',
], [
'password' => ['required', 'confirmed', Password::min(8)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised(),
],
]);
To prevent communication problems with the new Password
rule object, the password
rule was renamed to current_password
with the intention of removing it in Laravel 9.
This PR adds a helper method to the unique validation rule for working with models which can be soft deleted.
// Before
Rule::unique('users')->whereNull('deleted_at');
// After
Rule::unique('users')->withoutTrashed();
In addition to the unique
rule in the example above, you may also use the withoutTrashed
method on the exists
rule.
// Before
Rule::exists('users')->whereNull('deleted_at');
// After
Rule::exists('users')->withoutTrashed();
In addition to the accepted
rule, you can now use this rule if another field under validation is equal to a specified value.
Validator::validate([
'newsletter_subscription' => '1',
'newsletter_terms' => '0',
], [
'newsletter_subscription' => 'required|boolean',
'newsletter_terms' => 'accepted_if:newsletter_subscription,1',
]);
The field under validation will be excluded from the request data returned by the validate and validated methods. There are exclude_if
, exclude_unless
, and exclude_without
rules as well.
Validator::validate([
'role' => 'guest',
'email' => 'guest@company.com',
], [
'role' => ['required', 'in:user,guest'],
'email' => ['required_if:role,user', 'exclude_unless:role:user', 'email'],
]);
If the field under validation is present, no fields in anotherfield can be present, even if empty.
Validator::validate([
'email' => 'example@example.com',
'emails' => ['example@example.com', 'other.example@example.com']
], [
'email' => 'prohibits:emails'
]);
You may use Rule::when()
to create a new conditional rule set.
Validator::validate([
'password' => '123456',
], [
'password' => ['required', 'string', Rule::when(true, ['min:5', 'confirmed'])],
]);
You may now retrieve a portion of the validated input data:
$validator->safe()->only(['name', 'email']);
$validator->safe()->except([...]);
This works on Form Requests as well:
$formRequest->safe()->only([...]);
$formRequest->safe()->except([...]);
]]>
In this fourth post of the series, I've gathered new features added to Jobs and Queues in Laravel 8. Since the original release of 8.0 back in September 2020, Jobs and Queues got a bunch of significant updates that you don't want to miss. If you want to learn all the ins and outs of this topic, look at Learn Laravel Queues by Mohamed Said. It's one of the best Laravel books out there.
If you're new to this series, make sure also to check out the previous posts:
Collections
Database and Eloquent ORM (1/2)
Database and Eloquent ORM (2/2)
Alright, let's start! I got most code examples and explanations from the PRs and official documentation.
When the ShouldBeEncrypted
interface is used, Laravel will encrypt the command inside the payload and decrypt it when it processes. You can apply this to Listeners, Mailables and Notifications as well.
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
...
}
A convenient Artisan command to clear queues. Works great in development in conjunction with migrate:fresh
.
php artisan queue:clear
If you're using Laravel Horizon, you should use the horizon:clear
instead:
php artisan horizon:clear
In addition to clearing the queue, there's also a new command to prune stale entries from the batches database.
php artisan queue:prune-batches
There's also a new command to prune stale records from the failed_jobs
database table.
php artisan queue:prune-failed
You may define a set of chained jobs within a batch by placing the chained jobs within an array.
Bus::batch([
new Job1(),
[
new Job2(),
new Job3(),
new Job4(),
],
new Job5(),
])->dispatch();
This allows you to prevent job overlaps based on a key. This middleware requires a cache driver that supports locks. In the example below, it prevents job overlaps for the same order ID, so there is only one job at a time for this order.
use Illuminate\Queue\Middleware\WithoutOverlapping;
public function middleware()
{
return [new WithoutOverlapping($this->order->id)];
}
We already had rate-limiting middleware for request throttling. This PR enables the same rate limiter classes and syntax for rate-limiting jobs!
use Illuminate\Queue\Middleware\RateLimited;
public function middleware()
{
return [new RateLimited('backups')];
}
Following the above examples, you can also apply middleware to Mailables.
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
class OrderShipped extends Mailable
{
use Queueable, SerializesModels;
public function middleware()
{
return [new RateLimited('limiter')];
}
}
Following the above examples, you can also apply middleware to Listeners.
use App\Events\PodcastProcessed;
use Illuminate\Queue\Middleware\RateLimited;
class SendPodcastNotification
{
public function handle(PodcastProcessed $event)
{
//
}
public function middleware()
{
return [new RateLimited('limiter')];
}
}
You may implement the ShouldBeUnique
interface on your job class to ensure that only one instance of a specific job is on the queue at any point in time. This one is different from the WithoutOverlapping
middleware, as that one only limits the concurrent processing of a job. Unique jobs require a cache driver that supports locks.
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
...
}
You may dispatch jobs within database transactions. When you set the after_commit
option to true
(in queues.php
), Laravel will wait until all open database transactions have been committed before actually dispatching the job.
// Queue connection's configuration array:
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],
You may also chain afterCommit
onto your dispatch operation. There's also a beforeCommit
method that indicates that a specific job should be dispatched immediately event if the after_commit
configuration option is set to true
.
DB::transaction(function(){
$user = User::create(...);
SendWelcomeEmail::dispatch($user)->afterCommit();
// or
SendWelcomeEmail::dispatch($user)->beforeCommit();
});
As an alternative for dispatch_now
, you may use the dispatchSync
method to dispatch a job immediately (synchronously). When using this method, the job will not be queued and will be executed immediately within the current process.
$podcast = Podcast::create(...);
ProcessPodcast::dispatchSync($podcast);
// or
dispatch_sync(new ProcessPodcast($podcast));
Sometimes it may be useful to add additional jobs to a batch from within a batched job. This PR brings an add
method on the batch instance that may be accessed via the job's batch method:
use App\Jobs\ImportContacts;
use Illuminate\Support\Collection;
public function handle()
{
if ($this->batch()->cancelled()) {
return;
}
$this->batch()->add(Collection::times(1000, function () {
return new ImportContacts;
}));
}
This PR adds a $failOnTimeouts
job property. When set to true, and the job timeouts, the worker will fail the job immediately despite the $tries
or $maxExceptions
remaining.
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue
{
/**
* Indicate if the job should be marked as failed on timeout.
*
* @var bool
*/
public $failOnTimeout = true;
}
This command displays a table of the connections and queues and their sizes.
php artisan queue:monitor
You can use the --max
option to specify a desired job count threshold, which defaults to 100. When a queue has a job count that exceeds the threshold, a QueueBusy
event will be dispatched. You may listen for this event, for example, to send a notification.
php artisan queue:monitor --max=100
]]>
This blog post follows up on last week's overview of the new Database and Eloquent features in Laravel 8 since the original release in September 2020. I already covered Collections, and next week is all about Jobs and Queues. Enjoy!
I got most code examples and explanations from the PRs and official documentation.
Sometimes you may wish to "update" a given model without dispatching any events. You may accomplish this using the updateQuietly
method, which uses the saveQuietly
method under the hood:
$flight->updateQuietly(['departed' => false]);
As of v8.59, you may also use the createOneQuietly
, createManyQuietly
and createQuietly
methods when using Model Factories:
Post::factory()->createOneQuietly();
Post::factory()->count(3)->createQuietly();
Post::factory()->createManyQuietly([
['message' => 'A new comment'],
['message' => 'Another new comment'],
]);
You can now use a Model instance with the whereKey
and whereKeyNot
methods:
$passenger->tickets()
->whereHas('airline', fn (Builder $query) => $query->whereKey($airline))
->get();
In addition to the withCount
method, you may now use the withExists
method to check the existence of a relationship:
// before:
$users = User::withCount('posts')->get();
$isAuthor = $user->posts_count > 0;
// after:
$users = User::withExists('posts')->get();
$isAuthor = $user->posts_exists;
// the column name can also be aliased:
$users = User::withExists([
'posts as is_author',
'posts as is_tech_author' => function ($query) {
return $query->where('category', 'tech');
},
'comments',
])->get();
This PR follows upon the withExists
method in the example above. You may now use the loadExists
method in both Model and Eloquent Collections:
$books = Book::all();
$books->loadExists(['author', 'publisher']);
One-to-one relations that are a partial relation of a one-to-many relation. You can retrieve the "latest" or "oldest" related model of the relationship:
/**
* Get the user's most recent order.
*/
public function latestOrder()
{
return $this->hasOne(Order::class)->latestOfMany();
}
/**
* Get the user's oldest order.
*/
public function oldestOrder()
{
return $this->hasOne(Order::class)->oldestOfMany();
}
/**
* Get the user's largest order.
*/
public function largestOrder()
{
return $this->hasOne(Order::class)->ofMany('price', 'max');
}
You may instruct Laravel to always prevent the lazy loading of relationships. You should call this method within the boot
method of your app's AppServiceProvider
class.
Model::preventLazyLoading(! app()->isProduction());
This PR is a very advanced one! It allows you to change a "subselect"-builder, whereby the changes are also applied to the parent query builder. The preserved closures will be called before the query gets rendered. This way, you can keep "subquery"-builders alive and modify the subquery after applying it to the parent builder:
// 1. Add the subquery
$builder->beforeQuery(function ($query) use ($subQuery) {
$query->joinSub($subQuery, ...);
});
// 2. Add constraints to the subquery
$subQuery->where('foo', 'bar');
// 3. Render the subquery, the constraints from 2. are applied
$builder->get();
You may want to periodically delete models that are no longer needed. With the Prunable
trait Laravel will automatically remove obsolete model records from the database via a scheduled command.
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
class Flight extends Model
{
use Prunable;
/**
* Get the prunable model query.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function prunable()
{
return static::where('created_at', '<=', now()->subMonth());
}
}
The Artisan CLI command:
php artisan model:prune
You may also define a pruning
method on the model, which will be called before the model is deleted:
protected function pruning()
{
// Delete additional resources associated
// with the model, for example: files
Storage::disk('s3')->delete($this->filename);
}
Ssupport for accessing dates as immutable. Returns a CarbonImmutable
instance instead of a Carbon
instance.
class User extends Model
{
public $casts = [
'date_field' => 'immutable_date',
'datetime_field' => 'immutable_datetime',
];
}
You may now use the whereRelation
and whereMorphRelation
methods to query for a relationship's existence with a single, simple where condition attached to the relationship query:
// Before:
User::whereHas('posts', function ($query) {
$query->where('published_at', '>', now());
})->get();
// After
User::whereRelation('posts', 'published_at', '>', now())->get();
// Morph relation
Comment::whereMorphRelation('commentable', '*', 'public', true);
The new whereMorphedTo
and orWhereMorphedTo
methods are a shortcut for adding a where condition looking for models that are morphed to a specific related model, without the overhead of a whereHas
subquery:
Feedback::whereMorphedTo('subject', $user)->get();
Feedback::whereMorphedTo('subject', User::class)->get();
You may call the enforceMorphMap
method in the boot method of your apps' AppServiceProvider
class. This disallows morphs without a morph map on it.
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::enforceMorphMap([
'post' => Post::class,
'video' => Video::class,
]);
In addition to the value
method, you may now use the valueOrFail
method. This will throw a ModelNotFoundException
if the model is not found.
// Before:
$votes = User::where('name', 'John')->firstOrFail('votes')->votes;
// Now:
$votes = User::where('name', 'John')->valueOrFail('votes');
The new whereBelongsTo
method automatically determines the proper relationship and foreign key for the given model:
// Before:
$posts = Post::where('user_id', $user->id)->get();
// After:
$posts = Post::whereBelongsTo($user)->get();
]]>
In this series, I show you new features and improvements to the Laravel framework since the original release of version 8. Last week, I wrote about the Collection class. This week is about the Database and Eloquent features in Laravel 8. The team added so many great improvements to the weekly versions that I split the Database and Eloquent features into two blog posts. Here is part one!
I got most code examples and explanations from the PRs and official documentation.
Add a subquery cross join to the query.
use Illuminate\Support\Facades\DB;
$totalQuery = DB::table('orders')->selectRaw('SUM(price) as total');
DB::table('orders')
->select('*')
->crossJoinSub($totalQuery, 'overall')
->selectRaw('(price / overall.total) * 100 AS percent_of_total')
->get();
We can now do model comparisons between related models, without extra database calls!
// Before: foreign key is leaking from the post model
$post->author_id === $user->id;
// Before: performs extra query to fetch the user model from the author relation
$post->author->is($user);
// After
$post->author()->is($user);
If you would like to perform multiple "upserts" in a single query, then you may use the upsert
method instead of multiple updateOrCreate
calls.
Flight::upsert([
['departure' => 'Oakland', 'destination' => 'San Diego', 'price' => 99],
['departure' => 'Chicago', 'destination' => 'New York', 'price' => 150]
], ['departure', 'destination'], ['price']);
The explain()
method allows you to receive the explanation from the builder (both Query and Eloquent).
User::where('name', 'Illia Sakovich')->explain();
User::where('name', 'Illia Sakovich')->explain()->dd();
If you are eager loading a morphTo
relationship, Eloquent will run multiple queries to fetch each type of related model. You may add additional constraints to each of these queries using the MorphTo
relation's constrain
method:
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;
$comments = Comment::with(['commentable' => function (MorphTo $morphTo) {
$morphTo->constrain([
Post::class => function (Builder $query) {
$query->whereNull('hidden_at');
},
Video::class => function (Builder $query) {
$query->where('type', 'educational');
},
]);
}])->get();
This method allows you to directly order the query results of a BelongsToMany
relation:
class Tag extends Model
{
public $table = 'tags';
}
class Post extends Model
{
public $table = 'posts';
public function tags()
{
return $this->belongsToMany(Tag::class, 'posts_tags', 'post_id', 'tag_id')
->using(PostTagPivot::class)
->withTimestamps()
->withPivot('flag');
}
}
class PostTagPivot extends Pivot
{
protected $table = 'posts_tags';
}
// Somewhere in a controller
public function getPostTags($id)
{
return Post::findOrFail($id)->tags()->orderPivotBy('flag', 'desc')->get();
}
The sole
method will return the only record that matches the criteria. If no records are found, a NoRecordsFoundException
will be thrown. If multiple records were found, a MultipleRecordsFoundException
will be thrown.
DB::table('products')->where('ref', '#123')->sole()
The after method may be used to add columns after an existing column in the schema:
Schema::table('users', function (Blueprint $table) {
$table->after('remember_token', function ($table){
$table->string('card_brand')->nullable();
$table->string('card_last_four', 4)->nullable();
});
});
Laravel automatically assign a class name to all of the migrations. You may now return an anonymous class from your migration file:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up()
{
Schema::table('people', function (Blueprint $table) {
$table->string('first_name')->nullable();
});
}
};
The new chunkMap
method is similar to the each
query builder method, where it automatically chunks over the results:
return User::orderBy('name')->chunkMap(fn ($user) => [
'id' => $user->id,
'name' => $user->name,
]), 25);
Since the array cast returns a primitive type, it is not possible to mutate an offset of the array directly. To solve this, the AsArrayObject
cast casts your JSON attribute to an ArrayObject
class:
// Within model...
$casts = ['options' => AsArrayObject::class];
// Manipulating the options...
$user = User::find(1);
$user->options['foo']['bar'] = 'baz';
$user->save();
If you would like to override all items within the $with
property for a single query, you may use the withOnly
method:
class Product extends Model{
protected $with = ['prices', 'colours', 'brand'];
public function colours(){ ... }
public function prices(){ ... }
public function brand(){ ... }
}
Product::withOnly(['brand'])->get();
Cursor-based pagination places a "cursor" string in the query string, an encoded string containing the location that the next paginated query should start paginating and the direction it should paginate. This method of pagination is particularly well-suited for large data sets and "infinite" scrolling user interfaces.
use App\Models\User;
use Illuminate\Support\Facades\DB;
$users = User::orderBy('id')->cursorPaginate(10);
$users = DB::table('users')->orderBy('id')->cursorPaginate(10);
In addition to the withCount
method, Eloquent now provides withMin
, withMax
, withAvg
, and withSum
methods. These methods will place a {relation}_{function}_{column}
attribute on your resulting models.
Post::withCount('comments');
Post::withMin('comments', 'created_at');
Post::withMax('comments', 'created_at');
Post::withSum('comments', 'foo');
Post::withAvg('comments', 'foo');
Under the hood, these methods use the withAggregate
method:
Post::withAggregate('comments', 'created_at', 'distinct');
Post::withAggregate('comments', 'content', 'length');
Post::withAggregate('comments', 'created_at', 'custom_function');
Comment::withAggregate('post', 'title');
Post::withAggregate('comments', 'content');
In addition to the new with*
method above, new load*
methods are added to the Collection
and Model
class.
// Eloquent/Collection
public function loadAggregate($relations, $column, $function = null) {...}
public function loadCount($relations) {...}
public function loadMax($relations, $column) {...}
public function loadMin($relations, $column) {...}
public function loadSum($relations, $column) {...}
public function loadAvg($relations, $column) {...}
// Eloquent/Model
public function loadAggregate($relations, $column, $function = null) {...}
public function loadCount($relations) {...}
public function loadMax($relations, $column) {...}
public function loadMin($relations, $column) {...}
public function loadSum($relations, $column) {...}
public function loadAvg($relations, $column) {...}
public function loadMorphAggregate($relation, $relations, $column, $function = null) {...}
public function loadMorphCount($relation, $relations) {...}
public function loadMorphMax($relation, $relations, $column) {...}
public function loadMorphMin($relation, $relations, $column) {...}
public function loadMorphSum($relation, $relations, $column) {...}
public function loadMorphAvg($relation, $relations, $column) {...}
Add a polymorphic relationship count / exists condition to the query.
public function hasMorph($relation, ...)
public function orHasMorph($relation,...)
public function doesntHaveMorph($relation, ...)
public function whereHasMorph($relation, ...)
public function orWhereHasMorph($relation, ...)
public function orHasMorph($relation, ...)
public function doesntHaveMorph($relation, ...)
public function orDoesntHaveMorph($relation,...)
Example with a closure to customize the relationship query:
// Retrieve comments associated to posts or videos with a title like code%...
$comments = Comment::whereHasMorph(
'commentable',
[Post::class, Video::class],
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
// Retrieve comments associated to posts with a title not like code%...
$comments = Comment::whereDoesntHaveMorph(
'commentable',
Post::class,
function (Builder $query) {
$query->where('title', 'like', 'code%');
}
)->get();
]]>
This year, the Laravel team announced a new release schedule for major Laravel versions. Instead of a major version every six months, we now get a major release every 12 months. This change didn't stop the team from improving the current release, Laravel 8. Over the last 14 months, it got so many great updates that you might have lost track of it.
I'll release a series of blog posts to highlight some of the best features and improvements since the release of v8 back in September 2020. In total, I've gathered over 100 code examples, so I'll split this blog post into five or six posts and group them by topic. Let's start with Collections!
I got most code examples and explanations from the PRs and official documentation.
The pipeInto method creates a new instance of the given class and passes the collection into the constructor:
// Before:
Category::get()->pipe(function (Collection $categories) {
return new CategoryCollection($categories);
});
// After:
Category::get()->pipeInto(CategoryCollection::class);
Split a collection into a certain number of groups, and fill the first groups completely.
$array = collect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
// will return 3 chunks of sizes 4, 3, and 3.
$array->splitIn(3);
Determine if the collection contains a single element.
collect([])->isSingle(); // false
collect([1])->isSingle(); // true
collect([1, 2])->isSingle(); // false
Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
$collection = collect([
['name' => 'foo'],
['name' => 'bar'],
['name' => 'bar'],
]);
// $result will be equal to: ['name' => 'foo']
$result = $collection->where('name', 'foo')->sole();
// $result will be equal to: ['name' => 'foo']
$result = $collection->sole(function ($value) {
return $value['name'] === 'foo';
});
// This will throw an ItemNotFoundException
$collection->where('name', 'INVALID')->sole();
// This will throw a MultipleItemsFoundException
$collection->where('name', 'bar')->sole();
Create chunks representing a "sliding window" view of the items in the collection.
collect([1, 2, 3, 4, 5])->sliding(2);
// [[1, 2], [2, 3], [3, 4], [4, 5]]
$collection = collect([1, 2, 3, 4, 5]);
$collection->pop(3);
// [5, 4, 3]
$collection->all();
// [1, 2]
$collection = collect([1, 2, 3, 4, 5]);
$collection->shift(3);
// [1, 2, 3]
$collection->all();
// [4, 5]
Determine if any of the keys exist in the collection.
// This example would return true:
collect(['first' => 'Hello', 'second' => 'World'])->hasAny(['first', 'fourth']);
// While this would return false:
collect(['first' => 'Hello', 'second' => 'World'])->hasAny(['third', 'fourth']);
In the next blog post, I'll take a look at the Database and Eloquent improvements!
Update 2022-02-07:
The inverse of the contains
method, to determine whether the collection does not contain a given item.
$collection = collect([1, 2, 3, 4, 5]);
$collection->doesntContain(function ($value, $key) {
return $value < 5;
});
// false
This PR adds the pipeThrough()
method for collections, allowing developers to insert an array of pipe callbacks that can manipulate the collection, carrying the return value from the previous pipe into following pipes.
$pipes = [
fn ($podcasts) => $podcasts->each(
fn ($podcast) => $podcast->process()
),
fn ($podcasts) => $podcasts->sum(
fn ($podcast) => $podcast->hasProcessed()
),
];
$processed = Podcast::all()->pipeThrough($pipes);
The sortKeysUsing
method sorts the collection by the keys of the underlying associative array using a callback:
$collection = collect([
'ID' => 22345,
'first' => 'John',
'last' => 'Doe',
]);
$collection->sortKeysUsing('strnatcasecmp');
/*
[
'first' => 'John',
'ID' => 22345,
'last' => 'Doe',
]
*/
Get an item from the collection by key or add it to collection if it does not exist.
// Before:
if ($collection->has($key) === false) {
$collection->put($key, $this->create($data));
}
return $collection->get($key);
// After:
$collection->getOrPut($key, fn () => $this->create($data));
// Or with fixed values:
$collection->getOrPut($key, $value);
]]>
The routes file for this app is quite simple. Based on the slug URI segment of the route, I fetch the video from the database and return a Blade view:
use App\Models\Video;
Route::get('/{slug}', function ($slug) {
$video = Video::whereSlug($slug)->firstOrFail();
return view('video', ['video' => $video]);
});
I added a second route, which also returns a Blade view, just to render the OG image.
Route::get('/{slug}/openGraphImage', function ($slug) {
$video = Video::whereSlug($slug)->firstOrFail();
return view('videoOpenGraphImage', ['video' => $video]);
})->name('video.openGraphImage');
These images are typically 1200x630 pixels, so I created a container with those dimensions and centered it on the page. Be sure to enable Tailwind's JIT mode to get the square-bracket notation to work. In this container, I added the Artisan School logo and the title of the video.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
<body class="flex items-center justify-center min-h-screen">
<div class="bg-blue-500 w-[1200px] h-[630px]">
<!-- your content -->
</div>
</body>
</html>
Now how do we store an image out of this Blade view? There's a gorgeous package called Browsershot by Spatie. It converts a webpage to an image or PDF using a headless version of Google Chrome. It needs some setup, but it's straightforward to use. I added a method within my Video model that handles the conversion and saves the image to the public folder of my app.
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Spatie\Browsershot\Browsershot;
class Video extends Model
{
public function saveOpenGraphImage()
{
$path = Storage::disk('public')->path("{$this->slug}.jpg");
Browsershot::url(route('video.openGraphImage', ['slug' => $this->slug]))
->waitUntilNetworkIdle()
->showBackground()
->windowSize(1200, 630)
->setScreenshotType('jpeg', 100)
->save($path);
}
public function getOpenGraphImageUrl(): string
{
return Storage::disk('public')->url("{$this->slug}.jpg");
}
}
To make my life easy, I also added a method to return the full URL of the OG image. You can use this in the header section of the HTML page or with an SEO package like artesaos/seotools.
<meta property="og:image" content="{{ $video->getOpenGraphImageUrl() }}">
]]>
I've had the idea to record the complete Laravel documentation as video material for a while now. As you might know, I'm running a YouTube channel where I do live coding sessions now and then. However, due to the development of Launcher and other projects, the weekly schedule was not so weekly anymore, and I feel it's time to take it to the next level!
Starting next week, you can expect the following:
I'll upload new videos directly when they're ready, not waiting until I produce a complete collection of videos.
Want to keep notified about Artisan School? I've created a small landing page with a sign-up form. I'll start publishing the first videos soon, so please leave your email address to stay in the loop! There's also a new Twitter account where I'll post the videos as well.
Do you have ideas or suggestions for this platform? Don't hesitate to send me a message, for example, on Twitter :)
]]>Inertia.js has an event system to hook into lifecycle events. You can, for example, hook into page visits and unsuccessful form requests. The package allows you to wait for events to happen.
Imagine your application has a form to create new users. After successfully storing the user into the database, the application redirects to the "users index" route. When you write a Dusk test for this scenario, you fill in the form, trigger the submit button, and then you want to assert that the user gets redirected to the next page. Now you probably run into an issue where the browser is still handling the store request when you assert that the user is redirected.
This can be solved with the waitForInertiaNavigate()
method! You put it between the submit trigger and the following assertion, and everything should be fine. Here's an example of the Dusk test.
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Tests\DuskTestCase;
class ExampleTest extends DuskTestCase
{
use DatabaseMigrations;
/**
* A basic browser test example.
*
* @return void
*/
public function it_can_store_a_user_and_redirect_back_to_the_index_route()
{
$this->browse(function ($browser) {
$browser->loginAs(User::first())
->visit(route('user.create'))
->type('name', 'New User')
->press('Submit')
->waitForInertiaNavigate()
->assertRouteIs('user.index')
->assertSee('User Added!');
});
}
}
You can find the package at GitHub.
]]>In the first release, we support Vue 2.6 and Laravel 8, but there's a roadmap that includes support for Vue 3. The package consists of a server-side implementation and a client-side implementation, which are thoroughly documented at the GitHub repository. Installing the package and setting up your first table takes five minutes at most. Here's a sneak peek of what it can do:
You can find the package at GitHub.
]]>As our pages become more complex, we found ourselves in need of loading forms in modals. In some cases, you don't want to redirect the user to a new page, but you want the user to stay on the current page and provide extra functionality with a modal.
Imagine a form to create a new product. Amongst the name, description, and price, you can also attach a category to it. But what if you want to attach a category that doesn't exist yet? We have a dedicated route and component to create new categories, but wouldn't it be great if you could re-use that functionality while the user stays on the product creation page?
You could take the shortcut. Build the category form for the second time, but now within a modal on the product creation page. On a large scale, this seems unmaintainable to me. Now we have to build and test two category creation forms.
The past few days, I took a deep dive in the Inertia source code and found a way to re-use existing routes and use them in a modal on another page. It's a very early draft, and it has its limitations, but I want to share it anyway.
Here's a small demo of Ping CRM. On the organizations' index page, there's a button to navigate to the creation page. I've added a second button that loads the same route into a modal.
On the contact creation page, you can select an organization. Here I've added the button as well. Note how the select dropdown instantly updated after I created the organization.
The installation of this package is quite simple. You need to add a Vue Component to your root template add some code to the Laravel middleware. For every route you want to open in a modal, you need to make a minor modification, but nothing else is required. You don't even have to touch your Laravel code!
You can find the source code and instructions at GitHub.
]]>This week we released version 7.5, which brings support for Encrypted HLS. An HLS export consists of many segments: small pieces of video content that'll be stitched together at playback time. Now you can encrypt all segments automatically, either with a single-key or with multiple auto-generated keys. As the segments are unplayable without the encryption keys, you can put the keys behind authentication and authorization to limit the accessibility of your video content.
Adding single-key encryption to an HLS export is just two extra lines of code:
use ProtoneMedia\LaravelFFMpeg\Exporters\HLSExporter;
$encryptionKey = HLSExporter::generateEncryptionKey();
FFMpeg::open('video.mp4')
->exportForHLS()
->withEncryptionKey($encryptionKey)
->addFormat($lowBitrate)
->addFormat($midBitrate)
->addFormat($highBitrate)
->save('encrypted_video.m3u8');
If you want to use multiple keys, you need to store the auto-generated keys with a callback:
FFMpeg::open('video.mp4')
->exportForHLS()
->withRotatingEncryptionKey(function ($filename, $contents) {
Storage::disk('secrets')->put($filename, $contents);
// or...
DB::table('hls_secrets')->insert([
'filename' => $filename,
'contents' => $contents,
]);
})
->addFormat($lowBitrate)
->addFormat($midBitrate)
->addFormat($highBitrate)
->save('encrypted_video.m3u8');
You might be wondering: how does the browser handle the playback when the media segments and encryption keys are stored on different disks? We provide a tool for that as well. Let's take a look at the DynamicHLSPlaylist
class.
HLS uses playlists, which hold references to all different segments and encryption keys. The DynamicHLSPlaylist
class modifies these playlists on-demand and specifically for your application. It basically provides a way to route the encryption keys through your Laravel application. You can now perform additional logic, like authentication and authorization, before delivering the encryption key to the player. The media segments can still be served by the webserver or through a CDN without ever touching the Laravel application. You can read more about this feature in the documentation.
return FFMpeg::dynamicHLSPlaylist()
->fromDisk('public')
->open('encrypted_video.m3u8')
->setKeyUrlResolver(function ($key) {
return route('video.key', ['key' => $key]);
})
->setMediaUrlResolver(function ($segment) {
return Storage::disk('public')->url($segment);
})
->setPlaylistUrlResolver(function ($playlist) {
return route('video.playlist', ['playlist' => $playlist]);
});
Other features of the 7.5 release include support for PHP 8.0 and raw process output, which is great for use-cases like volume analysis. I'll do a live demonstration of the Encrypted HLS feature on YouTube, scheduled for December 24 at 13:30 CET.
So here we are, introducing another new package: Laravel Eloquent Where Not. Beautiful name, isn't it? Let me guide you through it.
You've probably been at the point where you've written a scope for your Eloquent Model that uses all kinds of constraints. Where clauses, relationship counts, date comparisons, you name it. I love the way scopes can clean up your code and prevent you from writing the same constraints repeatedly. But you've presumably been also at the point where you want the opposite outcome of a scope. In plain PHP, this would be an easy thing to do. Wrap the statement in parentheses and negate the expression. Quick example:
$onFrontPage = $post->votes > 100 && $post->comments_count > 20;
$notOnFrontPage = !($post->votes > 100 && $post->comments_count > 20);
Of course, there are more readable ways to achieve the same result. But how can we apply this to database queries? Let's take a look at the Eloquent model below and focus on the onFrontPage
scope:
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function scopeOnFrontPage($query)
{
$query->where('is_public', 1)
->where('votes', '>', 100)
->has('comments', '>=', 20)
->whereHas('user', fn($user) => $user->isAdmin())
->whereYear('published_at', date('Y'));
}
}
Now we can fetch all posts that are suitable for the front page:
$frontPagePosts = Post::onFrontPage()->get();
But what if we want to fetch all posts that didn't make the front page? You can write another scope, sure! But if your business rules change, you need to update at least two scopes.
This new package introduces a whereNot
method. This method allows you the flip the scope, or as some would say, invert the scope. This method is incredibly easy to use. You get a $query
instance, just like defining scopes, to call any scope or constraint on. This method will apply the opposite of the scope.
$nonFrontPagePosts = Post::whereNot(function($query) {
$query->onFrontPage();
})->get();
It comes with a bunch of shortcuts to make this even more readable:
$nonFrontPagePosts = Post::whereNot('onFrontPage')->get();
You can find the package and all its features and documentation on Github, and I did a live demo on YouTube as well. It's only 14 minutes, and it starts with a brief introduction of the demo application. Lately, I've been doing a weekly session on YouTube with all kinds of Laravel related demos and packages. Please subscribe to my channel for more videos!
You can chain local scopes to precisely define your query. This allows you to retrieve a specific set of records from your database. Take a look at this Post
Model. It can have one or more comments, and it has scopes to determine if it has a subtitle and whether someone published it this year.
class Post extends Model
{
protected $casts = [
'published_at' => 'datetime',
];
public function comments()
{
return $this->hasMany(Comment::class);
}
public function scopeHasSubtitle($query)
{
$query->whereNotNull('subtitle');
}
public function scopeFromCurrentYear($query)
{
$query->whereYear('published_at', date('Y'));
}
public function scopeHasTenOrMoreComments($query)
{
$query->has('comments', '>=', 10);
}
}
With these scopes, we can filter down the records to our needs:
$allPosts = Post::query()->get();
$postsWithSubtitles = Post::query()->hasSubtitle()->get();
$postsFromCurrentYear = Post::query()->fromCurrentYear()->get();
$popularPostsFromCurrentYear = Post::query()->fromCurrentYear()->hasTenOrMoreComments()->get();
You get the idea: scopes make your code extremely readable, and you can do fascinating stuff like querying relationships, advanced subqueries and iterating using cursors. But what if you want to retrieve all records and then want to know whether a post is popular? You can iterate over the collection and determine the popularity, but then you would be recreating the logic from your local scopes. In the example above, this would be something like this:
Post::query()
->withCount('comments')
->get()
->each(function (Post $post) {
$recentlyPopularWithSubtitle = $post->subtitle
&& $post->published_at->isCurrentYear()
&& $post->comments_count >= 10;
});
While this is technically correct and maybe fine for smaller projects, this can become cumbersome in larger projects. That's where this new package comes in!
You only have to install the package using composer
and add the macro to the query builder with the addMacro
method.
composer require protonemedia/laravel-eloquent-scope-as-select
In the boot
method of your AppServiceProvider
:
ScopeAsSelect::addMacro();
This package allows you to re-use your scopes as if it was a select statement. With the addScopeAsSelect
method, you can dynamically add an attribute to your Model, and call the scopes with a Closure
. Let's take a look!
Post::query()
->addScopeAsSelect('recently_populair_with_subtitle', function ($query) {
$query->fromCurrentYear()->hasTenOrMoreComments()->hasSubtitle();
})
->get()
->each(function (Post $post) {
$post->recently_populair_with_subtitle;
});
Instead of rewriting all that logic, you can re-use your scopes in the Closure
, and each Post
Model will have a recently_populair_with_subtitle
boolean attribute. You can add multiple selects, and you can use inline queries as well.
array:2 [
0 => array:6 [
"id" => 1
"title" => "foo",
"subtitle" => null,
"published_at" => "2019-06-01T12:00:00.000000Z"
"recently_populair_with_subtitle" => false
]
1 => array:6 [
"id" => 2
"title" => "foo"
"subtitle" => "bar",
"published_at" => "2020-12-01T12:00:00.000000Z"
"recently_populair_with_subtitle" => true
]
]
The documentation contains more examples, which you can find along with the package on GitHub. I'll do a live demonstration of this package on YouTube, scheduled for December 3 at 14:00 CET.
Update 8 dec: I'm currently working on another method that can flip/invert scopes. It'll probably end up in a new package/repository, but you can already check out the PR on GitHub.
After opening your video file, you can call a new addWatermark
method to open a watermark file. Like positioning elements with CSS, there are top
, right
, bottom
, and left
methods that also support offset values.
FFMpeg::open('lesson-1.mp4')
->addWatermark(function(WatermarkFactory $watermark) {
$watermark->open('logo.png')
->right(25)
->bottom(25);
});
Another way to position your watermark is by using the horizontalAlignment
and verticalAlignment
methods. Both require a constant and support an optional offset as well.
FFMpeg::open('lesson-2.mp4')
->addWatermark(function(WatermarkFactory $watermark) {
$watermark->open('logo.png')
->horizontalAlignment(WatermarkFactory::LEFT, 25)
->verticalAlignment(WatermarkFactory::TOP, 25);
});
You can read the full documentation at the GitHub repository.
I built this feature during a live coding session on YouTube. You can rewatch the whole process if you've missed it, and might even want to subscribe to my new YouTube channel for future sessions and lessons!
Update 26 oct: As of version 7.4, the package supports manipulating watermarks as well.
]]>multipart/form-data
encoded. You can access uploaded files through a Request
instance, and it has helper methods to store files easily. It also has a bunch of rules to validate incoming files. For example, you can verify that the incoming file is an image or matches a MIME type. It even has a convenient rule to validate that the image meets specific dimension constraints.
But what if the input is a base64
encoded string? You might want to upload files from an Android or iOS app that you're developing. Or maybe you're using a JavaScript library to process an image, like cropping, and you want to send the base64 string to your API. You could use the base64_decode()
method and handle the storage from thereon in your controller. Managing the conversion in your controller takes away the ease of Form Request and the file-related methods of the Request object.
Recently I released a small package called Laravel Mixins. It's a set of little helpers for your Laravel application, but every feature is opt-in. This package has a trait that you can add to a Form Request to convert base64 input data to files. Let's take a look at a Form Request that we use to store an image.
use Illuminate\Foundation\Http\FormRequest;
class UploadAvatarRequest extends FormRequest
{
public function rules()
{
return [
'avatar' => ['required', 'file', 'image'],
];
}
}
To use the trait, you need at least Laravel 6.0 and PHP 7.4 or higher. Let's install it using composer.
composer require protonemedia/laravel-mixins
Now we can add the ConvertsBase64ToFiles
trait to the Form Request. The only thing left to do is adding a base64FileKeys()
method to specify which input keys the trait should convert. You should return a simple key-value array. The key matches the input key, and the value is the desired filename since base64 encoded strings only includes the file contents.
use Illuminate\Foundation\Http\FormRequest;
use ProtoneMedia\LaravelMixins\Request\ConvertsBase64ToFiles;
class UploadAvatarRequest extends FormRequest
{
use ConvertsBase64ToFiles;
protected function base64FileKeys(): array
{
return [
'avatar' => 'avatar_cropped.jpg',
];
}
public function rules()
{
return [
'avatar' => ['required', 'file', 'image'],
];
}
}
When you're using the Form Request in your controller, now the base64 encoded data gets validated as well! Calling the file
method on the $request
instance will give you an UploadedFile
instance, just like a file that was uploaded using a multipart/form-data
encoded form.
public function store(UploadAvatarRequest $request)
{
// instance of Illuminate\Http\UploadedFile
$avatarFile = $request->file('avatar');
// avatar_cropped.jpg
$avatarFile->getClientOriginalName();
$path = $request->avatar->store('avatars', 's3');
}
This is just one of the features of the package. It also has Blade Directives, String macros, Validation rules, and other little components. You can find the documentation at the GitHub repository.
]]>Ever since using Tailwind CSS, I've been looking for ways to generate forms as elegant as the Bootforms package. I've found myself using Blade components a lot, and I started to experiment with ways to configure and style forms in different projects. Meanwhile, Blade components got a massive upgrade in Laravel 7. They are now class-based, and you can use a Vue-like syntax.
We're planning to keep using Tailwind CSS, so it was evident that extracting the form elements was gonna some us time in the future. I wanted to incorporate the things I loved about Bootforms as well: validation and model binding. Every project needs a different kind of style, so I wanted to achieve maximum flexibility and straightforward customization. Let's take a look at the core features of this package, which you can find on GitHub:
Imagine a form to update a user's profile, set a tweet of the day, and two dummy checkboxes. In this example, I'll bind the user model to the first and second elements, provide the select element its options, and put the checkboxes in an inline group with a default value.
<x-form class="max-w-lg">
@bind($user)
<x-form-input name="username" label="Your username" />
<x-form-select name="language" :options="$languages" label="Preferred language" />
@endbind
<x-form-textarea name="tweet_of_the_day" placeholder="What's happening?" />
<x-form-group label="Supported frameworks" inline>
<x-form-checkbox name="bootstrap_support" default="true" label="Bootstrap 4" />
<x-form-checkbox name="tailwind_css_support" :default="get_tailwind_support()" label="Tailwind CSS" />
</x-form-group>
<x-form-submit />
</x-form>
The great thing about Blade components is the handling of attributes. The use of PHP expressions and variables (those prefixed with a colon) is a neat way to set defaults and select options. Another great use is localization. To localize a label you could use something like :label="__('user.username')"
. Additional attributes are passed down to the form element, the placeholder
attribute is an example of that, as well as the class
attribute on the x-form
element.
For me, the most essential aspect of this package is the separation of the component logic and the views. The logic handles features like model binding, validation, default data, and old data. There's more than one way to customize the components of this package:
vendor:publish
artisan command and start customizing them from your resources
folder.Below you'll find an excerpt from the configuration file.
return [
'prefix' => '',
'framework' => 'tailwind',
'components' => [
'form-input' => [
'view' => 'form-components::{framework}.form-input',
'class' => \ProtoneMedia\LaravelFormComponents\Components\FormInput::class,
],
...
],
];
But that's not all of it! It supports Laravel Livewire with its real-time validation features, and there is support for the Spatie's popular translation package. There is extensive documentation available, and the package requires no additional dependencies. Check it out on GitHub!
Update January 19, 2021: I'm working on a Pro version of this package!
]]>.env
(tweet)dispatchNow
method (#32559)with
method (#32924)$dates
property on Eloquent modelsfirstOrNew
/ firstOrCreate
without parameters (#33334)Update September 7, 2020
startingValue
method to the Schema Builder (tweet)MissingAppKeyException
when the application encryption key is missing (#34114)$results = Search::add(Post::class, 'title')
->add(Video::class, 'title')
->get('howto');
We've made it really versitile and developer friendly. The example above is simple, but you can do really advanced searches as well:
$results = Search::add(Post::where('views', '>', 500), 'title', 'published_at')
->add(Video::with('tags')->published(), ['title', 'subtitle'], 'released_at')
->startWithWildcard()
->orderByDesc()
->paginate(25)
->get('howto');
We currently only support MySQL 5.7+, but we might add support for other drivers as well. So what can you expect from this package?
The GitHub repository contains extensive documentation of all features. Follow me on Twitter for Laravel tips and package updates!
]]>To boost the platform's development, we worked hard to replace all the custom code with testable, object-oriented code. We found out there was room for a Laravel package that combined the power of FFMpeg with the elegance of Laravel. Funnily enough, we're now researching the possibility of building such a video platform once again, more on that later.
We released the first version in August 2016. It took a few months to gain traction, and at the start of 2017, the first issues and PR's rolled in. We figured most issues were kind of 'beginner' issues. As the underlying package does most of the interaction with the FFMpeg binaries, most questions were about getting started and Laravel related topics as facades and it's filesystem.
Install statistics of pbmedia/laravel-ffmpeg from packagist.org
So I wrote a blog post about the package, and that's when it took off. The blog post is still the most popular of all, and almost four years later, the package has more than 310.000 downloads. It supported every new Laravel and PHP version, and it got a lot of new great features. We've built-in support for HLS exports, opening media from remote sources like Amazon S3 and custom filters.
To overcome some of the long-standing issues and feature requests, we had to rewrite the whole thing. Backward compatibility was essential but not mandatory for every piece of functionality. We're happy there are only a few breaking changes which you can solve in maybe 5-10 minutes. Let's explore some of the most important new features:
Upgrading is straightforward. Bump the dependency in your composer.json
file to ^7.0
and run composer update
. In the README, there is a small section that lists all breaking changes. It also includes examples of all new features. During its development, some have already been using it in production for some time.
One of my favorite new features is support for multiple inputs. This feature makes it easy to combine videos in stacks, or to replace the audio track of a video with something else. We got several feature requests for image sequences, storing frames (stills) to a variable instead of into a file, and better handling of remote sources. You can, for example, open a video from an S3 bucket, combine it with the audio from an FTP-server, and export it to Dropbox.
FFMpeg::fromDisk('s3')
->open('video.mp4')
->fromDisk('ftp')
->open('guitar.m4a')
->export()
->addFormatOutputMapping(new X264, Media::make('dropbox', 'combined.mp4'), ['0:v', '1:a'])
->save();
It also comes with improvements like better HLS playlists. They now contain additional data (bitrate, framerate, and resolution). Also, previously every format of an HLS export was processed separately. After the rewrite, this is just one job. The progress listener is more reliable than before, and now there's a unified way to add it to your export.
As I mentioned before, we're researching the possibility of rolling out a video platform or offering the tools to build one. This product could be a Laravel package or a one-click installer like Wordpress. It might even end up as a service for those of you who don't want to build or spend time setting up such a platform. Here is an overview of some of the features we're thinking about:
We're still debating the feature set and final form, but you could help us out by spreading the word and check out the poll on our website. It has only 1 question and 3 options to choose from, so it'll probably take you just a few seconds. Thanks in advance!
We sincerely hope you'll enjoy this new release! Let us know what you build with it and how the package helps you. You'll find this new release on the GitHub repository, reach out on Twitter, and don't forget to vote at videocoursebuilder.com!
]]>Sending events to @googleanalytics with @laravelphp pic.twitter.com/Y7eljklSkX
— Pascal Baljet (@pascalbaljet) May 6, 2020
The idea is simple. Add the ShouldBroadcastToAnalytics
interface to an event and the package will perform an API call to Google Analytics to track the event. Under the hood we're using the theiconic/php-ga-measurement-protocol
package to make to actual call. There are some additional methods you can add to your event to customize the event, for example, to set the value of the event or to specify a category.
<?php
namespace App\Events;
use App\Order;
use TheIconic\Tracking\GoogleAnalytics\Analytics;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use ProtoneMedia\AnalyticsEventTracking\ShouldBroadcastToAnalytics;
class OrderWasPaid implements ShouldBroadcastToAnalytics
{
use Dispatchable, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
// optional
public function withAnalytics(Analytics $analytics)
{
$analytics->setEventValue($this->order->sum_in_cents / 100);
}
// optional
public function broadcastAnalyticsActionAs(Analytics $analytics)
{
return 'CustomEventAction';
}
}
Google Analytics keeps track of visitors using a Client ID. This identifier is stored in a cookie and Google explicitly tells developers not to parse it themselves. Gladly their JavaScript implementation provides a way to extract the Client ID! This package comes with a Blade Directive that fetches the Client ID, sends it to your Laravel backend (using a POST request) and stores it in the session. Now we can track events with the visitor's Client ID.
The package is really easy to setup, check out the README to get started.
]]>Please note that Laravel 5.8 will not receive security updates after February 26, 2020 and that the previous LTS version (5.5) will only receive security updates until August 30, 2020. It is time to switch to Laravel 6.0 (LTS) or beyond!
]]>What about users who want to update their email address? Shouldn't those be verified as well? By updating the email
column of a User, the new email address could be used immediately. This could be solved by resetting the email_verified_at
column to null
and then sending the user a new verification mail. What if a user mispelled the new email address? What if the verification mail is not delivered for some reason? The development of the new laravel-verify-new-email package was started to solve this problem. We then added a few extra features and eventually it became a drop-in replacement for the built-in solution of the framework.
Long story short, this package ensures that the email
column is not updated until the new email address is verified. This way the user can continue to use the application with its old email address until the new one is verified. Users can verify without being logged in and it even has a setting to automatically log the user in after successfully completing the verification. The package provides a Controller to handle the verification logic and you can fully customize the Mailables and markdown views of the verification mails.
You can simply install the package with Composer and the only thing you have to is add the MustVerifyNewEmail
trait to your User
model. Now you can use the newEmail
method to generate a verification token and send a verification mail to the new email address:
$user = User::create([
'name' => 'John Appleseed',
'email' => 'john@oldcompany.com'
]);
$user->newEmail('john@newcompany.com');
It has three helper methods to help you manage the verification flow:
$user->getPendingEmail(); // returns 'john@newcompany.com'
$user->resendPendingEmailVerificationMail(); // resends the verification mail to 'john@newcompany.com'
$user->clearPendingEmail(); // deletes the token and thereby invalidates the verification mail
If you want to use this package's logic to handle that first verification flow as well, you must override the sendEmailVerificationNotification
method of your User
Model.
<?php
namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use ProtoneMedia\LaravelVerifyNewEmail\MustVerifyNewEmail;
class User extends Authenticatable implements MustVerifyEmail
{
use MustVerifyNewEmail, Notifiable;
public function sendEmailVerificationNotification()
{
$this->newEmail($this->getEmailForVerification());
}
}
There are two separate Mailables and markdown views to handle both this first verification flow as well as the update flow. Check out the documentation to read more about customizing and configuring the package. You can follow me on Twitter to stay up to date on our Laravel packages and other related tweets.
]]>As you might know we are working on a SaaS to monitor and run health checks on Laravel applications. It's called Upserver.online and this project was the main reason to dive into these international tax rules. We've previously used Mollie to handle payments (with great success!) so we started working with the new laravel/cashier-mollie package to handle the subscriptions and payments.
When we found out about the complexity of the digital tax rules we decided to switch to Paddle. Not only do they handle payments, they are actually the service provider to customers. They sell your product or service on your behalf (this is called Merchant of Record (MOR)). Subscriptions, invoicing, taxes, fraud detection, they all have it.
The package provides a fluent interface for the Paddle.com API and it can handle incoming webhooks, it uses events and listeners for maximum flexibility. It also comes with a Blade directive so you can use Paddle in your front-end app. Check out the package documentation to get started and let me know on Twitter what you think about Paddle and the package.
]]>// without tap:
function getServer($id) {
$server = ServerModel::findOrFail($id);
Log::debug('Server was found', $server->toArray());
return $server;
}
// with tap:
function getServer($id) {
return tap(ServerModel::findOrFail($id), function ($server) {
Log::debug('Server was found', $server->toArray());
});
}
Lately we've used a Tappable
trait in one of our projects to avoid the global tap
helper method. This allowed us to call tap
directly on an instance. The example above could be rewritten like this:
function getServer($id) {
return ServerModel::findOrFail($id)->tap(function ($server) {
Log::debug('Server was found', $server->toArray());
});
}
This might look like a small difference but we think it's more readable and it even gets better when the logic increases, for example when you use Eloquent scopes.
// before
tap(ServerModel::active()->paid()->withTrashed()->findOrFail($id), function ($server) {
Log::debug('Server was found', $server->toArray());
});
// after
ServerModel::active()
->paid()
->withTrashed()
->findOrFail($id)
->tap(function ($server) {
Log::debug('Server was found', $server->toArray());
});
We've submitted a PR to make this trait available in the illuminate/support
repository. Luckily it was accepted and is it's now available in Laravel 5.8.17!
For sending notifications it uses Laravel's excellent built-in Notifications component. We've made it really easy to send notifications to Slack and mail but of course you can use every notification channel that Laravel supports. It dispatches events whenever the status of checker changes so you can write listeners and hook into these changes.
It has an Artisan CLI interface to see the status of all your checkers. In addition to that, there are dedicated method so you can fetch the status of a checker in your code as well. Best of all, it has a scheduling feature. You can configure each checker individually within your code so you don't have to worry about managing cron entries. We've built this feature upon Laravel's Task Scheduling options. Furthermore, there is an automatic retry feature and helpers for writings tests.
The GitHub repository contains full documentation of the package. It shows you how to use the built-in checkers, create your own checkers and configure the notification and scheduling options. It supports Laravel 5.6 and requires PHP 7.1 or higher. We're planning to add some additional checkers out of the box, so if you want to stay updated you can follow me on Twitter (@pascalbaljet)!
]]>browser-kit-testing
package.
You can use the rewritten HTTP testing layer in Laravel 5.4+ and the 'old' Laravel 5.3 testing layer side-by-side. What you can't do is mix those two in one test. I wrote a little package which solves that constraint. It contains just one macro which adds a browserKit
method to the TestResponse
class. This way you can make and test requests with the new layer and do assertions with the old layer.
Now why would you do that? Laravel 5.4+ provides a lot of assertions for responses but the old layer has some gems too, for example: seeElement
. Take a look at the Laravel 5.3 TestCase to find some other cool ones. In the example below I'm mixing the two testing layers, you can find the package at GitHub.
/** @test */
public function it_presents_a_registration_form()
{
$this->get('register')
->assertStatus(200)
->browserKit(function ($test) {
$test->seeElement('input', ['name' => 'email']);
});
}
]]>
I'm not digging into writing tests but if you're interested I suggest to take a look at Test Driven Laravel. It's great if you want to learn more about testing! The package itself is compatible with Laravel 5.1 and up but for this blogpost I'll use Laravel 5.4.
What we'll build is a controller that stores an uploaded video and then dispatches two jobs that will process the video. In this example I'll use three different Filesystem disks. One non-public disk to store the original uploaded video, one public disk to store a low-bitrate version of the video and another public disk to store a HLS export to do HTTP streaming. The names of these disks are videos_disk
, downloadable_videos
and streamable_videos
. I'll not dig into the configuration of these disks, you can find it in the Laravel documentation.
First let's generate a Video
model and make sure we create a controller and database migration as well:
php artisan make:model Video --migration --controller
Since this is an example app, the database migration is quite simple.
Schema::create('videos', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->string('original_name');
$table->string('disk');
$table->string('path');
$table->datetime('converted_for_downloading_at')->nullable();
$table->datetime('converted_for_streaming_at')->nullable();
$table->timestamps();
});
Personally I don't use Eloquent's mass-assignment protection so I define the $guarded
property of the Video
model as an empty array and fill the $dates
property with the two datetime
columns.
class Video extends Model
{
protected $dates = [
'converted_for_downloading_at',
'converted_for_streaming_at',
];
protected $guarded = [];
}
Before we can start working on the controller, we need a form request class to validate the user's input. I prefer to keep validation rules out of the controllers but that's just a personal preference. You could perfectly put the validation logic in your controller. Besides the form request class, let's generate two job classes so we can queue some video processing.
php artisan make:request StoreVideoRequest
php artisan make:job ConvertVideoForDownloading
php artisan make:job ConvertVideoForStreaming
Make the authorize
method of the StoreVideoRequest
class returns true (or implement your own authorization logic) and fill the rules
method with the necessary rules. Replace the mime types with the actual types you want to support.
public function rules()
{
return [
'title' => 'required',
'video' => 'required|file|mimetypes:video/mp4,video/mpeg,video/x-matroska',
];
}
Alright now it's time for the controller. We'll use a store
method to upload the video file, save it to the database and dispatch the jobs. The learn more about queues and jobs, please visit the Laravel documentation. In this example I'll return a JSON response containing the ID of the video but of course you can implement your own response. Don't forget to register this route in your routes file!
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreVideoRequest;
use App\Jobs\ConvertVideoForDownloading;
use App\Jobs\ConvertVideoForStreaming;
use App\Video;
class VideoController extends Controller
{
public function store(StoreVideoRequest $request)
{
$video = Video::create([
'disk' => 'videos_disk',
'original_name' => $request->video->getClientOriginalName(),
'path' => $request->video->store('videos', 'videos_disk'),
'title' => $request->title,
]);
$this->dispatch(new ConvertVideoForDownloading($video));
$this->dispatch(new ConvertVideoForStreaming($video));
return response()->json([
'id' => $video->id,
], 201);
}
}
For the first conversion I want a low-bitrate and resized version of the video. Install the FFmpeg package via composer and add the service provider and facade to your config files. Some of the opened issues on GitHub concern Facades. Please read the Laravel documentation about Facades carefully! Also take a look at the laravel-ffmpeg.php
configuration file and especially the binaries
settings. As you can see, the package allows you to chain all the methods but I added some comments to show you what's going on.
<?php
namespace App\Jobs;
use App\Video;
use Carbon\Carbon;
use FFMpeg;
use FFMpeg\Coordinate\Dimension;
use FFMpeg\Format\Video\X264;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ConvertVideoForDownloading implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $video;
public function __construct(Video $video)
{
$this->video = $video;
}
public function handle()
{
// create a video format...
$lowBitrateFormat = (new X264)->setKiloBitrate(500);
// open the uploaded video from the right disk...
FFMpeg::fromDisk($this->video->disk)
->open($this->video->path)
// add the 'resize' filter...
->addFilter(function ($filters) {
$filters->resize(new Dimension(960, 540));
})
// call the 'export' method...
->export()
// tell the MediaExporter to which disk and in which format we want to export...
->toDisk('downloadable_videos')
->inFormat($lowBitrateFormat)
// call the 'save' method with a filename...
->save($this->video->id . '.mp4');
// update the database so we know the convertion is done!
$this->video->update([
'converted_for_downloading_at' => Carbon::now(),
]);
}
}
Now let's create the second job! The beauty of HLS is that you can specify multiple bitrates. Here's a quote from Wikipedia:
To enable a player to adapt to the bandwidth of the network, the original video is encoded in several distinct quality levels. The server serves an index, called a "master playlist", of these encodings, called "variant streams". The player can then choose between the variant streams during playback, changing back and forth seamlessly as network conditions change.
The package handles all the playlist stuff for you. The only thing you have to do is specify the different formats.
<?php
namespace App\Jobs;
use App\Video;
use Carbon\Carbon;
use FFMpeg;
use FFMpeg\Format\Video\X264;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ConvertVideoForStreaming implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $video;
public function __construct(Video $video)
{
$this->video = $video;
}
public function handle()
{
// create some video formats...
$lowBitrateFormat = (new X264)->setKiloBitrate(500);
$midBitrateFormat = (new X264)->setKiloBitrate(1500);
$highBitrateFormat = (new X264)->setKiloBitrate(3000);
// open the uploaded video from the right disk...
FFMpeg::fromDisk($this->video->disk)
->open($this->video->path)
// call the 'exportForHLS' method and specify the disk to which we want to export...
->exportForHLS()
->toDisk('streamable_videos')
// we'll add different formats so the stream will play smoothly
// with all kinds of internet connections...
->addFormat($lowBitrateFormat)
->addFormat($midBitrateFormat)
->addFormat($highBitrateFormat)
// call the 'save' method with a filename...
->save($this->video->id . '.m3u8');
// update the database so we know the convertion is done!
$this->video->update([
'converted_for_streaming_at' => Carbon::now(),
]);
}
}
If you want to stream the HLS export in a browser, take a look at this package. It adds HLS support to the excellent Video.js HTML5 video player, even for browsers that don't support HLS natively. When the processing of the video is done you can easily create URLs of the downloadable and streamable versions:
use Illuminate\Support\Facades\Storage;
$downloadUrl = Storage::disk('downloadable_videos')->url($video->id . '.mp4');
$streamUrl = Storage::disk('streamable_videos')->url($video->id . '.m3u8');
That's it! Check out the GitHub repository to find some more examples and follow me on Twitter (@pascalbaljet) to stay updated!
]]>Besides the Laravel News articles you can also read the full changelog at GitHub!
]]>Collection
class that Laravel provides it also has a Str
class that has lots of great functions to work with strings. Some of these functions are available through helper functions and others you have to find in the API documentation.
Last week we were working with a third party API that doesn't support accented characters as input values. I knew back in the CodeIgniter days there was a convert_accented_characters
function and fortunately the Laravel String class offers something similar. The documentation describes the function as 'Transliterate a UTF-8 value to ASCII' and it does exactly what we wanted:
<?php
use Illuminate\Support\Str;
use PHPUnit\Framework\Assert;
$input = 'á é í ó ú ñ ü';
$converted = Str::ascii($input);
Assert::assertEquals('a e i o u n ue', $converted);
]]>
First we have to install MailHog on your Mac. With brew, which is also used to setup Laravel Valet, this is a breeze. Brew can also start MailHog as a service.
brew install mailhog
brew services start mailhog
With any luck it's now possible to open MailHog's UI in the browser by visiting http://127.0.0.1:8025
. Finally you have to edit your app's mail config. You can do it directly in the mail.php
config file, but I like to keep these settings in the .env
file:
MAIL_DRIVER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
That's it! Now when your app sends a mail, it will be send to the local MailHog service so you can view it the browser. It even has support for notifications!
]]>withValidator
method on a FormRequest
class. If this method exists on your request class, this method gets called with the Validator
instance as first argument. It allows you to interact with the validator instance before the actual validation happens.
Now, why do you want this? You can define your rules in the rules
method right? One of the things you could do is adding 'After Validation Hooks' to the validator. According to the documentation this allows you to attach callbacks to be run after validation is completed. You can read more about this and other cool features of the validator in the documentation.
Here is an example of a request class that has a withValidator
method that adds a hook the validator:
<?php
namespace App;
use Illuminate\Foundation\Http\FormRequest;
class StoreBlogPost extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => 'required|unique:posts|max:255',
'body' => 'required',
];
}
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add('field', 'Something is wrong with this field!');
}
});
}
}
Pretty cool if you ask me! If you're curious where this magic happens, take a look at the getValidatorInstance
method on the FormRequest
class. You can find it in the Illuminate\Foundation\Http
namespace.
New features
contains
method of the Collection class. For example: $users->contains('age', '>=', 18)
.every
method on the Collection class that returns a boolean wether every item passes the given test.retry
helper method that accepts a Closure and lets you specify the number of attempts and the interval between the attempts.Enhancements
contains
, each
, every
, filter
, first
, map
, partition
, reject
, sortBy
, sortByDesc
, and sum
. This lets you write things like $tasks->each->complete()
instead of passing a Closure to the each
method.Route::middleware('auth')->namespace($this->namespace)->prefix('api')->group(...)
.Changes
tinker
Artisan command is no longer included by default but is now an optional package.The release of Laravel 5.5 is expected in July and will require PHP 7.0 or higher. Just like 5.1 it was supposed to be a LTS-release but that's probably not going to happen. Much more information about 5.5 is not available yet. Furthermore, there is a new UI coming to Laravel Forge as well as an official API.
]]>Of course there is the main character of the ecosystem, the Laravel framework. On GitHub you can find two repositories. The first one, named Framework, contains the core code of the framework. The current version (5.3) consists of 29 components. These components all share the Illuminate namespace. Matt Stauffer started a repository named Torch which has examples of how to use these components in non-Laravel applications. This is really helpful but also gives you a great feel of how the components are wired together.
The second repository, simply called Laravel, contains the skeleton that you use when you're building an application. As you might expect, the composer requirements exists of some PHP version and the framework repository. Starting a new project is easy, you can clone this repository and run the composer install
command or use the dedicated Laravel Installer.
But there is more! A third repository called Lumen. This is the super fast, minimal version of Laravel. It shares the same components but not all of them. The main use is to build micro-services and APIs. Take a look at the documentation to see if it fits your project. I started some projects with Lumen but find myself upgrading to Laravel along the path.
The Laravel brand offers two development environments, one runs locally on your Mac and one is a pre-packaged Vagrant box which has virtually everything you need.
For macOS users this is probably the quickest way to get started. Choose a directory in which all your projects are 'parked' and Valet gets your projects, using DnsMasq and Nginx, accessible in the browser. You've probably setup a development environment before or used things like XAMPP/WAMP. Believe me, this is magic. It is super fast and lightweight and does not require Vagrant or you having to mess around with your /etc/hosts
file. You can even share your sites since it supports ngrok tunnels. One thing you have to do manually is setup a database server if you need one. Since Valet is using Homebrew, this is really easy by running brew install mysql
or brew install mariadb
, whatever you like!
The documentation has a section that helps you choose between Valet and Homestead.
This development environment is not limited to macOS, but is fully supported on Linux and Windows as well. It is a Ubuntu-based VM that has all required software installed and configured. It includes PHP 7.1, Nginx, Git, Composer, Node (With Yarn, PM2, Bower, Grunt, and Gulp), Redis, Memcached, Beanstalkd and much more. It uses Vagrant to manage and provision VMs, which can be hosted by VirtualBox (free), Parallels or VMWare (both paid). The Ubuntu VM is mostly compatiable with the configuration that Laravel Forge provisions. What's great about this approch is the ability to destroy a VM and start with a fresh box without having to install and configure everything over and over again. Take a look at the documentation to see what's included and how to get started with this environment.
This packages integrates the PHP League OAuth 2.0 Server package into Laravel which is great for API authentication. It comes with all necessary database migrations, routes, controllers and even Vue components which are of course completely optional. Highly recommended if you're building APIs.
GitHub repository / Documentation
Another Laravel package that makes searching your Eloquent models super easy. It uses Model observers to keep your search indexes in sync. There is a first-party Algolia driver but the community created all sorts of other drivers like Elasticsearch. One of those is the TNTSearch driver which adds full text search support in PHP to your models.
GitHub repository / Documentation
A new tool that has not been released yet but is supposed to be released around the same time as Laravel 5.4 (january 2017). According to Laravel News it is an end-to-end browser testing tool for JavaScript enabled applications. Check out the article on Laravel News to read more about it.
A package that enables your users to authenticate via Facebook, Twitter, Google, LinkedIn, GitHub and Bitbucket and all kinds of other OAuth clients. It is really simple and has great community support. Check out this GitHub repository which offers an enormous amount of drivers.
This time no PHP package but a JavaScript library! Laravel has built-in support for broadcasting events and has support for Pusher and Redis out of the box. To receive these events broadcast you can use Laravel Echo to listen to events, subscribe to channels, and join/leave presence channels. When installing it with NPM it will also install the pusher-js
library for you.
GitHub repository / Documentation
Another JavaScript package which is mostly written by Jeffrey Way, the hero of Laracasts. It makes working with Gulp a lot easier as it comes with a lot of goodies. It supports common CSS and JavaScript pre-processors. It can compile and bundle Less, Sass and Stylus into (minified) CSS files and supports Webpack and Rollup to compile and bundle ECMAScript 2015 into plain JavaScript. It also comes with support for Versioning and Cache Busting and even BrowserSync, which automatically refreshes your browser when you make changes to one of your files. A no-brainer when it comes to front-end development!
GitHub repository / Documentation
This package is made to make subscription billing a lot easier. It offers subscription management and supports both Stripe and Braintree. Besides that, it can handle coupons, swapping subscription, subscription "quantities", cancellation grace periods, and generate invoice PDFs.
GitHub repository / Documentation
Envoy is the only package I've never used myself. It lets you write (common) tasks in a Blade-like syntax that can be executed on remote servers. It has build-in support for notifications through Slack and is only supported on macOS and Linux.
GitHub repository / Documentation
This service lets you manage and provision webservers. It supports AWS, DigitalOcean and Linode but also has support for your own custom VM, as long as it is a fresh Ubuntu Server installation. It configures supervisor for your queues, manages your database server, makes SSL a breeze (support for Let's Encrypt included), built-in support for cronjobs, and even load balancing. This service starts at 15 dollars a month for new users. An absolute no-brainer considering all the hazzle you won't have to deal with. I've been using this service for over a year now and it is my favorite part of the Laravel family.
Laravel Envoyer focuses on zero-downtime deployments. It integrates with GitHub, Bitbucket, self-hosted GitLab servers and can do notifications through Slack. It lets you deploy new versions of your projects to one or more servers with no downtime and support for rollbacks. It also has Application Health Checks and Cron Job Monitoring. Some people wonder why it's not integrated into Forge and I mostly agree. Forge also has deployment features but not as cool as Envoyer, I would be nice to have them in the same UI. Envoyer is lower priced than Forge and starts at 10 dollars a month.
Laravel Spark is a great starting-point for your ideas. This scaffolding package is handcrafted by Taylor to get you started without having to worry about authentication, teams, billing, invoices, subscriptions and user impersonation. It has Two-Factor Authentication support out of the box as well as API authentication. It comes with a installer and an upgrader and is highly customizable. It is the only first-party Laravel package that isn't free but at 99 dollars it saves you a lot of work!
]]>self-update
command (see here in the documentation).
You can read the full changelog on GitHub.
]]>What I want to show you is a little helper we created to echo out monetary values in this Laravel project. First we created a dedicated Service Provider so we could reuse an instance of IntlMoneyFormatter
by binding the object into the container. Then we created a custom Blade directive which uses the binding to format a 'price in cents' to a representation in Euros.
<?php
namespace App;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Money\Currencies\ISOCurrencies;
use Money\Formatter\IntlMoneyFormatter;
use NumberFormatter;
class MoneyServiceProvider extends ServiceProvider
{
public function boot()
{
//
}
public function register()
{
$this->app->singleton('IntlMoneyFormatter', function () {
return new IntlMoneyFormatter(
new NumberFormatter('nl_NL', NumberFormatter::CURRENCY),
new ISOCurrencies
);
});
Blade::directive('CentsToEuros', function ($cents) {
return "<?php echo app('IntlMoneyFormatter')->format(\Money\Money::EUR($cents)); ?>";
});
}
}
Now in your Blade views you can use this directive which is very expressive and clean!
Today's price: @CentsToEuros($product->priceInCents)
Imagine the variable being 1500, this will render into "Today's price: € 15,00".
]]>You can find the source code on GitHub and installing it is easy with Homebrew:
brew install xdebug-osx
# outputs the current status
xdebug-toggle
# enables xdebug
xdebug-toggle on
# disables xdebug
xdebug-toggle off
# toggles xdebug without restarting apache or php-fpm
xdebug-toggle on|off --no-server-restart
By the way, starting from version 1.3.0 (which has not been released yet), Composer will automatically restart PHP without Xdebug to save some time.
]]>UserModel::has('Roles')->get();
UserModel::doesntHave('Roles')->get();
This is really cool but there is more! You can actually add 'where clauses' to the count condition. This is handy if you want to fetch all user with a specific role, for example all admins:
UserModel::whereHas('Roles', function($query) {
$query->where('label', 'admin');
})->get();
UserModel::whereDoesntHave('Roles', function($query) {
$query->where('label', 'admin');
})->get();
I think this is a very elegant solution!
]]>With the latest 10.12.2 update, Apple fixed this with a preference you can set. According to Apple, this behavior can be adjusted using the Terminal:
To disable:
sudo defaults write /Library/Preferences/com.apple.NetworkAuthorization AllowUnknownServers -bool YES
To enable it again:
sudo defaults delete write /Library/Preferences/com.apple.NetworkAuthorization AllowUnknownServers
Installing the application on macOS is easy if you use Homebrew Cask:
brew update
brew cask install hyper
]]>
$ git pull
Enter passphrase for key '/Users/pascalbaljet/.ssh/id_rsa':
Of course you could regenerate your keys and update them across all your servers and services but that's just a lot of work and completely unnecessary. You could use ssh-add
which, according to the man page, adds private key identities to the authentication agent.
$ ssh-add ~/.ssh/id_rsa
Enter passphrase for /Users/pascalbaljet/.ssh/id_rsa:
Now enter your passphrase and you're good to go!
By the way, we use GitLab for collaboration and revision control, the Community Edition of their software is free to use and is absolutely awesome!
Update 23 dec.:
It seems like Apple changed something in macOS Sierra 10.12.2 that makes you enter the passphrase after every restart, they actually added an UseKeychain
option! Open your ~/.ssh/config
file and add this to it:
Host *
UseKeychain yes
Thanks a lot to Jukka Suomela for sharing this solution!
]]>