guava/tutorials
Guava Tutorials is a Laravel package for adding in-app tutorials and guided onboarding flows to your application. Define steps, target users, and track progress through a simple, flexible API, helping new users learn features without leaving your app. It keeps tutorial content organized and easy to...
Install and publish, selectively.
composer require guava/tutorials:^1.1
php artisan vendor:publish --provider="Guava\\Tutorials\\TutorialsServiceProvider"
php artisan migrate
The provider will offer several publish tags (config, migrations, lang, views, assets). Publish config, migrations, and lang immediately. Hold off on views and assets until you have an actual theming requirement — a published Blade view stops receiving upstream improvements the moment it lands in your repo, and you inherit a manual diff on every upgrade.
Read these four things before writing application code, in this order:
config/tutorials.php. The description promises configurable models, routes, and views, so this file is the contract. Model bindings, route prefix/middleware, table names, and the view/component names are almost certainly here. Every customization decision later — custom model, custom route guard, custom overlay — is a config edit rather than a fork. Read it first so you do not accidentally build around hardcoded assumptions.First use case: prove the whole loop on one low-risk flow.
Pick something boring and stable — "create your first project" — with three steps, one overlay target on a page that will not churn, and one completion record. The point of the spike is not the tutorial; it is answering four questions that decide everything downstream:
php artisan migrate produce a schema you are willing to own and query?php artisan config:cache still boot cleanly once a tutorial definition exists (especially one containing a Closure)?Budget one to two developer-days for that loop: install → define one tutorial with ordered steps → render on one authenticated page → complete a step → verify the progress row. Turning the same flow into production content with targeting, theming, and instrumentation is closer to a sprint.
Keep content in data, keep logic in code.
The authored unit is a tutorial composed of ordered steps; each step carries markdown body content plus one or more actions (highlight a target, navigate, dispatch). The healthy split:
The failure mode is expressing tutorial definitions as PHP arrays in a service provider — at that point you have rebuilt the package inside your own repo and lost the ability for a content owner to edit anything.
Put a thin boundary between your app and the package.
This is the highest-leverage pattern in the whole assessment. Do not let controllers and Blade views call vendor models directly. Write one small service that answers "which tutorial, if any, should this user see on this surface right now?":
// Sketch — verify method names against the source.
final class TutorialRegistry
{
public function forUser(User $user, string $surface): ?Tutorial
{
return $this->eligible($user)
->first(fn ($t) => $t->surface === $surface && ! $t->isCompletedBy($user));
}
}
Everything downstream — the layout component, the completion endpoint, your analytics events — talks to that boundary. Audience eligibility depends on your roles, plans, and tenancy model, which the package can never know. Keeping selection in your code means the package stays an implementation detail you can swap or drop.
Use Closure action properties for segmentation, not for stored state.
1.1.0 added Closure support to action property types, and this is the feature that earns the package a place next to a static tour library. The valuable use is one approved content artifact that varies at runtime by plan, role, or tenant:
// Shape, not API contract.
'actions' => [
['type' => 'highlight', 'target' => '[data-tutorial="create-project"]'],
['type' => 'navigate', 'url' => fn (User $u) => $u->isOnTrial()
? route('billing.upgrade')
: route('projects.create')],
],
That converts personalization from a content-duplication problem into a config problem. Two rules: closures live only in PHP files that ship with your code (never persisted to a DB column, never inside a cached config array), and they stay pure — no queries you would not want running on every overlay render.
Record progress idempotently.
Users double-click, they refresh mid-step, they finish a step twice. Completion writes should be firstOrCreate-style no-ops keyed on (tutorial, step, user) rather than inserts. Separately, decide up front what happens when a tutorial is unpublished: prefer a published flag over deletion, because orphaned completion rows are harmless but confusing, and reordering steps silently invalidates existing progress. Retire, do not delete.
Render once, high in the authenticated layout.
Register the overlay component once in your app's authenticated layout and let the registry decide whether anything renders. Per-page registration invites double-mounting and makes "which tutorial wins" unanswerable. The overlay must sit above whatever page it targets, so the layout is the natural home.
Bind to semantic selectors, not CSS classes.
Adopt a convention — data-tutorial="create-project" — and point actions at those attributes instead of classes or DOM structure. Class names are the most volatile thing in a frontend; a dedicated attribute is a contract you can grep for, assert on in CI, and refactor against with confidence.
Instrument from the first PR.
The package renders guidance; it does not measure it. There is no funnel analytics, no A/B variant assignment, and no holdout group in evidence. Fire your own tutorial_shown, tutorial_step_viewed, tutorial_completed, and tutorial_dismissed events through your existing pipeline as part of the pilot. Retrofitting analytics onto onboarding means relaunching to get a baseline.
Three CI checks that pay for themselves:
config:cache in CI with your Closure-bearing definitions present, and an Octane smoke test if you run Octane.Closures are not serializable — this is the headline risk. If a definition containing a Closure ends up inside a config array or a database column, config:cache will fail or silently drop it, and Octane's long-lived workers may hold stale bindings across a deploy. Test this explicitly before you rely on closures anywhere real, and if your release step runs config:cache, make that step part of the spike — not part of launch day.
Selector drift is your risk, not the package's. Overlay tutorials bind to DOM in your application. Every refactor of a target page is a potential silent breakage: no exception, just a tutorial highlighting nothing. Mitigate with the data-tutorial convention and the CI existence check. Consider a step-validity guard in your boundary service that skips a step whose target is absent, rather than rendering a broken overlay.
Debugging "nothing renders" is hard, because the failure mode is silence. No tutorial, no error, and you cannot tell whether it was the audience rule, the published flag, the ordering, an existing completion row, or a missing selector. Log the registry's decision — which tutorial was selected, which were filtered and why — behind a debug flag. That log turns a 40-minute mystery into a one-line answer.
Swapping models is a config edit with consequences. Overriding the tutorial or step model in config is straightforward; keeping your custom model compatible with the package's expectations (table name, ordering scope, relationships) is where people get hurt. Extend the vendor model rather than reimplementing it, and re-read the vendor class after every minor bump.
Route and auth configuration is on you. Tutorials expose content through configurable routes. Check the middleware stack in a fresh config file — it is easy to end up with a tutorial route outside auth, or outside your tenancy scoping, simply because the default did not match your app.
Localization coverage is unstated. Persian exists and an RTL alignment fix shipped as a bug fix in a minor release, which tells you the presentation layer's locale and theme coverage is still maturing. Per-locale key completeness is not documented. Missing keys usually render as raw keys to end users — very visible. Publish the lang files, diff key sets per locale before enabling a locale, and contribute gaps upstream rather than maintaining a permanent override.
Upgrades are deliberate, not automatic. Pin ^1.1 and read each release. Treat the visual layer as a theming pass against your design system rather than a drop-in, and keep published view/asset surface area minimal so upgrades stay cheap.
Maturity and bus factor are real. Single-maintainer agency package, version 1.1.0, effectively no dependent ecosystem, no stated SLA or commercial support path. None of that precludes using it, but it does mean: name an internal owner, budget a few hours a quarter for release review and smoke tests, keep the boundary wrapper so forking is a days-not-months decision, and do not put this on a business-critical surface without a fallback.
Verify, don't assume. Everything above is derived from the 1.1.0 release notes and the package description — no source, composer constraints, or test coverage was available to review. Confirm the actual API surface, PHP/Laravel version constraints, and whether Filament or Livewire is a hard dependency during the spike, before any of these patterns become load-bearing.
How can I help you explore Laravel packages today?