Sitemap

Doppar 4.0.0 Is Coming — Here’s Everything That’s New, Changed, and Gone

8 min readSep 12, 2026

--

Next doppar release — 4.0.0 — is the biggest one since the framework’s public debut. We’re not sugar-coating it: this is a breaking release. Providers become launchers, Kernel becomes Gateway, the whole skeleton gets reorganized, and the minimum PHP version jumps to 8.5. If you maintain a Doppar application or package, this post is for you.

We wanted to publish this early — a full runway before the release date — so nothing here surprises anyone. Below is the complete picture: what’s landing, what’s changing shape, and what’s being retired outright, followed by a straight migration checklist. If you only read one section, read the Highlights at a Glance table. Everything else is detail.

Highlights at a Glance

Everything in that table gets its own section below, in three groups: new, changed, and removed.

Press enter or click to view image in full size

What’s New in 4.0.0

Launchers, and a Genuinely Lazy Bootstrap. Service Providers are gone, replaced by Launchers — same two-phase lifecycle in spirit register() for bindings, now launch() instead of boot() for everything that depends on other services already being wired up), but with one real capability 3.x never had: ghostable launchers.

A GhostableLauncher doesn't register its services eagerly on every request. It registers them the moment something actually asks for one of its declared services — and never before. If a request never touches your package's functionality, your package's bootstrap cost is zero for that request. This matters more than it sounds: on a large app with a dozen installed packages, most requests only ever touch two or three of them. Ghostable launchers mean the other nine stop taxing every single request just for existing.

namespace App\Launchers;
use Phaseolies\Launchers\ServiceLauncher;
use Phaseolies\Launchers\GhostableLauncher;

class ReportingLauncher extends ServiceLauncher implements GhostableLauncher
{
public function register(): void
{
$this->app->singleton(ReportGenerator::class, ReportGenerator::class);
}
public function launch(): void
{
// runs only once ReportGenerator is actually resolved
}
public function ghosts(): array
{
return [ReportGenerator::class];
}
}

env.toml — Typed Configuration, Not Strings

.env files have one structural limitation every framework built on them eventually runs into: everything is a string. APP_DEBUG=false isn't a boolean, it's the four-character string "false" — and (bool) "false" is true in PHP. That's not a hypothetical; it's a real footgun that's bitten real deployments.

4.0.0 replaces .env.env.example with *env.toml**/*env.toml.example**. Values keep their real type from the file all the way through env():

# env.toml
APP_NAME = "Doppar"
APP_ENV = "local"
APP_DEBUG = false
APP_URL = "http://localhost:8000"
DB_CONNECTION = "mysql"
DB_PORT = 3306

env('APP_DEBUG') now hands back an actual bool. env('DB_PORT') hands back an actual int. No more defensive (bool)(int) casts sprinkled through config files to compensate for a format that couldn't carry types in the first place.

A couple of things behave differently from .env by necessity — strings need quotes now APP_ENV = "local", not bare local), and there's no equivalent of .env's ${VAR} interpolation, since TOML is static data with no substitution syntax. Both are one-time adjustments when you convert a file, not an ongoing cost. And if env.toml isn't present at all, Doppar boots fine and falls back to whatever real OS/server environment variables are already set — handy for containers that inject configuration directly instead of shipping a file.

Attribute-Based DTO Validation

You can now declare validation constraints directly on a DTO’s properties, right next to the property they describe, instead of maintaining a separate validation rule set that has to stay in sync by hand:

namespace App\DTO;

use Phaseolies\Validation\Attributes\Between;
use Phaseolies\Validation\Attributes\Integer;
use Phaseolies\Validation\Attributes\Length;
use Phaseolies\Validation\Attributes\NotBlank;
use Phaseolies\Validation\Attributes\StringType;

final class BookData
{
#[NotBlank]
#[StringType]
#[Length(max: 255)]
public string $title;

#[NotBlank]
#[Integer]
#[Between(min: 1, max: 5)]
public int $rating;
}

Turn it on per-endpoint with validate: true alongside #[BindPayload]:

#[Route(uri: 'books', methods: ['POST'])]
public function store(
#[BindPayload(strict: true, validate: true)]
BookData $book,
) {
Book::create($book->toArray());

return redirect('/books');
}

Ships with #[NotBlank], #[StringType], #[Integer], #[Length], and #[Between] out of the box.

Symfony Mailer

PHPMailer is out; Symfony Mailer is in. This isn’t just a dependency swap — it’s a real upgrade in what mail sending can do. Because Symfony Mailer is DSN-driven, a single MAILER_DSN environment variable can now replace a whole block of host/port/username/password settings, and you get failoverroundrobin transport composition and third-party provider bridges (Mailgun, SES, Postmark, and others) for free once their package is installed. CC/BCC recipients also now genuinely reach the SMTP envelope — no more silent gaps between what PHPMailer showed you and what actually got delivered.

Your Mailable classes don’t change at all. subject(), content(), attachment() — same contract, same make:mail stub.

Semantic Search via doppar/embeds

Doppar 4.x introducesdoppar/embeds, a new package that adds attribute-driven semantic search to your models. Mark a column with #[Embeds], and Doppar keeps a numeric representation of its meaning — a vector — in sync automatically, so Product::whereSimilarTo('description', 'a durable waterproof backpack') finds "rugged daypack for hiking in the rain" even though the two share almost no words.

use Phaseolies\Database\Entity\Model;
use Doppar\Embeds\Attributes\Embeds;
use Doppar\Embeds\Concerns\Embeddable;

class Product extends Model
{
use Embeddable;

#[Embeds]
protected $description;
}

$results = Product::whereSimilarTo('description', 'a durable waterproof backpack', limit: 10);

Embeddings are computed locally via doppar/ai — no external API call, no API key, no per-request cost. Vector storage and ranking is handled by a swappable driver: the zero-setup brute_force driver works on every database Doppar supports, while pgvector and redis push ranking into a real HNSW index for large tables. See the Doppar Embeds documentation for installation, driver configuration, and the full API reference.

What’s Changing

Service Providers → Launchers

Get Mahedi Hasan’s stories in your inbox

Join Medium for free to get updates from this writer.

The core architectural change of this release. Phaseolies\Providers\ServiceProvider is gone; Phaseolies\Launchers\ServiceLauncher takes its place, and the class itself moves from app/Providers to src/Launchers.

// 3.x
namespace App\Providers;
use Phaseolies\Providers\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function register(): void { /* ... */ }
public function boot(): void { /* ... */ }
}
// 4.x
namespace App\Launchers;
use Phaseolies\Launchers\ServiceLauncher;

