# Builder Package - Architecture Documentation

> PHP/Laravel architecture. For the JavaScript editor architecture (ContentBox layer, RTE, grid system, plugins), see [ARCHITECTURE-JS.md](./ARCHITECTURE-JS.md). For the 3.x → 4.x migration notes, see [UPGRADE-4.x.md](./UPGRADE-4.x.md).

## Table of Contents

1. [Overview](#overview)
2. [Directory Structure](#directory-structure)
3. [Class Architecture](#class-architecture)
4. [Data Flow](#data-flow)
5. [Storage Architecture](#storage-architecture)
6. [Route Architecture](#route-architecture)
7. [Frontend Asset Pipeline](#frontend-asset-pipeline)
8. [Testing Architecture](#testing-architecture)
9. [Extension Points](#extension-points)
10. [How to Add Features](#how-to-add-features)

## Overview

The **bongo/builder** package is a Laravel visual page builder that provides:

- **713 pre-built blocks**: 427 components (16 categories), 266 designs (16 categories), 20 layouts (3 categories)
- **File-based storage** — no database tables; every block is a directory holding `index.blade.php` + `preview.png`
- **Vendor/resource override pattern** — customisations shadow vendor originals without modifying package files
- **Admin CRUD interface** for all three block types plus an icon browser
- **API endpoints** feeding the WYSIWYG editor (authenticated) and rendered page content (public)
- **A bespoke vanilla-JS editor** — a fully modularised ContentBox.js v4.1 derivative (156 ES modules)
- **Shortcode support** — `[company_*]` setting placeholders parsed by response middleware
- **2,045 FontAwesome icons** (163 regular, 1,392 solid, 490 brand) configured in `src/Config/builder.php`

### Key Design Principles

1. **Single model, multiple types** — one `BuilderItem` class handles component, design, and layout polymorphically via a `$type` constructor argument
2. **File-based storage** — the filesystem IS the database; a 10-minute cache makes it fast
3. **Override, never mutate** — writes always target the application's `resources/views/vendor/builder/` path; vendor files are read-only at runtime
4. **Facade access** — `Component`, `Design`, `Layout` global aliases over container singletons
5. **Zero-config bootstrapping** — `AbstractServiceProvider` (bongo/framework) auto-registers config, routes, views, and translations

## Directory Structure

```
bongo/builder/
├── src/
│   ├── BuilderServiceProvider.php    # Singletons, facade aliases, asset publishing
│   ├── Config/
│   │   └── builder.php               # Paths, prefixes, categories, icons (2,125 lines)
│   ├── Models/
│   │   └── BuilderItem.php           # Core file-based polymorphic model
│   ├── Facades/
│   │   ├── ComponentFacade.php       # accessor: 'component'
│   │   ├── DesignFacade.php          # accessor: 'design'
│   │   └── LayoutFacade.php          # accessor: 'layout'
│   ├── Exceptions/
│   │   ├── ComponentException.php
│   │   └── DesignException.php
│   ├── Http/
│   │   ├── Controllers/
│   │   │   ├── Api/                  # 22 controllers feeding the editor
│   │   │   │   ├── ButtonController.php      SnippetController.php
│   │   │   │   ├── CarouselController.php    SliderController.php
│   │   │   │   ├── ComponentController.php   StyleController.php
│   │   │   │   ├── DesignController.php      SymbolController.php
│   │   │   │   ├── FileController.php        ValidationController.php
│   │   │   │   ├── FontController.php        PostController.php
│   │   │   │   ├── FormController.php        ProjectController.php
│   │   │   │   ├── GalleryController.php     QuestionController.php
│   │   │   │   ├── IconController.php        ReviewController.php
│   │   │   │   ├── ImageController.php       ReviewSummaryController.php
│   │   │   │   ├── MenuController.php        ModuleController.php
│   │   │   └── Backend/              # Admin CRUD
│   │   │       ├── ComponentController.php
│   │   │       ├── DesignController.php
│   │   │       ├── LayoutController.php
│   │   │       └── IconController.php
│   │   └── Middleware/
│   │       └── HasShortCodes.php     # Shortcode parsing (response middleware)
│   ├── Routes/
│   │   ├── api.php                   # 11 authenticated endpoints
│   │   ├── backend.php               # Admin CRUD + icon browser
│   │   └── custom.php                # 11 public endpoints
│   ├── Seeders/
│   │   ├── DataSeeder.php
│   │   ├── ComponentSeeder.php
│   │   └── DesignSeeder.php
│   ├── Translations/en/backend.php
│   └── Views/
│       ├── api/                      # Blade fragments returned by API controllers
│       │   ├── icons.blade.php  fonts.blade.php  snippets.blade.php  ...
│       │   └── modules/
│       ├── backend/                  # Admin UI (component/, design/, layout/)
│       └── frontend/                 # THE BLOCK LIBRARY
│           ├── component/{category}/{name}/{index.blade.php, preview.png}
│           ├── design/{category}/{name}/...
│           ├── layout/{category}/{name}/...
│           └── {carousels,posts,projects,questions,sliders}/style_1..5.blade.php
│                                     # Module style fragments (public custom.php endpoints)
├── resources/
│   ├── backend/
│   │   ├── js/                       # Editor source (see ARCHITECTURE-JS.md)
│   │   │   ├── editor.js             # Entry point
│   │   │   ├── EditorDefaults.js  AssetLoader.js  helpers.js
│   │   │   ├── shared/               # 9 framework-free helpers
│   │   │   └── contentbuilder/       # grid/ rte/ elements/ sections/ ui/
│   │   │                             # plugins/ config/ core/ openai/ utils/
│   │   └── sass/editor.scss
│   ├── frontend/
│   │   ├── sass/box.scss
│   │   └── js/box.js
│   └── vendor/                       # jquery.appear.js, skrollr.js, aos.js
├── public/                           # Compiled assets (published: builder:assets)
│   ├── backend/  (editor.js, editor.css, helpers.js, icons, snippets, ...)
│   └── frontend/ (box.js, box.css)
├── tests/
│   ├── browser/                      # Playwright browser suite
│   ├── vitest/                       # vitest unit suite
│   ├── Unit/                         # PHPUnit, no application booted
│   ├── Feature/                      # PHPUnit, Testbench application via TestCase
│   └── TestCase.php                  # Testbench base — Feature only, aliases noIndex
├── scripts/lint-css-selectors.mjs    # Custom CSS selector linter
├── webpack.mix.js                    # Laravel Mix build definition
├── playwright.config.js              # Browser test config
└── composer.json / package.json
```

## Class Architecture

### Class Diagram

```
┌─────────────────────────────────────────────────────────────────┐
│              AbstractServiceProvider (bongo/framework)          │
│  Auto-registers: config, routes (+prefixes/middleware), views,  │
│  translations, $middlewares, $commands, $listeners, $composers  │
└────────────────────────────┬────────────────────────────────────┘
                             │ extends
┌────────────────────────────▼────────────────────────────────────┐
│                     BuilderServiceProvider                      │
│  $module = 'builder'                                            │
│  $middlewares = ['hasShortCodes' => HasShortCodes::class]       │
│  register() ─► registerBindings():                              │
│    singleton 'component' ──► new BuilderItem('component')       │
│    singleton 'design'    ──► new BuilderItem('design')          │
│    singleton 'layout'    ──► new BuilderItem('layout')          │
│    AliasLoader: Component / Design / Layout                     │
│  boot() ─► bootAssets(): publishes public/ (tag builder:assets) │
└─────────────────────────────────────────────────────────────────┘

┌──────────────────┐   accessor    ┌──────────────────────────────┐
│ ComponentFacade  │──'component'──►                              │
│ DesignFacade     │──'design'─────►      BuilderItem($type)      │
│ LayoutFacade     │──'layout'─────►  (plain class, no Eloquent)  │
└──────────────────┘               └──────────────┬───────────────┘
                                                  │ reads/writes
                              ┌───────────────────▼─────────────────────┐
                              │  Filesystem (vendor + resource paths)   │
                              │  + Cache ("{type}_files", TTL 600s)     │
                              └─────────────────────────────────────────┘
```

### BuilderItem — Public API

| Group | Method | Returns | Notes |
|---|---|---|---|
| Retrieval | `all()` | `Collection` | Cached under `{type}_files`, 600s |
| | `allByCategory()` | `array` | `[category][key] => BuilderItem`, ksorted |
| | `allAsJson()` | `false\|string` | name/thumbnail/category/whitespace-stripped html |
| | `categories()` | `array` | slug ⇒ name, from `builder.{type}_categories` |
| | `categoriesAsJson()` | `false\|string` | `[{name, key}]` |
| | `find($key)` | `mixed` | `firstWhere('key', $key)` |
| | `findOrFail($key)` | mixed | `abort('404')` on miss |
| | `findByCategory($category)` | `Collection` | |
| Persistence | `save()` | `void` | Writes to resource path; clears cache |
| | `delete()` | `void` | Deletes resource copy only; clears cache |
| Hydration | `createFromFile(SplFileInfo)` | `BuilderItem` | Category/name from relative path segments |
| Paths | `getBasePath()` | `string` | Vendor path; throws `Exception` if missing |
| | `getResourcePath()` | `string` | Auto-creates if missing (0755, recursive) |
| | `getAllFiles()` | `array` | `array_replace(vendor, resource)` |
| | `getVendorFiles()` / `getResourceFiles()` | `array` | `.php` files keyed by relative path |
| Content | `getHtml()` | `?string` | Rendered via `view('builder::frontend.{type}...')` |
| | `getRawHtml()` | `?string` | Raw file contents of the resolved view |
| | `getThumbnail()` | `?string` | Base64 data URI; `public/images/default_image.png` fallback |
| | `getRawThumbnail()` | `?string` | Binary PNG |
| Setters | `setName()` / `setCategory()` | `void` | `trim(strtolower(Str::slug(...)))` |
| | `setKey()` | `void` | Via the `make_key()` helper |

The private `isExtended()` check makes `save()` idempotent about location: an item loaded from the vendor path gets its absolute paths repointed to the resource path before writing.

### HasShortCodes Middleware

Response middleware (alias `hasShortCodes`, registered via `$middlewares`):

```
Request ─► $next() ─► Response
                        │ has content() method?  ──no──► return untouched
                        ▼ yes
          ShortCode::parse($response->content())     (bongo/framework helper)
                        │  replaces [company_phone] [company_name] [company_email]
                        │  [company_contact] [registered_address] [office_address]
                        ▼
          $response->setContent($content) ─► return
```

## Data Flow

### Block discovery and rendering

```
Component::all()
    │
    ├─► Cache hit "component_files"? ──yes──► Collection
    │
    └─► getAllFiles()
          ├─ getVendorFiles()    vendor/bongo/builder/src/Views/frontend/component/**
          └─ getResourceFiles()  resources/views/vendor/builder/frontend/component/**
          array_replace() — resource shadows vendor (keyed by "{category}/{name}")
    │
    └─► createFromFile() per file ─► Collection ─► cached 600s

$item->getHtml()
    └─► view('builder::frontend.component.{category}.{name}.index')->render()
        (Laravel's vendor view resolution applies the same override rules)
```

### Block save (admin)

```
POST /admin/components/{key}/update
    └─► Backend\ComponentController::update()
          └─► BuilderItem: setName/setKey/setCategory/setHtml/setThumbnail
                └─► save()
                      ├─ repoint paths to resource dir (unless already extended)
                      ├─ File::makeDirectory() if needed
                      ├─ File::put(preview.png), File::put(index.blade.php)
                      └─ Cache::forget('component_files')
```

### Editor feed (authenticated API)

```
Editor JS (public/backend/editor.js)
    └─► GET /api/builder            ─► ComponentController@index  (all components as JSON)
    └─► GET /api/builder/snippets   ─► SnippetController@index    (Blade fragment: src/Views/api/snippets.blade.php)
    └─► GET /api/builder/icons      ─► IconController@index       (2,045 icon names from config)
    └─► GET /api/builder/images ... (see Route Architecture)
```

### Public content endpoints (custom.php)

Rendered pages embed dynamic content by calling the public endpoints (`post`, `review`, `form-builder`, `menu-builder`, `gallery-builder`, `carousels/{carousel}`, ...) which integrate the bongo/post, bongo/question, bongo/gallery, bongo/menu, bongo/form and custom/project packages. All carry the `noIndex` middleware. The post/project/question/carousel/slider controllers render one of five style fragments (`style_1..5.blade.php`) from `src/Views/frontend/{carousels,posts,projects,questions,sliders}/` — e.g. the FAQ accordion styles output WYSIWYG HTML answers via `{!! !!}` (DTB-597).

## Storage Architecture

```
VENDOR (read-only originals)                RESOURCE (writable overrides)
base_path(                                  base_path(
  'vendor/bongo/builder/src/Views/'          'resources/views/vendor/builder/'
  + 'frontend/component/')                    + 'frontend/component/')
        │                                           │
        └────────────── array_replace ──────────────┘
                       (resource wins)
```

- Config keys: `vendor_path`, `resource_path`, `component_path`, `design_path`, `layout_path`
- Each block: `{category}/{name}/index.blade.php` + `{category}/{name}/preview.png`
- `delete()` removes ONLY the resource copy — deleting a customised block reverts it to the vendor original
- Cache keys: `component_files`, `design_files`, `layout_files` (600s; forgotten on save/delete)

## Route Architecture

Prefixes and middleware for `api.php`/`backend.php` are applied by `AbstractServiceProvider`; `custom.php` is registered with no session middleware and prefixes itself.

### api.php — `/api/builder/*`, `auth:sanctum`, names `api.builder.*`

| Endpoint | Controller | Purpose |
|---|---|---|
| `GET /` | Api\ComponentController | All components (editor palette) |
| `GET buttons` | Api\ButtonController | Button styles |
| `GET designs` | Api\DesignController | All designs |
| `GET snippets` | Api\SnippetController | Snippet panel content |
| `GET files` | Api\FileController | File manager |
| `GET fonts` | Api\FontController | Font list |
| `GET images` | Api\ImageController | Image library (bongo/image) |
| `GET icons` | Api\IconController | FontAwesome icon browser |
| `GET symbols` | Api\SymbolController | Symbol palette |
| `GET styles` | Api\StyleController | Style presets |
| `GET modules/{module}` | Api\ModuleController | Module templates (src/Views/api/modules/) |

### backend.php — `/admin/*`, `auth` + `employee`, names `backend.*`

Three identical CRUD groups, prefixes from config:

| Group | Prefix (config) | Routes |
|---|---|---|
| `component.*` | `builder.component_prefix` = `components` | `index, create, store, {key}/show, {key}/edit, POST {key}/update, DELETE {key}/delete` |
| `design.*` | `builder.design_prefix` = `sections` | same shape |
| `layout.*` | `builder.layout_prefix` = `layouts` | same shape |
| `icon.*` | `/icons` | `index` (icon browser; config comment: use it to refresh the icon lists) |

### custom.php — public, `noIndex`, prefix `config('settings.api_prefix', 'api')`, names `api.builder.*`

| Endpoint | Controller | Integrates |
|---|---|---|
| `GET carousels/{carousel}` | CarouselController | Carousel render |
| `GET sliders/{slider}` | SliderController | Slider render |
| `GET validation` | ValidationController | Client-side validation rules |
| `GET post` | PostController | bongo/post listings |
| `GET project` | ProjectController | custom/project portfolio |
| `GET question` | QuestionController | bongo/question FAQs |
| `GET review` / `GET review-summary` | Review(Summary)Controller | Reviews |
| `GET form-builder` | FormController | bongo/form |
| `GET menu-builder` | MenuController | bongo/menu |
| `GET gallery-builder` | GalleryController | bongo/gallery |

## Frontend Asset Pipeline

`webpack.mix.js` (Laravel Mix 6):

| Source | Output | Notes |
|---|---|---|
| `resources/backend/sass/editor.scss` | `public/backend/editor.css` | Editor chrome styles |
| `resources/backend/js/editor.js` | `public/backend/editor.js` | ES-module bundle (151 modules) |
| `resources/backend/js/helpers.js` | `public/backend/helpers.js` | Standalone helpers bundle |
| `resources/frontend/sass/box.scss` | `public/frontend/box.css` | Published block styles |
| `resources/vendor/{jquery.appear,skrollr,aos}.js` + `resources/frontend/js/box.js` | `public/frontend/box.js` | Concatenated via `mix.scripts()` |

- `npm run dev` / `npm run prod` run `npm run lint` first: ESLint over `resources/backend/js` plus the custom selector linter `scripts/lint-css-selectors.mjs`
- Compiled assets are published to consumer apps via `php artisan vendor:publish --tag=builder:assets --force`
- Asset paths/filenames are env-overridable: `BACKEND_JS_FILE`, `BACKEND_CSS_FILE`, `FRONTEND_*`, `VENDOR_ASSET_PATH`, ...
- JS runtime dependencies: `sortablejs` (drag-drop), `moveable`, `rangeslider-pure`

The editor internals (two-layer ContentBox/ContentBuilder design, RTE event bus, grid system, plugin system, iframe-hosted panels) are documented in [ARCHITECTURE-JS.md](./ARCHITECTURE-JS.md).

## Testing Architecture

| Layer | Tooling | Location | Status |
|---|---|---|---|
| Unit | Vitest + jsdom | `tests/vitest/` — runs in seconds, no consumer site needed (`npm run test:unit`, which prints the counts) | Fast regression net |
| Browser | Playwright 1.59 | `tests/browser/` — `npx playwright test` prints the counts; organised across core/, elements/, grid/, rows/, columns/, rte/, modals/, panels/, plugins/, sections/, modules/, ui/ | Full-stack suite |
| PHP | PHPUnit 10 + Orchestra Testbench 8 | `tests/Feature/`, `tests/Unit/`, `tests/TestCase.php` | `tests/Unit/` needs no app; `tests/Feature/` boots one via `TestCase` |
| Static | PHPStan (larastan) | `phpstan.neon.dist` | `vendor/bin/phpstan analyse` |
| Style | Pint / ESLint / selector linter | `pint.json`, `.eslintrc.json`, `scripts/` | Lint runs on every JS build |

Playwright configuration (`playwright.config.js`): reads `.env.testing` (`TEST_BASE_URL`), single worker, `retries: 1`, 60s test timeout, shared login state in `storageState.json` created by `tests/browser/global-setup.js`, chromium project, screenshots on failure, trace on first retry. Helpers live in `tests/browser/helpers/` (auth.js, editor.js, assertions.js).

## Extension Points

1. **Override any block** — copy it under `resources/views/vendor/builder/frontend/{type}/{category}/{name}/` in the consumer app, or edit via the admin UI (which does exactly that)
2. **Add categories** — extend `builder.{type}_categories` in config (publish/override the config in the consumer app)
3. **Swap asset files** — env vars (`BACKEND_JS_FILE` etc.) let a consumer serve its own editor build
4. **Admin URL prefixes** — `component_prefix`, `design_prefix`, `layout_prefix` config keys
5. **Shortcodes anywhere** — apply the `hasShortCodes` middleware alias to any route that should expand the `[company_*]` setting placeholders
6. **Editor plugins** — JS `PluginManager` loads plugins from `public/backend/plugins/` (see ARCHITECTURE-JS.md § Plugin System)
7. **Editor defaults** — `EditorDefaults.js#buildEditorDefaults()` centralises the option surface consumed by consumer apps

## How to Add Features

### A new block

1. `src/Views/frontend/{type}/{category}/{name}/index.blade.php` (+ `preview.png`, ~1-2KB)
2. Nothing to register — discovered on next uncached `all()`; `php artisan cache:clear` (or forget `{type}_files`) to see it immediately

### A new category

1. Add to `{type}_categories` in `src/Config/builder.php`
2. Create `src/Views/frontend/{type}/{category-slug}/` and add blocks

### A new editor API endpoint

1. Controller in `src/Http/Controllers/Api/` with an `index()` action
2. Route in `src/Routes/api.php` (editor-only, authenticated) or `src/Routes/custom.php` (public page content)
3. Blade fragment in `src/Views/api/` if returning HTML
4. Wire the fetch into the relevant JS module under `resources/backend/js/contentbuilder/`, rebuild with `npm run dev`

### A new admin screen

1. Controller in `src/Http/Controllers/Backend/`
2. Routes in `src/Routes/backend.php` (named group, config-driven prefix)
3. Views in `src/Views/backend/{feature}/`
4. Labels in `src/Translations/en/backend.php`

### An editor behaviour change

1. Locate the subsystem via [ARCHITECTURE-JS.md](./ARCHITECTURE-JS.md)
2. Edit under `resources/backend/js/`, rebuild (`npm run dev`)
3. Add/extend a Playwright spec in `tests/browser/` and run `npm test`
4. Republish assets in the consumer app to verify end-to-end
