Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Embed Helpers Laravel Package

21torr/embed-helpers

A PHP/Symfony library for working with platforms that allow embedded content, such as video services. It provides reusable helpers to simplify common embed-related tasks, making it easier to integrate and handle embeddable content in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check: 21torr/embed-helpers is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no config/embed.php to publish. Composer will install it, Laravel will ignore it, and nothing will work until you write the glue yourself. Budget half a day for that, not five minutes.

In a Symfony app you'd get Flex auto-registration and #[Autowire]-injected services for free. In Laravel you are doing three things manually:

  1. Install and check the constraints.
  2. Instantiate the resolver/providers yourself in a service provider.
  3. Wrap the result in a view layer (Blade component or directive).

Install

composer require 21torr/embed-helpers

Confirm the exact Composer name against vendor/21torr/embed-helpers/composer.json (the repo is 21TORR/EmbedHelpersBundle). Constraints to know before you try: PHP 8.3+ and Symfony 7 (2.1.0 adds Symfony 8). If you're on PHP 8.1/8.2 or a Laravel version pinned to older Symfony components, Composer will fail or you'll get a partial resolution — check composer why-not before debugging anything else.

Where to look first

  • The bundle's src/ for the provider interface and the resolver — that's the whole library; it parses URLs and returns a DTO (confirmed: VideoDetails, with a direct getEmbedUrl() getter since 1.0.1).
  • The docs site referenced in the README (21torr-docs.pages.dev/docs/php/symfony/embed-helpers) — the README itself is four lines.
  • config/services.php / the bundle's DI extension, to see how providers are tagged. That tag is exactly what you need to replicate in Laravel.

First use case: a "video URL" field on a Post

The canonical day-one flow: an editor pastes https://www.youtube.com/watch?v=abc123 (or a youtube.com/shorts/... URL), you store the raw URL, and at render time you resolve it to an embed URL and emit a responsive iframe.

// app/Providers/EmbedServiceProvider.php
public function register(): void
{
    $this->app->singleton(VideoResolver::class, function () {
        // Names/constructor signature vary by version — read src/ once, then lock this in.
        return new VideoResolver([
            new YouTubeProvider(),
            new VimeoProvider(),
        ]);
    });
}
// app/Services/EmbedService.php
final class EmbedService
{
    public function __construct(private VideoResolver $resolver) {}

    public function resolve(string $url): ?VideoDetails
    {
        try {
            return $this->resolver->resolve(trim($url));
        } catch (\Throwable $e) {
            report($e);
            return null;
        }
    }
}
{{-- resources/views/components/embed.blade.php --}}
@props(['url', 'title' => null])

@php($video = app(\App\Services\EmbedService::class)->resolve($url))

@if ($video)
    <div class="aspect-video">
        <iframe
            src="{{ $video->getEmbedUrl() }}"
            title="{{ $title ?? 'Embedded video' }}"
            frameborder="0"
            allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
            allowfullscreen
            loading="lazy"
        ></iframe>
    </div>
@else
    <a href="{{ $url }}" rel="noopener nofollow">{{ $url }}</a>
@endif

That's the whole integration. Everything below is refinement.

Implementation Patterns

Resolve on write, render on read. Don't call the resolver in a loop inside a Blade view for a list of 50 posts. Either persist the resolved fields when the model is saved (provider key + video id + embed URL), or wrap the whole render in Cache::remember("embed:{$post->id}", now()->addDay(), ...). The bundle does pure URL parsing with no network calls, so it's fast — but a Blade partial calling app() 50 times per page is still noise.

Store the parts, not just the HTML. Persisting raw HTML makes later platform changes (new parameters, new provider) unfixable without a data migration. Persist video_provider, video_id, and the source video_url; regenerate the embed URL on read. This is also what makes the youtube-short / videoType breaks below survivable.

Validation rule. Turn resolution into a reusable rule so invalid URLs never reach the database:

final class EmbeddableUrl implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || app(EmbedService::class)->resolve($value) === null) {
            $fail('This URL is not a supported video/embed URL.');
        }
    }
}

Apply it on the FormRequest, not in the controller — it keeps the "which platforms do we support" question in one place.

