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

Written by / Original link on Oct. 13, 2020

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.

This post is a part of a series where we explore the source code of which you'll find in this repo on GitHub
  • Part 6: Building a video section using Vimeo (you are here)

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;
<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(Vimeo $vimeo)</span>
    <span class="hljs-keyword">$this</span>-&gt;vimeo = $vimeo;

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">execute</span><span class="hljs-params">(Video $video)</span>: <span class="hljs-title">Video</span>
    $video-&gt;withoutEvents(<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-params">()</span> <span class="hljs-title">use</span> <span class="hljs-params">($video)</span> </span>{
        $vimeoVideo = <span class="hljs-keyword">$this</span>-&gt;vimeo-&gt;getVideo($video-&gt;vimeo_id);

        $slug = Str::slug($vimeoVideo[<span class="hljs-string">'name'</span>]);

            <span class="hljs-string">'slug'</span> =&gt; $slug,
            <span class="hljs-string">'title'</span> =&gt; $vimeoVideo[<span class="hljs-string">'name'</span>],
            <span class="hljs-string">'description'</span> =&gt; $vimeoVideo[<span class="hljs-string">'description'</span>],
            <span class="hljs-string">'runtime'</span> =&gt; $vimeoVideo[<span class="hljs-string">'duration'</span>],
            <span class="hljs-string">'thumbnail'</span> =&gt; $vimeoVideo[<span class="hljs-string">'pictures'</span>][<span class="hljs-string">'sizes'</span>][<span class="hljs-number">1</span>][<span class="hljs-string">'link'</span>],

    <span class="hljs-keyword">return</span> $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<span class="hljs-title">Services<span class="hljs-title">Vimeo;

use GuzzleHttp<span class="hljs-title">Client;

class Vimeo { private Client $client;

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span><span class="hljs-params">(Client $client)</span>
    <span class="hljs-keyword">$this</span>-&gt;client = $client;

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getVideos</span><span class="hljs-params">()</span>: <span class="hljs-title">array</span>
    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;get(<span class="hljs-string">''</span>, [
        <span class="hljs-string">'query'</span> =&gt; [
            <span class="hljs-string">'per_page'</span> =&gt; <span class="hljs-number">100</span>,

    $data = json_decode($response-&gt;getBody()-&gt;getContents(), <span class="hljs-keyword">true</span>);

    <span class="hljs-keyword">return</span> $data[<span class="hljs-string">'data'</span>];

<span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getVideo</span><span class="hljs-params">(string $vimeo_id)</span>: <span class="hljs-title">array</span>
    $response = <span class="hljs-keyword">$this</span>-&gt;client-&gt;get(<span class="hljs-string">"{$vimeo_id}"</span>);

    <span class="hljs-keyword">return</span> json_decode($response-&gt;getBody()-&gt;getContents(), <span class="hljs-keyword">true</span>);


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;
<span class="hljs-keyword">if</span> (! auth()-&gt;check()) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;

<span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;display === VideoDisplayEnum::AUTH) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;

$userOwnsSeries = <span class="hljs-keyword">$this</span>-&gt;series-&gt;isOwnedByCurrentUser();

<span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;display === VideoDisplayEnum::SPONSORS) {
    <span class="hljs-keyword">return</span> auth()-&gt;user()-&gt;isSponsoring() || $userOwnsSeries;

<span class="hljs-keyword">if</span> (<span class="hljs-keyword">$this</span>-&gt;display === VideoDisplayEnum::LICENSE) {
    <span class="hljs-keyword">return</span> $userOwnsSeries;

<span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;


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.


« ★ Selling digital products using Laravel part 7: Importing package documentation from GitHub - ★ Selling digital products using Laravel part 5: Using Satis to install private packages »