CRUDBooster SOLID Architecture Redesign Specification
1. Executive Summary & Architectural Goals
CRUDBooster is a widely used rapid backend and CRUD generator for Laravel. In version 7.x, the framework has been successfully upgraded to support PHP 8.2–8.5 and Laravel 12/13 while preserving the classic 5.x architecture.
However, the classic codebase suffers from classical architectural debt:
- God Class Anti-Pattern:
CBController.php(1,790 lines) andCRUDBooster.php(1,600+ lines) encapsulate dozens of unrelated responsibilities (session auth, RBAC permissions, database schema introspection, query generation, complex multi-table joins, form parsing, file uploads, image transformations, audit logging, export/import handling, and view rendering). - Hardcoded Static Coupling: Methods rely directly on static facades (
DB::,Session::,Request::,CRUDBooster::), preventing unit testing and dependency injection. - Modification over Extension (OCP violation): Adding a new field type, export format, or custom validation logic currently requires editing large switch/if-else ladders within
CBController.
The Core Objective
Redesign the framework according to SOLID principles and modern Laravel design patterns while preserving 100% backward compatibility for all existing applications, controllers, and modules.
IMPORTANT
Zero Breaking Changes Guarantee:
- Existing module controllers extending
CBControllerand definingcbInit(),$this->table,$this->col,$this->form, andhook_*()will continue to work without changing a single line of code. - Existing static calls to
CRUDBooster::*andCB::*will remain intact, operating as backward-compatible facade proxies to underlying container services. - Existing blade views, routes, database tables, and configuration files will continue to function unchanged.
2. Current Architecture vs. New SOLID Architecture
Current Monolithic Architecture
flowchart TD
UserReq["HTTP Request"] --> Route["CBRouter / routes.php"]
Route --> LegacyCtrl["Legacy Controller (e.g. AdminShopProductsController)"]
LegacyCtrl --> CBController["CBController (God Class - 1,790 lines)\n- Query Builder & Join Logic\n- Form Processing & Sanitization\n- File & Image Uploads\n- Export/Import Orchestration\n- View Presentation"]
CBController <--> CBStatic["CRUDBooster Static Helper (1,600+ lines)\n- Auth & Permissions\n- Schema Inspection\n- Direct DB calls\n- Audit Logs & FCM/Email"]
CBController --> DB[("Database Direct Queries")]Proposed SOLID Layered Architecture
flowchart TD
subgraph Presentation ["1. Presentation Layer"]
HttpReq["HTTP Request"]
LegacyCtrl["Existing Module Controllers\n(AdminProductsController)"]
CBController["CBController (Adapter / Orchestrator)"]
NewResource["Modern CrudController / Fluent API (Optional)"]
end
subgraph FacadeLayer ["Backward-Compatibility Bridge"]
CRUDBoosterFacade["CRUDBooster & CB Static Facades\n(Proxy to IoC Container)"]
end
subgraph Operations ["2. Operations / Actions Layer (SRP)"]
IndexAction["IndexAction"]
SaveAction["SaveAction (Create / Update)"]
DeleteAction["DeleteAction"]
ExportAction["ExportAction"]
ImportAction["ImportAction"]
end
subgraph DomainServices ["3. Domain Services & Contracts (DIP / ISP)"]
GridBuilder["GridQueryBuilderInterface"]
FormProcessor["FormProcessorInterface"]
SchemaInspector["SchemaInspectorInterface"]
PermissionMgr["PermissionCheckerInterface"]
AuditLogger["AuditLoggerInterface"]
end
subgraph Extensibility ["4. Extensibility Plugins (OCP)"]
FieldRegistry["FieldHandlerRegistry"]
UploadField["UploadFieldHandler"]
PasswordField["PasswordFieldHandler"]
RelationField["RelationshipFieldHandler"]
CustomField["CustomFieldHandler (App-defined)"]
ExportDrivers["ExporterInterface (Excel, CSV, PDF)"]
end
subgraph Persistence ["5. Persistence & Infrastructure Layer"]
Models["Eloquent Models\n(CmsUser, CmsModul, CmsPrivilege...)"]
DBLayer["Laravel DB & Schema Connection"]
end
HttpReq --> LegacyCtrl
LegacyCtrl --> CBController
HttpReq --> NewResource
CBController --> Operations
NewResource --> Operations
CRUDBoosterFacade -. delegates .-> DomainServices
IndexAction --> GridBuilder
IndexAction --> PermissionMgr
SaveAction --> FormProcessor
SaveAction --> AuditLogger
ExportAction --> ExportDrivers
ExportAction --> GridBuilder
FormProcessor --> FieldRegistry
FieldRegistry --> UploadField
FieldRegistry --> PasswordField
FieldRegistry --> RelationField
FieldRegistry --> CustomField
GridBuilder --> SchemaInspector
GridBuilder --> DBLayer
FormProcessor --> DBLayer
PermissionMgr --> Models3. SOLID Principles Application Matrix
| Principle | Classic CRUDBooster Problem | Redesigned SOLID Solution |
|---|---|---|
| S - Single Responsibility | CBController and CRUDBooster do auth, schema, query building, validation, file upload, export, import, logging. | Decompose into focused services: SchemaInspector, GridQueryBuilder, FormProcessor, PermissionManager, AuditLogger, Action classes. |
| O - Open / Closed | Adding new field types or export formats requires editing core controller switch/if statements. | Registry & Strategy patterns: FieldHandlerRegistry with FieldHandlerInterface, ExporterRegistry with ExporterInterface. New fields can be added from plugins without touching core files. |
| L - Liskov Substitution | Concrete static methods and classes cannot be swapped, subclassed, or mocked in tests. | Controllers and services depend on interfaces (PermissionCheckerInterface, AuditLoggerInterface, etc.). Custom implementations can replace defaults in the container. |
| I - Interface Segregation | Large god-interfaces or monolithic classes force callers to know about everything. | Micro-interfaces: AuditLoggerInterface, PermissionCheckerInterface, GridQueryBuilderInterface, SchemaInspectorInterface. Consumers depend only on the methods they use. |
| D - Dependency Inversion | High-level modules directly depend on low-level global singletons and static helpers (Session, DB, Request). | High-level operations depend on domain interfaces injected through Laravel's service container. Legacy facades act as lightweight adapters. |
4. Domain Decomposition & Package Structure
The package keeps its lowercase top-level directories (controllers/, helpers/, export/, ...), so the new domains follow the same convention. A capitalized Export/ next to the existing export/ would collide on case-insensitive file systems such as the macOS default.
src/
├── contracts/ <-- [ISP / DIP] Interfaces
│ ├── Actions/CrudActionInterface.php
│ ├── Audit/AuditLoggerInterface.php
│ ├── Export/ExporterInterface.php
│ ├── Files/FileUploaderInterface.php
│ ├── Forms/FieldHandlerInterface.php, FormProcessorInterface.php, FormValidatorInterface.php
│ ├── Generator/ModuleGeneratorInterface.php, ApiGeneratorInterface.php
│ ├── Grid/GridQueryBuilderInterface.php, GridFilterParserInterface.php
│ ├── Mail/TemplateMailerInterface.php
│ ├── Modules/ModuleRegistrarInterface.php
│ ├── Navigation/MenuRepositoryInterface.php
│ ├── Notifications/NotificationSenderInterface.php
│ ├── Schema/SchemaInspectorInterface.php
│ ├── Security/AuthenticationServiceInterface.php, PermissionCheckerInterface.php, PrivilegeRolesInterface.php
│ └── Settings/SettingsRepositoryInterface.php
│
├── models/ <-- Eloquent models for CRUDBooster's own cms_* tables
│ └── CmsMenu.php, CmsModul.php, ... (relations and named query scopes; see Phase 8)
│
├── security/ <-- Auth & RBAC
│ ├── AdminAuthenticationService.php (login, logout, lock screen, password reset)
│ ├── AdminAuthenticationContext.php
│ ├── SessionAuthenticationService.php (implements AuthenticationServiceInterface)
│ ├── PermissionManager.php (implements PermissionCheckerInterface)
│ └── PrivilegeRoles.php (role matrix, session roles; implements PrivilegeRolesInterface)
├── audit/AuditLogger.php (implements AuditLoggerInterface)
├── schema/SchemaInspector.php (columns, primary keys, types, nullability, cached)
│
├── grid/ <-- Index page
│ ├── GridQueryBuilder.php (joins, subqueries, search, where, filters, ordering)
│ ├── GridFilterParser.php (the filter_column URL format)
│ ├── GridQuery.php (query + resolved columns DTO)
│ └── GridRowRenderer.php (cell HTML, row buttons, hook_row_index)
│
├── forms/ <-- Input handling
│ ├── FormContext.php (table, primary key, create/edit, record id)
│ ├── FormProcessor.php (input_assignment and the after-save step)
│ ├── FormValidator.php (validation)
│ ├── FieldHandlerRegistry.php (OCP: type => handler)
│ └── handlers/ BaseFieldHandler, Text, Money, Select, Select2, Checkbox,
│ Multitext, GoogleMaps, Upload, FileManager, Child (+ SyncsPivotTable)
├── files/FileUploader.php (uploads, image resizing, thumbnails)
│
├── actions/ <-- One class per endpoint
│ ├── CrudAction.php (shared permission check / logging)
│ ├── IndexAction.php, FormAction.php (add/edit forms), DetailAction.php, SaveAction.php,
│ ├── DeleteAction.php (+ delete image), BulkAction.php, ExportAction.php, ImportAction.php
│ └── LookupAction.php (find-data, data-table, modal-data)
│
├── export/ <-- Strategy-based exporters (OCP)
│ ├── ExporterRegistry.php, PdfExporter.php, ExcelExporter.php, CsvExporter.php
│ └── DefaultExportXls.php, DefaultImportRows.php (unchanged)
│
├── settings/SettingsRepository.php (cms_settings, cached under the 5.x "setting_{name}" keys)
├── navigation/MenuRepository.php (sidebar and dashboard menus for the current privilege)
├── mail/TemplateMailer.php (email templates, the queue, the SMTP mailer)
├── notifications/NotificationSender.php (cms_notifications)
├── modules/ModuleRegistrar.php (the sidebar menus that come with a module)
├── generator/ <-- Module and API code generators
│ ├── ModuleGenerator.php, ApiGenerator.php
│ └── stubs/module_controller.stub, api_controller.stub
│
├── compatibility/CrudConfig.php <-- Immutable module configuration (props <-> fluent API)
├── modern/ <-- Phase 6 fluent API
│ ├── CrudController.php, CrudDefinition.php, Column.php, Field.php
│
├── controllers/CBController.php <-- Same public API, delegates to actions/
├── helpers/CRUDBooster.php, CB.php <-- Same static API, delegates to contracts
└── CRUDBoosterServiceProvider.php <-- Container bindings5. Backward Compatibility Strategy
How do we guarantee that existing modules like AdminShopProductsController continue working seamlessly without any edits?
1. The Controller Adapter Pattern
In existing code:
class AdminShopProductsController extends CBController
{
public function cbInit()
{
$this->table = 'shop_products';
$this->primary_key = 'id';
$this->col = [...];
$this->form = [...];
}
}In the redesigned CBController:
- All public properties (
$this->table,$this->col,$this->form,$this->button_*, hooks) remain exactly as they are. - In
cbLoader(),CBControllerpackages its configuration properties into an immutableCrudConfigDTO. - Action methods (such as
getIndex(),postAddSave(),getDelete()) simply resolve their corresponding Action class from the container and execute it, passing$thisor the config DTO:
// Inside redesigned CBController.php:
public function getIndex()
{
$this->cbLoader();
$action = app(IndexAction::class);
return $action->execute($this);
}
public function postAddSave()
{
$this->cbLoader();
$action = app(SaveAction::class);
return $action->execute($this, mode: 'create');
}Existing hooks (hook_before_add, hook_query_index, etc.) are invoked by the Action classes via the controller reference, preserving the exact execution lifecycle.
2. The Facade Proxy Pattern for CRUDBooster
Currently, user code and views call static methods: CRUDBooster::myId()CRUDBooster::isCreate()CRUDBooster::insertLog($text)CRUDBooster::getSetting($name)
In the redesigned CRUDBooster.php: The static methods remain, but their internal implementations become one-line delegations to the container interface:
// Inside helpers/CRUDBooster.php:
public static function myId(): ?int
{
return app(AuthenticationServiceInterface::class)->currentUserId();
}
public static function isCreate(): bool
{
return app(PermissionCheckerInterface::class)->canCreate();
}
public static function insertLog(string $description, string $details = ''): void
{
app(AuditLoggerInterface::class)->log($description, $details);
}
public static function getTableColumns(string $table): array
{
return app(SchemaInspectorInterface::class)->getColumnNames($table);
}This transforms CRUDBooster from a God Helper into a convenient Laravel Facade, while business logic resides in testable, injectable services.
6. Detailed Design of Key SOLID Components
A. The Extensible Field Handler System (OCP)
Currently, CBController::input_assignment() has a 200-line switch($ro['type']) with hardcoded cases for upload, password, checkbox, select2, etc.
New SOLID Design:
FieldHandlerInterface:
namespace crocodicstudio\crudbooster\contracts\Forms;
interface FieldHandlerInterface
{
public function supports(string $type): bool;
public function beforeSave(array $field, mixed $value, array &$payload, FormContext $context): void;
public function afterSave(array $field, mixed $value, FormContext $context): void;
}FormContext carries the module's table, primary key, the mode (create/edit) and the record id. The record id alone is not enough, because pivot handlers also need the owning table to find their foreign keys.
- Dedicated Handlers (SRP):
UploadFieldHandler: Handles file validation, disk storage, image resizing, and path generation.PasswordFieldHandler: Handles bcrypt/argon hashing or skipping empty passwords on edit.RelationshipFieldHandler: Handles many-to-many pivot insertions and child table sync.
FieldHandlerRegistry(OCP): Developers or plugins can register custom field types dynamically:
$registry->register('signature', new SignatureFieldHandler());
$registry->register('upload', S3UploadFieldHandler::class); // replace a built-in typeCore controllers no longer need to be modified when new UI field types are introduced.
B. Clean Grid Query Builder (SRP & DIP)
Currently, CBController::getIndex() contains over 400 lines of raw SQL string concatenation, alias generation, and nested join logic.
New SOLID Design:
GridQueryBuilderis responsible purely for taking the table, column specifications, filters, and soft delete settings and returning a prepared Eloquent/Database query builder.- Can be tested in isolation with SQLite in-memory without invoking any HTTP controllers.
namespace crocodicstudio\crudbooster\Contracts\Grid;
interface GridQueryBuilderInterface
{
// Returns the prepared query plus the columns resolved for the views (field, field_with, type_data...).
public function buildQuery(string $table, string $primaryKey, array $columns, array $options = []): GridQuery;
public function applyFilters(Builder $query, array $filterParams): bool;
}C. Permissions & Security (ISP)
Split the auth and permission duties:
AuthenticationServiceInterface: Who is the user? (login, logout, lockscreen, user context).PermissionCheckerInterface: What can the user do in the current module? (canRead(),canCreate(),canUpdate(),canDelete(),canView()).
Controllers and blade directives only ask PermissionCheckerInterface, making it simple to plug in Laravel's native Gates/Policies or Spatie Permission in the future if desired.
7. Migration & Implementation Roadmap
flowchart LR
P1["Phase 1\nContracts & IoC"] --> P2["Phase 2\nSecurity & Audit"]
P2 --> P3["Phase 3\nSchema & Query Builder"]
P3 --> P4["Phase 4\nForm & Field Handlers"]
P4 --> P5["Phase 5\nAction Extraction"]
P5 --> P6["Phase 6\nModern Resource API"]Before Phase 3, tests/Feature/CrudBehaviourTest.php pinned down what a classic module does today. It covers every field type, joins, subquery columns, search, filters, soft deletes, hooks and their order, bulk actions, export and lookups. Those tests passed against the pre-refactor code, except where a bug was fixed on purpose (see section 9), and they pass unchanged after it.
Phase 1: Contracts & Service Container Setup — Implemented.
Phase 2: Security, Permissions & Audit Logging — Implemented.
CRUDBooster::myId(), isSuperadmin(), isView/Create/Read/Update/Delete() and insertLog() delegate to the security and audit contracts.
Phase 3: Schema Inspector & Grid Query Builder — Implemented.
SchemaInspectoralso owns column types and nullability (same cache keys as 5.x).GridQueryBuilderbuilds the whole index query andGridRowRendererthe cell HTML.
Phase 4: Form Processor & Field Handler Registry — Implemented.
validation()andinput_assignment()stay public onCBController, because modules override and call them. They delegate toFormValidatorandFormProcessor.- The after-save loop (pivot tables, child rows) became each handler's
afterSave(). - Uploads moved to
FileUploader.CRUDBooster::uploadFile()anduploadBase64()delegate to it. - Password hashing depends on the field name (
PASSWORD_FIELDS_CANDIDATE), not on thepasswordtype, so it lives inBaseFieldHandlerfor every type instead of in a separatePasswordFieldHandler.
Phase 5: Action Orchestrator Refactoring — Implemented.
CBControllershrank from 1,790 to about 440 lines, and most of the rest are its public properties and hook stubs. Each endpoint callscbLoader()and thenapp(SomeAction::class)->execute($this, ...).- Actions read the controller's live properties and call its hooks and overridable methods (
getIndex(),validation(),input_assignment(),actionButtonSelected()). This keeps the 5.x behaviour of hooks that change$this->arror$this->return_url, and of modules that override one step.
Phase 6 (Bonus): Optional Modern CrudController API — Implemented.
modern\CrudController + CrudDefinition, Column and Field builders. The definition becomes a compatibility\CrudConfig, which is applied to the classic properties, so everything downstream is shared. See docs/en/how-to-extend.md.
Phase 7: Remaining helper domains — Implemented.
The helper still held whole domains after Phase 5. Each now has a contract, and the static method delegates to it:
| Static helper | Contract | Implementation |
|---|---|---|
getSetting() | SettingsRepositoryInterface | settings\SettingsRepository |
sidebarMenu(), sidebarDashboard() | MenuRepositoryInterface | navigation\MenuRepository |
sendEmail(), sendEmailQueue(), mailer() | TemplateMailerInterface | mail\TemplateMailer |
sendNotification() | NotificationSenderInterface | notifications\NotificationSender |
generateController(), getNameTable() | ModuleGeneratorInterface | generator\ModuleGenerator |
generateAPI() | ApiGeneratorInterface | generator\ApiGenerator |
tests/Feature/CodeGeneratorTest.phppins the exact generated source againsttests/fixtures/generated/, because users keep and edit those files. The fixtures were recorded from the pre-refactor generator, and the stubs reproduce them byte for byte. SetCB_UPDATE_GOLDEN=1to re-record them after an intended change.AuditLogger,AdminAuthenticationServiceand themailqueuescommand take these contracts by injection instead of calling the static helper.CRUDBooster.phpwent from 1,735 to about 900 lines. What is left is mostly request context (mainpath(),getCurrentModule(),redirect()), the small query shortcuts (first(),get(),insert()) androuteController().
Phase 8: Laravel models for CRUDBooster's tables — Implemented.
About 100 raw DB::table('cms_…') calls in the built-in controllers, views, router, middleware and helper now go through the models in src/models. The conventions:
- CRUDBooster's own tables (
cms_*) are read and written only through their models. A query that is used in more than one place, or that encodes a rule, is a named scope on the model:CmsMenu::visibleTo(),CmsModul::routable(),CmsNotification::visibleTo()->unread(),CmsEmailQueue::due(). Callers never repeat the SQL. - Steps that span several tables are services behind contracts.
PrivilegeRolesInterfaceowns the role matrix and the session copy of it (five copies of the same query became one).ModuleRegistrarInterfaceowns the menus created with a module (two copies). Services take models by constructor injection; controllers, views and helpers useModel::query()or resolve the contract. - Module data stays on the query builder. A module's own table is passed to hooks,
callback_phpand views that iterate rows, and iterating an Eloquent model yields its properties, not its columns.CBController, the actions and the form components therefore keepDB::table($this->table). - Legacy shapes are kept where user code can see them.
CRUDBooster::me(),getCurrentModule(),myPrivilege()and theadmin_privileges_rolessession key return plain rows through->toBase(), as in 5.x. Login used to store role models there and the refresh plain rows; both now store plain rows. - No
SoftDeletesoncms_moduls. CBController's delete action writesdeleted_atitself and some reads include deleted modules, sonotDeleted()is applied explicitly. - Timestamps stay manual. The models keep
$timestamps = false, and code setscreated_atwhere 5.x did, soupdated_atcolumns are not suddenly written. - Views do not query. The privilege, menu and settings pages get their data from the controller (the menu page eager-loads children and privileges instead of one query per menu). The settings page no longer writes default labels during a GET;
CmsSetting::displayLabel()derives them.
Tests are split into two PHPUnit suites (composer test:unit, composer test):
tests/Unit: one model or service at a time, against the test database: every scope and relation, thePrivilegeRolesandModuleRegistrarservices andSettingsRepository.tests/Feature: whole requests. New tests log in as a non-superadmin (loginAsStandardAdmin()) to cover access control, and cover each bug below. They were checked against the previous code: 8 of the 10 new feature tests fail there, one per bug; the other 2 pin down behaviour that must not change.- The
CoverageCI job runs the suite with PCOV, prints the summary and uploads a Clover report.composer test:coveragedoes the same locally when PCOV or Xdebug is installed.
The seeder keeps its plain inserts: it runs during installation and upgrades, where model behaviour such as casts adds nothing.
Phase 9: A code-first resource layer — Implemented.
Phases 1–8 cleaned up the internals but kept the developer workflow: generate a module from a table, then edit arrays in cbInit(). Phase 9 adds a second front end on the same services, built for developers instead of generation. It lives in its own namespace, CrudBooster\ (core/), so it is free of the 5.x API's constraints while the classic layer stays untouched.
flowchart TD
subgraph Classic ["Classic layer (unchanged)"]
Module["CBController modules\n(cms_moduls rows, cbInit arrays)"]
end
subgraph Panel ["Resource layer (CrudBooster\\)"]
PanelReg["Panel registry\n(discovery, plugins, render hooks)"]
Resource["Resource: Table / Form / Detail\nColumns, Fields, Filters, Actions"]
Page["Page: view + data() / submit()"]
Ctrl["ResourceController / PageController"]
Authz["Authorizer\n(policy, then privileges)"]
end
subgraph Shared ["Shared"]
Auth["Login session, CBBackend"]
Layout["admin_template + sidebar"]
Priv["cms_privileges_roles"]
Audit["AuditLoggerInterface"]
end
Module --> Auth & Layout & Priv & Audit
PanelReg --> Resource & Page
Ctrl --> Resource & Page & Authz & Audit
Ctrl --> Auth
Ctrl --> Layout
Authz --> PrivDesign rules:
- Eloquent first. A resource names a model; relationships, casts, scopes, events and policies work as in any Laravel code. Classic modules keep
DB::table()(Phase 8, rule 3). - Components are classes, not array keys. A column, field, filter or action is an object with autocompleted options and its own Blade view. A new type is a subclass, so no registry has to be edited (the classic layer's
FieldHandlerRegistrystays forcbInit()types). - Closures go through the container. Every option that takes a closure can ask for
$record,$state,$operationor any service by name. - The form is the whitelist. Only declared, visible, enabled fields are read from the request. Rules come from the fields, choices are validated against their options or the related table, and records are saved with
forceFill()->save()in a transaction. - Routes come from classes, not the database. Resources and pages are discovered or registered at boot. Their routes are named and support
route:cache. A slug shared by two classes fails at boot. - Same session, layout and privileges. Resource routes run behind
CBBackend. The admin layout gained an opt-inpage_headersection and two render hooks; the sidebar appends panel links. Without a policy, abilities map onto the classic privilege matrix through the module path (the slug).crudbooster:sync-panelcreates those modules with no controller, soCBRouternever routes them.
| Concern | Classic | Resource layer |
|---|---|---|
| Entry point | CBController + cbInit() | Resource / Page subclasses |
| Registration | cms_moduls row | app/Admin/* discovery, Panel::resources(), plugins |
| Customisation | hook_*(), $this->col / $this->form arrays | Lifecycle methods, component subclasses, render hooks |
| Authorization | PermissionCheckerInterface (privileges) | Authorizer (policy, then privileges) |
| Scaffolding | Module Generator (UI) | crudbooster:make-resource --generate, crudbooster:make-page |
Covered by tests/Feature/PanelTest.php (requests through a resource, a read-only resource, a page and a plugin, as superadmin and as a standard admin), PanelCommandsTest.php and tests/Unit/Panel.
8. Summary of Benefits
- Maintainability:
CBController.phpshrinks from 1,790 lines to ~440, most of it the documented public properties and hook stubs. - Testability: Every component (Query Builder, Form Processor, Field Handlers, Permissions) can be unit-tested independently with mocks.
- Extensibility: Adding custom fields or export drivers requires writing a single class and registering it, without modifying vendor code.
- Reliability: Legacy projects upgrade with zero breaking changes; classic controllers and static helper methods continue to behave identically.
9. Implementation Notes
Deviations from the original plan
- Directory case: lowercase domain directories (see section 4).
- No
CBControllerBridge: each endpoint delegates in one line, so a trait or delegator would only add indirection. CrudConfigis a snapshot, not the actions' input: hooks may change controller properties mid-request, so actions read the controller.CrudConfiglinks the classic properties and the fluent API, andCBController::crudConfig()exposes it read-only.ImportActionandLookupActionexpose one method per step or endpoint instead ofexecute(), because each covers several routes.ExporterInterface::label(): the export dialog lists the registered formats, so custom exporters show up in the UI.AuditLoggerapplies theapi_debug_modecheck thatCRUDBooster::insertLog()used to do, so every caller follows the same policy.
Bugs fixed along the way
Found by the characterization tests or while moving code:
- A
childfield submitted without rows inserted an empty child row, which failed on NOT NULL columns. - The
googlemapsform component crashed with "Array to string conversion" ($row->$form['latitude']parses differently since PHP 7). - The
str_limitcolumn option calledstr_limit(), which Laravel 6 removed. - Audit log entries lost
created_atin the Phase 2 extraction. hide_formwas passed tounserialize()with objects allowed (PHP object injection). It now allows plain values only.- The select2
find-datalookup interpolated the search term into SQL when aformatwas set. It is now a bound parameter. - A bare
uniquerule used a table literally named "unique". It now means the module's table. - A string
$orderbywith a qualified column ("table.column,desc") producedtable.table.column. getIndex()no longer turns a string$orderbyinto an array, which broke a second call on the same instance.- The
where[column]=valueindex parameter used the table of the last listed column instead of the module's table. - XLS/CSV export failed on any cell containing
&or<(found in the playground: the empty-image placeholder URL has a bare&). The export view escapes cell values for these formats. - Import no longer calls
exit()when creating a related row fails; the error surfaces as an exception. - The forgot-password email showed a literal
[password]. The user model was passed as template data, and iterating a model yields its public properties, not its attributes. Models now contribute their attributes. - Generating a module for a table with an
is_*column crashed with "Array to string conversion" (before PHP 8 it wrote"dataenum"=>"Array"). The options are now written as"1|Paid;0|Un-Paid". - A third module on the same table overwrote
Admin{Name}1Controller, and a module generated without a name becameAdmin1Controller. The generator now picks the first free number after the table or module name. - Queued email attachments were passed to
unserialize()with objects allowed. Only plain values are revived now. - Sending an email with a template slug that does not exist failed with "Attempt to assign property on null". It now throws an
InvalidArgumentExceptionthat names the slug. - The sidebar menu queries interpolated the session's privilege id into SQL. It is now a bound parameter.
- On MySQL,
SchemaInspector::columns()readinformation_schemawithoutORDER BY, so the Module Generator could shuffle columns and form fields.
Found while moving the built-in controllers onto models (Phase 8):
- Security: a standard (non-superadmin) admin could list API secret keys, create, disable or delete API keys, generate API controllers (which writes PHP files into
app/Http/Controllers), delete them, read any table's columns through the API generator, change statistic dashboards and delete setting files. Those endpoints now require a superadmin, like the rest of the API generator and statistic builder. - The Module Manager's add form saved the new module's full-access role with a null module id, so the module had no permissions. Its menu group was also not linked to any privilege, so it never appeared in the sidebar.
- Deleting a module with an empty
controllerranLIKE '%%'and deleted every menu. Deleting a module also left its empty menu group behind. - The notification bell listed read broadcast notifications:
id_cms_users = 0 OR (... AND is_read = 0). notifications/read/{id}marked any user's notification as read. It now answers 404 for others' notifications.- "Save as default paper size" on export wrote the setting without clearing its cache, so the old value stayed.
- The privilege edit page ran one query per module, the menu page one per menu, and the settings page wrote missing labels to the database on every view.