Code

Small code that preserves design boundaries.

BEAR.Sunday code is centered on resources. Inputs, dependencies, links, and cross-cutting concerns remain visible, allowing the reader to trace the meaning of the application.

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;
}

What this enables

All just the same small declaration — which is why it stays readable.

Resources hold state; representation and transport are separated to the outside. Dependencies are injected; cross-cutting concerns go to AOP. This simple separation becomes the foundation for testing and extension.

  • Tests can be written per resource
  • The same resource can be used as Web API, HTML, and CLI
  • Caching and transactions are separated from business logic
  • Links and schemas make API documentation easy to assemble

Start with one resource

Start small, keep the structure.