> For the complete documentation index, see [llms.txt](https://vladimirkostikov.gitbook.io/laraboom/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://vladimirkostikov.gitbook.io/laraboom/readme.md).

# LaraBoom

<div align="center"><img src="/files/1rOCM6FD9xGdf5y8Hd0j" alt="LaraBoom" width="420"></div>

<h2 align="center">LaraBoom</h2>

<p align="center"><a href="https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/README.ru.md">Русский</a> · <a href="/pages/HiD0W3DYThD0THQBaW7I">English</a> · <a href="https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/README.de.md">Deutsch</a> · <a href="https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/README.ua.md">Українська</a> · <a href="https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/README.es.md">Español</a></p>

<p align="center"><a href="https://vladimirkostikov.gitbook.io/laraboom"><img src="https://img.shields.io/badge/docs-GitBook-blue" alt="Docs"></a></p>

<p align="center">Code you write on top of Laravel.<br>One entity lives in one Resource (and a Path if you need pages).<br>Schema, JSON CRUD, filters, gates, jobs, and mail come from that description.</p>

### Quick start

```bash
composer create-project laraboom/apps my-app
```

From this monorepo with Docker:

```bash
cp .env.example .env
docker compose up -d --build
docker compose exec php composer setup
```

Open `http://127.0.0.1:8080`. Default host is `apps`.

Or from the monorepo without Packagist:

```bash
./bin/create-project my-app
```

Useful CLI from the host directory (or via Docker php):

```bash
php boom schema|sync|rebuild|doctor|routes|explain|openapi|seed|shell
```

Host tests:

```bash
cd apps && composer test
```

Package tests:

```bash
cd packages/laraboom && composer test
```

### What problem this solves

Laravel is a strong runtime. Eloquent, Blade, queues, validation, and Sanctum stay. The pain is not the framework. The pain is how a typical app spreads one business entity across many files.

For a single `Product` you often end up with:

```
app/Models/Product.php
database/migrations/xxxx_create_products_table.php
app/Http/Controllers/ProductController.php
app/Http/Requests/StoreProductRequest.php
app/Http/Requests/UpdateProductRequest.php
app/Policies/ProductPolicy.php
app/Http/Resources/ProductResource.php
routes/api.php                    (+ lines for the resource)
app/Jobs/RefreshProductCache.php  (optional)
app/Mail/ProductWelcome.php       (optional)
app/Events/ProductCreated.php     (optional)
app/Listeners/LogProductCreated.php
```

That is ten or more files for one noun. Rename a column and you touch the migration history, the model, two FormRequests, the API resource, maybe the policy. Add a filter and you open the controller, a scope, and the route docs. An AI assistant asked to add a `featured` flag on Product must open and keep consistent a pile of files that all describe the same thing in different shapes.

LaraBoom cuts the files you write. You describe the entity once. The package runtime turns that into table sync, `/api/{resource}` CRUD, list filters, gates, and optional side effects.

Laravel is still there. You are not rewriting Laravel. You stop cloning Laravel folder trees for every feature.

### Core idea

A **Resource** owns data and the JSON API. Fields define columns and validation. Attributes define migrate, allow, scopes, hooks, jobs, mail, broadcast.

A **Path** owns web pages. Methods with `#[Get]` / `#[Post]` return Blade views, redirects, forms. Middleware aliases come from `App::through()`.

A host **App** extends `Boom`. It wires middleware names, shared view data, schedule ticks, and custom `php boom` commands.

Reference for how code should look:

* `apps/app/Resources/Demo.php` - full Resource example
* `apps/app/Routes/Demo.php` - Path example
* `apps/app/App.php` - host surface
* [`../agents.md`](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/agents.md) - short reference for agents

### Side by side

| Task                | Usual Laravel                                             | LaraBoom                                           |
| ------------------- | --------------------------------------------------------- | -------------------------------------------------- |
| Create table        | write a migration, keep it in sync with the model forever | `fields()` + `#[Migrate]` + `php boom sync`        |
| Rename column       | new migration + hunt string usages                        | `Text('title')->was('name')` then sync             |
| JSON CRUD           | controller methods + `Route::apiResource`                 | automatic `/api/{resource}`                        |
| Field validation    | FormRequest classes                                       | rules on fields, extra rules via `#[Check]`        |
| List filters / sort | scopes + manual query parsing                             | `->filter()`, `->sort()`, `#[Filter('min_price')]` |
| Authorization       | Policy class + Gate registration                          | `#[Allow('update', 'auth\|owner\|method')]`        |
| Eager load on show  | controller `with()` or API resource nesting               | `#[With('category')]`                              |
| Soft deletes        | trait + migration column                                  | `#[Migrate(softDeletes: true)]`                    |
| Web page            | `routes/web.php` + controller + view                      | Path method + `view(...)` under `web/views`        |
| Rate limit          | middleware on the route group                             | `#[Throttle(60, 1)]` on the method                 |
| Transaction         | `DB::transaction` in the controller                       | `#[Atomic]` on the method                          |
| Job                 | `app/Jobs/...` class                                      | `#[Job]` method on the Resource                    |
| Mail                | `app/Mail/...` mailable                                   | `#[Mail('Subject')]` method returning HTML         |
| Event / listener    | Event + Listener classes                                  | `fire(...)` + `#[On('evt')]` method                |

You still use Eloquent query builder inside Resource methods, Blade, queues, cache, Sanctum, validators, and the container.

### Example: Resource

```php
#[Migrate(softDeletes: true)]
#[With('parent')]
#[Allow('index', true)]
#[Allow('show', true)]
#[Allow('store', 'auth')]
#[Allow('update', 'auth')]
#[Allow('destroy', 'auth')]
final class Product extends Resource
{
    public function fields(): array
    {
        return [
            Text('title', needed: true, max: 255)->sort()->filter(),
            Money('price', currency: 'RUB')->sort()->filter(),
            Number('stock', needed: true, min: 0, starts_as: 0)->sort(),
            OneOf('status', ['draft', 'active', 'archived'], starts_as: 'draft')->filter()->sort(),
            YesNo('featured', starts_as: false)->filter(),
            Upload('cover'),
            Link('parent', to: self::class)->filter(),
        ];
    }

    #[Scope]
    public function hideArchived(Builder $query): void
    {
        $query->where($this->table().'.status', '!=', 'archived');
    }

    #[Filter('min_price')]
    public function minPrice(Builder $query, mixed $value): void
    {
        $query->where('price', '>=', (int) $value);
    }

    #[Created]
    public function afterCreate(Model $product): void
    {
        go([self::class, 'touchCache'], (int) $product->id);
        send([self::class, 'welcomeMail'], $product->email, $product);
    }

    #[Job]
    public function touchCache(int $id): void
    {
        // queued work next to the entity
    }

    #[Mail('Welcome')]
    public function welcomeMail(Model $product): string
    {
        return "<p>Hello {$product->title}</p>";
    }
}
```

After `php boom sync` you get the `products` table (plural snake of the class name). After boot you get:

```
GET    /api/products
POST   /api/products
GET    /api/products/{id}
PUT    /api/products/{id}
PATCH  /api/products/{id}
DELETE /api/products/{id}
```

Query examples without writing a list controller:

```
GET /api/products?status=active&sort=-price&min_price=1000
```

Gates are named `{resource}.{action}`, for example `products.update`.

### Example: Path (web)

```php
final class Shop extends Path
{
    public function __construct(
        private readonly Product $products,
    ) {}

    #[Get('/', name: 'shop.home')]
    public function home(): mixed
    {
        return view('shop.home', [
            'rows' => $this->products->query()->latest('id')->limit(20)->get(),
        ]);
    }

    #[Post('/login', name: 'shop.login', only: 'guest')]
    #[Allow('guest')]
    #[Throttle(10, 1)]
    #[Check([
        'email' => ['required', 'email'],
        'password' => ['required', 'string'],
    ])]
    public function login(Request $request, array $data): mixed
    {
        if (! Auth::attempt($data)) {
            return back()->withErrors(['email' => 'Invalid credentials.']);
        }

        $request->session()->regenerate();

        return redirect()->route('shop.home');
    }
}
```

Views live in `{host}/web/views`. Public entry is `{host}/web/index.php`. There is no host `routes/web.php` and no `app/Http/Controllers` tree.

`only: 'guest'` maps to an alias from the host App:

```php
final class App extends Boom
{
    public function through(): array
    {
        return [
            'auth' => 'auth',
            'guest' => 'guest',
        ];
    }

    public function share(): array
    {
        return [
            'user' => fn () => auth()->user(),
        ];
    }
}
```

### Why the shape is like this

**Attributes over class hierarchies.** Access, throttle, migrate, and hooks sit on the class or method that already owns the behavior. You do not invent parallel Policy / FormRequest / Job trees because that is how stock Laravel apps are laid out.

**Keep logic next to the entity.** Business code sits beside the Resource or Path (`Resources/Product/Service.php` when a method grows). No top-level `app/Services` catch-all.

**Schema from code.** The field list is the source of truth. Sync alters types, nullability, defaults. `->was('old')` renames. `Link` drives foreign keys. Soft deletes are a migrate flag.

**No Laravel-clone folders in the host.** Do not use as the place you write features: host `routes/`, host `config/`, `app/Http`, host `database/`, `bootstrap/app.php`, and top-level `Jobs/`, `Events/`, `Listeners/`, `Mail/`, `Commands/`, `Middleware/`, `Policies/`, `Services/`. The package runtime may use Laravel internals. Your feature code should not mirror those folders.

**Two HTTP worlds on purpose.** JSON is always under `/api`. CRUD and Resource custom actions live there. Blade and sessions live on Paths. One Resource can feed both. A Path injects the Resource and calls `query()`.

### Token cost with AI assistants

What you pay for with an assistant is mostly **input context + output diff**, not prompt wording.

#### 1. Fewer files in context per feature

Laravel task: add a filterable `featured` boolean, only auth may update.

| What the model must read   | Laravel | LaraBoom                      |
| -------------------------- | ------- | ----------------------------- |
| Migration or schema        | yes     | no separate file              |
| Model / casts              | yes     | `fields()` in Resource        |
| Store + Update FormRequest | yes     | field rules                   |
| Controller update + index  | yes     | automatic CRUD + `->filter()` |
| Policy                     | yes     | `#[Allow]`                    |
| API Resource / transformer | often   | present() from fields         |
| Route registration         | yes     | discovery                     |

A typical LaraBoom edit is one Resource file (and maybe one Path if the Blade form changes). The assistant loads about 1-2 files instead of 6-10. Input tokens drop with the file count. More important: less chance the model updates the FormRequest and forgets the migration.

#### 2. Smaller diffs

A Laravel change to one field often produces a multi-file patch. Review and CI noise grow. LaraBoom patches sit in the entity file. Output tokens and review time both shrink.

#### 3. One layout instead of many Laravel habits

Agents drift. One run puts logic in a controller. The next invents a service. The next adds a listener to look tidy.

LaraBoom has one reference: match `Demo`. Agent ingest is short ([`../agents.md`](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/agents.md)). Rules say what not to create. That cuts scaffolding the model would otherwise invent every session.

#### 4. Less repeated boilerplate in prompts

You do not paste "remember FormRequest, Policy, migration, apiResource route" into every task. The runtime already owns that scaffold. The prompt names the field and the allow rule. The model writes attributes and field helpers.

#### 5. Same win for humans

This is not a model trick. A compact write surface helps anyone reading the repo. Token meters just make the cost visible. Same feature, less text through the API.

Scale for one medium feature (field + filter + gate + optional job):

|                 | Laravel-shaped      | LaraBoom-shaped              |
| --------------- | ------------------- | ---------------------------- |
| Files touched   | 5-10                | 1-2                          |
| Context to load | large               | small                        |
| Typical mistake | drift between files | wrong attribute on one class |

Exact numbers depend on the model and the repo. The stable rule: keep the entity in one place, drop the mirror folders.

### Layout of this repo

```
packages/laraboom/   # laraboom/core library
apps/                # main host + Demo
demos/shop/          # shop demo host
demos/blog/          # blog demo host
docker/              # nginx, php, mysql wiring
docs/                # human docs (ru is source) + agents.md
```

Each host has `app/`, `web/`, `boom`, and a Composer path repo to the package. Docker `APP_TARGET=apps` (default) or `demos/shop` / `demos/blog`.

Host DB from the Mac: `127.0.0.1:3307`. Inside the php container `DB_HOST=mysql`.

### Docs

Docs site: <https://vladimirkostikov.gitbook.io/laraboom>

* [Table of contents](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/en/SUMMARY.md)
* [1. Introduction](/laraboom/getting-started/01-introduction.md)
* [2. Installation](/laraboom/getting-started/02-installation.md)
* [4. Resources](/laraboom/data-model/04-resources.md)
* [7. JSON API](/laraboom/http/07-json-api.md)
* [8. Paths](/laraboom/http/08-paths.md)

Other languages: [ru](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/ru/SUMMARY.md) · [de](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/de/SUMMARY.md) · [ua](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/ua/SUMMARY.md) · [es](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/es/SUMMARY.md)

Agent reference: [`../agents.md`](https://github.com/VladimirKostikov/LaraBoom/tree/main/docs/agents.md)

### Requirements

* PHP `^8.3`
* Laravel `^13`
* MySQL 8.4 in the Docker stack (tests often use SQLite `:memory:`)

### License

[MIT](https://github.com/VladimirKostikov/LaraBoom/tree/main/LICENSE/README.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://vladimirkostikov.gitbook.io/laraboom/readme.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
