Selling digital products using Laravel part 6: Building a video section using Vimeo

Original – by Freek Van der Herten – 5 minute read

On our website, we have a video section. All our videos uploaded to and handled via Vimeo where we have a Pro subscription. We chose Vimeo because it has an excellent widget to display videos, it converts our videos very fast, and it has a nice API to work with. Let's take a look at how Vimeo is integrated in our site.

Videos on our site are grouped per series, such as "Laravel Beyond CRUD", "Laravel Package Training", ... When clicking on a series, you'll see the outline of that series. Some of the videos can be viewed for free. Other videos require you to have purchased the product the series belongs to. Other videos can be viewed when sponsoring us. Let's take a look at how we built this.


All our videos are uploaded on Vimeo where we have a Pro subscription. We chose Vimeo because it has an excellent widget to display videos, it converts our videos very fast, and it has a nice API to work with.


At Vimeo, for each uploaded video, we input a title and a description. In the description field, we use markdown. On, that markdown will be rendered as HTML. For each video, we specify that only people with the private link can view it. This will have the videos have an unguessable URL. At we'll retrieve the right URL using the API.

Adding videos using Nova

Let's take a look at how we integrate with Vimeo. At we built a Laravel Nova powered admin panel. In the video section, we can add a new video by specifying the Vimeo video id. We can also specify who can view the video.


Let's take a look at how it works under the hood. In the booted method you'll see this line that will execute UpdateVideoDetailsAction each time a Video model gets saved.

static::saved(fn (Video $video) => app(UpdateVideoDetailsAction::class)->execute($video));

This is the entire UpdateVideoDetailsAction class.

class UpdateVideoDetailsAction
    private Vimeo $vimeo;

    public function __construct(Vimeo $vimeo)
        $this->vimeo = $vimeo;

    public function execute(Video $video): Video
        $video->withoutEvents(function () use ($video) {
            $vimeoVideo = $this->vimeo->getVideo($video->vimeo_id);

            $slug = Str::slug($vimeoVideo['name']);

                'slug' => $slug,
                'title' => $vimeoVideo['name'],
                'description' => $vimeoVideo['description'],
                'runtime' => $vimeoVideo['duration'],
                'thumbnail' => $vimeoVideo['pictures']['sizes'][1]['link'],

        return $video;

In that class above, we use the Vimeo class to fetch and update our local Video model with the title, runtime, thumbnail, and more. This code is wrapped in withoutEvents. This is necessary because otherwise, the update inside this class would trigger UpdateVideoDetailsAction again, and we'd get stuck in an infinite loop.

That Vimeo class above is a plain wrapper around the Vimeo API.

namespace App\Services\Vimeo;

use GuzzleHttp\Client;

class Vimeo
    private Client $client;

    public function __construct(Client $client)
        $this->client = $client;

    public function getVideos(): array
        $response = $this->client->get('', [
            'query' => [
                'per_page' => 100,

        $data = json_decode($response->getBody()->getContents(), true);

        return $data['data'];

    public function getVideo(string $vimeo_id): array
        $response = $this->client->get("{$vimeo_id}");

        return json_decode($response->getBody()->getContents(), true);

Displaying videos

In the videos/show blade view, a video is displayed. Here is the part where the Vimeo player is rendered.

@if ($currentVideo->canBeSeenByCurrentUser())
    <iframe id="player" class="absolute inset-0 w-full h-full"
            src="{{ $currentVideo->vimeo_id }}?loop=false&amp;byline=false&amp;portrait=false&amp;title=false&amp;speed=true&amp;transparent=0&amp;gesture=media"
            allowfullscreen allowtransparency></iframe>

We just use the standard Vimeo player embed that gets passed the vimeo_id of the video that should be displayed.

On the Video model, we store which audience is allowed to see the video in the display attribute. The model has canBeSeenByCurrentUser method that determines if the video can be seen by the currently logged in user.

public function canBeSeenByCurrentUser(): bool
    if ($this->display === VideoDisplayEnum::FREE) {
        return true;

    if (! auth()->check()) {
        return false;

    if ($this->display === VideoDisplayEnum::AUTH) {
        return true;

    $userOwnsSeries = $this->series->isOwnedByCurrentUser();

    if ($this->display === VideoDisplayEnum::SPONSORS) {
        return auth()->user()->isSponsoring() || $userOwnsSeries;

    if ($this->display === VideoDisplayEnum::LICENSE) {
        return $userOwnsSeries;

    return false;

If this function does not allow a user to see a video, we display a friendly message informing the user what needs to be done to be able to see the video.

Marking a video as completed

Each video can be marked as completed. In this video, I explain how that works under the hood. How meta!

This series is continued in part 7: Importing package documentation from GitHub.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.



Nicolas Giraud liked on 14th October 2020
Robin Dirksen liked on 13th October 2020
Richard Radermacher liked on 13th October 2020