Conductor follows a DDD-lite architecture. All domain code lives under app/Domains/{Domain}/.
Single-responsibility classes with one public execute() method. Dependencies via constructor injection.
<?php
declare(strict_types=1);
namespace App\Domains\{Domain}\Actions;
final class DoSomething
{
public function __construct(
private readonly SomeDependency $dependency,
) {}
public function execute(SomeDTO $data): ResultType
{
// Single responsibility
}
}Rules:
- Always
final class - Always
declare(strict_types=1) - Single public
execute()method - Constructor injection for dependencies
- Explicit return type declarations
Immutable data carriers using final readonly class and constructor property promotion.
<?php
declare(strict_types=1);
namespace App\Domains\{Domain}\DTOs;
final readonly class ExampleData
{
/**
* @param array<int, string> $items
*/
public function __construct(
public string $name,
public int $priority,
public array $items = [],
) {}
/**
* @param array{name: string, priority: int, items?: array<int, string>} $data
*/
public static function fromArray(array $data): self
{
return new self(
name: $data['name'],
priority: $data['priority'],
items: $data['items'] ?? [],
);
}
}Rules:
- Always
final readonly class - Always
declare(strict_types=1) - Constructor property promotion
- PHPDoc
@paramgenerics for array types - Include
fromArray()orfromRequest()static factory method
Domain concepts with equality semantics.
<?php
declare(strict_types=1);
namespace App\Domains\{Domain}\ValueObjects;
final readonly class StoryId
{
public function __construct(
public string $prefix,
public int $number,
) {}
public function toString(): string
{
return sprintf('%s-%03d', $this->prefix, $this->number);
}
}Rules:
- Always
final readonly class - Always
declare(strict_types=1) - Provide
toString()or similar display method
Finite state sets with string backing type and TitleCase keys.
<?php
declare(strict_types=1);
namespace App\Domains\{Domain}\Enums;
enum Status: string
{
case Pending = 'pending';
case InProgress = 'in_progress';
case Completed = 'completed';
}Rules:
- Always
stringbacking type - Always TitleCase keys (e.g.,
InProgress, notIN_PROGRESS) - Always
declare(strict_types=1)
Eloquent models with ULID primary keys.
<?php
declare(strict_types=1);
namespace App\Domains\{Domain}\Models;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
use HasFactory, HasUlids;
protected $fillable = ['name', 'path'];
protected function casts(): array
{
return [
'settings' => 'array',
];
}
}Rules:
- Use
HasUlidstrait (Laravel built-in) for ULID primary keys - Use
HasFactoryfor testing - Define
casts()as a method, not a property - Use
Model::query()for queries, notDB::facade
Events that need to reach the NativePHP frontend implement ShouldBroadcastNow on the nativephp channel:
use Illuminate\Broadcasting\Channel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
class SomethingHappened implements ShouldBroadcastNow
{
public function broadcastOn(): array
{
return [new Channel('nativephp')];
}
}Frontend listeners must be inside the native:init handler:
window.addEventListener('native:init', () => {
Native.on('App\\Domains\\Example\\Events\\SomethingHappened', (payload) => {
// Handle event
});
});Browser dev fallback: Use Inertia polling (router.reload() at 1-2s intervals) when running without NativePHP.
- Run
vendor/bin/pint --dirtyafter modifying PHP files - Run
npm run lintafter modifying TypeScript files - Commit format:
feat: {STORY_ID} - {Title}
- Standard jobs dispatch to the
defaultqueue - Long-running agent processes dispatch to the
executionqueue (no timeout)