Custom / missing providers. Provider registration is a tagged-iterator setup in Symfony (switched from #[TaggedIterator] to #[AutowireIterator] in 2.1.0). In Laravel you have two clean options:

  • Manual registry: implement the bundle's provider interface, push it into the array you pass to the resolver in your service provider. Simplest, works today, no Symfony DI involved.
  • Config-driven: put a providers array in config/embed.php, resolve class names with app(), and let non-engineers add a platform by editing config. Useful if marketing wants TikTok next quarter.

Either way, wrap custom providers in tests that assert on a table of sample URLs — that's the boundary that breaks.

Backfill job for existing content. When you add a provider (or upgrade across the 2.0 break), you need to re-resolve existing rows. Write it as a queued job with chunkById, log failures instead of throwing, and make it idempotent — you will run it twice.

Analytics hook. Because the DTO gives you a normalized provider + id, tracking is trivial: store the provider key on the embed record and use it to attribute plays, or to decide whether to swap in a privacy-enhanced domain (youtube-nocookie.com). Don't try to parse the embed URL string later to recover this — capture it at resolve time.

Testing boundary. The resolver is pure, deterministic PHP — no HTTP mocking needed. Test your EmbedService wrapper (trimming, exception → null, cache behavior) rather than the library internals, and add one smoke test per supported provider so a dependency bump that drops a platform fails loudly.

Gotchas and Tips

It is not a Laravel package. No auto-discovery, no facade, no config publish, no Blade directive, no Embed:: static. Everything in the "how to pitch it" style material ({{ embed('...') }}) is aspirational, not shipped API. Write your own ~30-line service provider and facade if you want that ergonomics.

videoType is gone as of 2.0.0. It existed in 1.0.x (VideoDetails::$videoType, required, defaulting to "video", with the YouTube short type renamed from "shorts" to "short" in 1.0.2) and was removed in 2.0.0. If you find tutorial code calling getVideoType(), it's stale. Derive the type from the provider key yourself, or store your own enum column.

youtube-short only exists in 2.0.0+. On 1.0.x, youtube.com/shorts/{id} URLs won't resolve. If short-form video is a real use case, you're pinned to 2.x and therefore to PHP 8.3.

URL trimming was a 1.0.3 fix. Older versions choke on leading/trailing whitespace — which is exactly what happens when users copy-paste from a mobile share sheet or a CMS field with trailing newlines. Either upgrade or trim() before you ever call the resolver (do both; it costs nothing).

Resolution failure is not necessarily null. Depending on the entry point, an unsupported or malformed URL may throw rather than return null. Always wrap the call in try/catch in your service layer, log with the offending URL, and degrade to a plain link in the view. Don't let a bad user-pasted URL 500 a blog post page.

Escape the embed URL like it's user input. It is user input. {{ $video->getEmbedUrl() }} in Blade escapes automatically; {!! !!} does not. If you must build the <iframe …> markup yourself in PHP, run the URL through e() / htmlspecialchars and reject anything that isn't https. The library's job is parsing, not sanitizing your output context.

It does not fetch metadata. No oEmbed, no title, no thumbnail, no duration — it turns a URL into an embed URL. If your UI shows a thumbnail before play, you're calling YouTube/Vimeo APIs yourself (or storing the thumbnail from an upload flow) and this package gives you nothing there. Plan for it rather than discovering it mid-sprint.

Query strings are lossy. Start times (?t=90), playlist IDs, si= share tokens, and list= params are the usual casualties of URL normalizers. Test with the messy real-world URLs your users actually paste, not the canonical clean ones in the README.

Zero stars, zero dependents, one maintainer org. Treat it as source you're vendoring, not infrastructure you depend on. Pin an exact version in composer.json, commit composer.lock, and copy any behaviour you can't live without into your own wrapper so a breaking 3.0.0 doesn't ambush you. Forking is cheap — the surface area is small.

Extension point is the provider tag. If you need a new platform, implement the provider interface and register it; don't patch vendor/. The 2.1.0 change from #[TaggedIterator] to #[AutowireIterator] shows the DI plumbing moves between minor versions, so keep your registration in one file you can update in isolation.

Debugging checklist when a URL "doesn't work": (1) is it trimmed? (2) which provider should match — does it exist in your installed version? (3) does it throw or return null — are you catching both? (4) is www./m./youtu.be short form handled by a different provider than the one you assumed? (5) run the resolver standalone with php artisan tinker before blaming the Blade layer.

Version pinning matrix to keep in mind: 1.0.3+ = PHP 8.3 & Symfony 7; 2.0.0 = drops videoType, adds youtube-short; 2.1.0 = Symfony 8 allowed, unused Console/String/Validator requires dropped. The 2.1.0 cleanup means you can't rely on those Symfony packages arriving transitively — if your glue code used symfony/string, require it explicitly.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator