Resource
A resource corresponding to a URI decides its state and returns it.
final class Profile extends ResourceObject
{
public function onGet(int $id): static
{
$this->body = $this->profileQuery->item($id);
return $this;
}
}
Dependency Injection
Required dependencies are made explicit in the constructor.
public function __construct(
private readonly ProfileQuery $profileQuery,
private readonly ClockInterface $clock,
) {
}
AOP
Cross-cutting concerns are offloaded to attributes and Interceptors.
#[Transactional]
#[Loggable]
public function onPost(string $name): static
{
$this->body = $this->command->create($name);
return $this;
}
Hypermedia
Declare the next reachable resources as links.
#[Link(rel: 'orders', href: 'app://self/orders{?id}')]
#[Embed(rel: 'profile', src: 'app://self/profile{?id}')]
public function onGet(int $id): static
{
return $this;
}
CacheableResponse
Cache is invalidated by a dependency change, not by elapsed time.
use BEAR\RepositoryModule\Annotation\CacheableResponse;
#[CacheableResponse]
public function onGet(string $id): static
{
$this->body = $this->blog->entry($id);
return $this;
}
DonutCache
Cache the page; do not cache the comment “hole” — fresh every request.
#[DonutCache]
#[Embed(rel: 'comment', src: 'page://self/blog/comment')]
public function onGet(int $id): static
{
$this->body += ['article' => '...'];
return $this;
}
CLI
The same resource also becomes a CLI with one attribute.
use BEAR\Cli\Attribute\Cli;
use BEAR\Cli\Attribute\Option;
#[Cli(name: 'greet', description: 'Greet in many languages', output: 'greeting')]
public function onGet(
#[Option(shortName: 'n')] string $name,
#[Option(shortName: 'l')] string $lang = 'en',
): static {
// this onGet becomes a standalone 'greet' command
// --help and --name/-n are generated from the declaration
return $this;
}
SQL
SQL stays SQL — pass types, receive types; even the clock is injected.
interface OrderRepositoryInterface
{
// the return type is the intent: an immutable domain object
#[DbQuery('order_item', factory: OrderFactory::class)]
public function getOrder(string $id): Order;
// typed argument; the clock is injected; return type = affected rows
#[DbQuery('order_close')]
public function close(string $id, DateTimeInterface $at): AffectedRows;
}