Where PHP,
Meets Elegance.
Doppar reimagines PHP. Temporal time-travel ORM, Observable Model Properties, multi-actor auth, AI pipelines, frozen services, and attribute-driven architecture — every feature crafted with intention, delivering a cohesive, intuitive DX.
Autonomous LLM
Native integration with self-hosted and cloud transformers.
Aristocratic Syntax
Code that reads like literature, executes like lightning.
Your stack
0 configuration
The Doppar skeleton ships with Vite, Odo layouts, CSRF tokens, and HMR already wired in — the moment you create your project, the frontend is ready.
Choose React, Vue, Svelte, or Vanilla JS during setup. Doppar handles the build pipeline, asset hashing, and production delivery — you just write the UI.
-
1
Create a project with
composer create-project— the frontend stack is included from the start. - 2 Pick your stack during setup — React, Vue, Svelte, or Vanilla JS. Vite and HMR are wired automatically.
- 3 Build — the pipeline handles manifest hashing, compression, and production asset delivery out of the box.
Done before
your coffee cools
The Doppar skeleton scaffolds the client folder, Vite config, Odo layout bridge, and production asset flow. No docs needed.
Any framework,
one flow
React, Vue, Svelte, or Vanilla — the same Doppar pipeline wraps them all. Switch stacks without relearning the tooling.
Already wired in.
Just start building.
Vite powers the build. Odo wraps the shell. Your framework owns the UI. No glue code required.
One command. Everything set up.
Watch Doppar scaffold the project, install the frontend stack, and wire the build pipeline automatically. No loose setup steps. No manual configuration.
Creates the app shell, environment, and backend foundation.
Pick Tailwind, Bootstrap, React, Vue, Svelte, or Vanilla.
Vite, HMR, manifest hashing, and Odo layouts arrive prewired.
Performance, measured under load.
These numbers come from a real Doppar request running on Nginx + PHP-FPM + PHP 8.5, hitting a SQLite-backed route that returns User::find(1).
2103.41
requests/sec at 4 threads and 200 connections
24.16ms
average latency on the 2-thread baseline run
500 conns
sustained near 2k req/s before pressure appeared
Throughput
Requests per second
Latency and saturation
Average latency under concurrency
The goal here is not a hollow hello-world number. This route enters Doppar through the full HTTP layer, resolves the route, hits SQLite through the ORM, and returns a real response.
Full benchmark notesWrite Less, Do More.
From attribute-based routing to built-in AI agents, every Doppar feature is crafted with intention — delivering a cohesive, intuitive DX.
use Doppar\AI\Agent;
use Doppar\AI\AgentFactory\Agent\SelfHost;
$response = Agent::using(SelfHost::class)
->withHost('http://localhost:1234')
->model('local-model-name')
->prompt('Generate a PHP function to validate email')
->send();
use Doppar\AI\Agent;
use Doppar\AI\AgentFactory\Agent\OpenAI;
$stream = Agent::using(OpenAI::class)
->withKey(env('OPENAI_API_KEY'))
->model('gpt-4')
->prompt('Write a short story about a robot learning.')
->withStreaming()
->send();
foreach ($stream as $chunk) {
echo $chunk;
flush();
}
use Doppar\AI\Pipeline;
$order = $orderRepository->find(123);
$isUrgent = Pipeline::query(
item: $order,
question: 'Is this order urgent?'
);
if ($isUrgent) {
// Handle urgent order
}
#[Mapper(prefix: 'user', middleware: ['auth'])]
class UserController extends Controller
{
#[Route(
uri: '/{user}', methods: ['GET'], name: 'user',
middleware: ['is_permitted'],
domain: '{tenant}.app.com'
)]
public function show(#[Model] ?User $user, string $tenant) {}
}
use Phaseolies\Support\Facades\Route;
Route::group(['prefix' => 'api'], function () {
Route::bundle('users', UserController::class);
Route::post('login', [AuthController::class, 'login']);
});
#[Route(uri: '/dashboard', domain: '{tenant}.app.com')]
#[Middleware([TenantMiddleware::class])]
public function dashboard(string $tenant, TenantService $tenant)
{
$settings = $tenant->getTenantSettings($tenant);
return view('tenant.dashboard', compact('settings'));
}
class PostController extends Controller
{
public function __construct(
#[Bind(PostRepository::class)] private PostRepositoryInterface $post
) {}
#[Route('/')]
public function __invoke()
{
return $this->post->all();
}
}
#[Route('/process')]
public function process(
#[Bind(AnotherConcrete::class)] AnotherAbstract $repo
) {
return $repo->execute();
}
$this->app->bind(PaymentInterface::class, StripeProcessor::class);
public function charge(PaymentInterface $processor)
{
}
class PaymentController extends Controller
{
#[Transaction]
#[Route('payment', methods: ['POST'])]
public function __invoke()
{
// Automatically wrapped in a DB transaction
}
}
#[Transaction(attempts: 3, connection: 'mysql')]
public function checkout()
{
$order = $this->createOrder();
$this->processPayment($order);
}
use Phaseolies\Support\Facades\DB;
DB::transaction(function () {
// Complex operations with manual control
});
namespace App\Models;
use App\Watchers\EmployeeStatusWatcher;
use Phaseolies\Database\Entity\Attributes\Watches;
use Phaseolies\Database\Entity\Model;
class Employee extends Model
{
#[Watches(EmployeeStatusWatcher::class)]
protected bool $status;
}
// Watcher
namespace App\Watchers;
use Phaseolies\Database\Entity\Model;
class EmployeeStatusWatcher
{
/**
* Handle the watched property change.
*
* @param mixed $old
* @param mixed $new
* @param Model $model
* @return void
*/
public function handle(mixed $old, mixed $new, Model $model): void
{
info("Employee status changed from '{$old}' to '{$new}' for employee ID: {$model->id}");
}
}
namespace App\Models;
use Phaseolies\Database\Entity\Attributes\Hook;
class Post extends Model
{
#[Hook('before_created')]
public function generateSlug(): void
{
$this->slug = str()->slug($this->title);
}
#[Hook('after_created')]
public function notifySubscribers(): void
{
(new NotifySubscribersJob($this->id))->dispatch();
}
}
#[Queueable(tries: 3, retryAfter: 10, delayFor: 300)]
class SendWelcomeEmailJob extends Job
{
public function __construct(private User $user) {}
public function handle(): void
{
Mail::to($this->user->email)->send(new WelcomeEmail($this->user));
}
}
use Doppar\Queue\Drain;
Drain::conduct([
new Job1(),
new Job2(),
new Job3()
])
->onQueue('priority')
->delayFor(60)
->then(fn() => echo "Done!")
->catch(fn($job, $ex, $index) => Log::error($ex))
->dispatch();
namespace App\Models;
use Phaseolies\Database\Entity\Model;
use Phaseolies\Database\Temporal\Attributes\Temporal;
#[Temporal]
class Contract extends Model
{
protected $creatable = ['title', 'amount', 'client_id'];
}
User::embed([
'posts' => fn($q) => $q->newest()->limit(5),
'profile.address',
'roles.permissions',
])->get();
public function __published($query)
{
return $query->where('status', 'published');
}
$posts = Post::published()->get();
$schedule->command('sync:data')
->atSeconds([0, 15, 30, 45])
->noOverlap();
# Start daemon
php pool cron:run --daemon
php pool cron:daemon start
$schedule->command('health:check')->everySecond();
$schedule->command('data:sync')->throttle('3/1d');
Packages
Doppar ships with elegant solutions for the features modern web apps demand. Our first-party packages handle the hard parts, so you don't have to reinvent the wheel.
Self Hosted LLM or OpenAI integration
A Robust async background task processing
Modern, scalable WebSockets, build real-time applications easily
Local debugging and insights dashboard
Powerfull API Authentication
Multi-channel notification system
Run shell commands with ease
A Powerfull authorization manager
Fluent HTTP client
Social OAuth authentication
Bloom filters - space-efficient probabilistic data structures
Use Twig templates in your views
The documentation
you're reading right now
is built with Doppar.
Every route, every rendered page, every query — this entire site runs on the very framework it documents. A real production application.