class AppLauncher extends ServiceLauncher
{
public function register(): void { /* ... */ }
public function launch(): void { /* ... */ }
}

register() means exactly what it always did. launch() takes over everything boot() used to do. The generator command follows suit: php pool make:provider becomes php pool make:launcher.

Kernel → Gateway, and an Inverted Dependency

App\Http\Kernel is now App\Http\Gateway — but the more important part is why. In 3.x, Phaseolies\Support\Router extended your application's Kernel class directly, meaning a core framework class depended on application code by name. In 4.x, Router depends on a new Phaseolies\Http\Contracts\GatewayInterface and resolves your Gateway through the container instead. The framework no longer reaches into your application's namespace to function.

Your $middleware, $middlewareGroups, and $routeMiddleware arrays keep the exact same shape. Rename the file and class, implement GatewayInterface, add three small getter methods getGlobalMiddleware(), getMiddlewareGroups(), getRouteMiddleware()), and you're done — Doppar still finds Gateway by convention, no manual container binding required in the common case.

Directory Structure, Reorganized End to End

The skeleton was restructured around clearer boundaries: application code under src/, views and translations under templates/, database resources under schema/, and configuration, routes, and bootstrapping consolidated under runtime/. bootstrap/ is gone entirely — runtime/app.php does its job now, as a single readable file instead of a scattered folder.

Path Helpers, Renamed to Match

resource_path() and database_path() are renamed to template_path() and schema_path(), matching where those directories actually live now. There's no deprecated alias for either — a leftover resource_path() call throws a fatal error under 4.x, so this one's worth a project-wide search before you flip the switch.

Attribute Namespaces, Organized by Subsystem

Every attribute Doppar ships used to live under one flat Phaseolies\Utilities\Attributes namespace, regardless of what it actually did. In 4.x, each attribute sits next to the subsystem it belongs to — #[Bind] and #[Resolver] under DI\Attributes, #[Route], #[Mapper], and #[Throttle] under Support\Router\Attributes, #[Middleware] under Middleware\Attributes, and so on. The attribute names, signatures, and behavior are all unchanged — this is a use statement update, nothing more, across every controller, model, and middleware class that imports one.

vendor:publish Targets Launchers

Since publishable resources are now declared inside launchers instead of service providers, the identifying flag on vendor:publish changes from --provider= to --launcher=. If you maintain a Doppar package yourself, this is a breaking change for your consumers too — update your install docs and any install scripts that shell out to vendor:publish.

What’s Being Removed

  • Phaseolies\Providers\ServiceProvider** — replaced by ServiceLauncher. No shim, no deprecated alias.
  • App\Http\Kernel** — replaced by App\Http\Gateway, via GatewayInterface.
  • resource_path() / database_path()** — replaced by template_path() / schema_path(), with no fallback. A missed call site is a fatal error, not a silent bug.
  • #[CastToDate]** — removed outright, no 4.x replacement. If a 3.x model used it to format $timeStamps as a date instead of a datetime, both the import and the attribute need to go — there's nothing to swap it for.
  • PHPMailer — replaced by Symfony Mailer. Along with it, the qmail and PHP mail() mail transports are gone; move to smtp, sendmail, or a DSN-based provider bridge.
  • .env / .env.example** — replaced by env.toml / env.toml.example, along with .env's ${VAR} interpolation syntax, which has no TOML equivalent.
  • PHP 8.3 and 8.4 support — 4.x requires PHP 8.5+, full stop. There’s no compatibility mode for older runtimes.

None of these removals ship without a direct 4.x replacement described above — this is a retirement list, not an unfinished-migration list.

Under the Hood: Reliability Work

Alongside the headline changes, this release carries a substantial amount of internal hardening across the request lifecycle, the query builder, the encryption layer, and the dependency container — the kind of work that doesn’t show up in a feature list but shows up in fewer surprises in production. We’re not detailing individual fixes here; full technical write-ups will accompany the release itself. If you’re running a 3.x application in production today, treat the 4.0.0 upgrade as worth prioritizing once it ships, not just a “nice new features” update.

We Want to Hear From You

This post is going out ahead of the release specifically so the community has time to react before anything ships as final. If something here looks like it’ll be painful for your project, or if there’s a rough edge in how a change is described, tell us — open an issue on the framework repository. Package maintainers in the Doppar ecosystem: please start looking at your own vendor:publish flags, provider classes, and attribute imports now, so your packages are ready the same day 4.0.0 is.

See you next month

— The Doppar Team

--

--