21torr/rad
RAD Bundle is a Symfony package for rapid application development, bundling commonly used features and 21TORR's opinionated conventions so teams can scaffold and ship applications faster.
21torr/rad is a Symfony bundle, not a Laravel package. If you're a Laravel developer, the day-one decision is whether you're working in a Symfony codebase at all — there is no service provider, no Eloquent integration, and no Laravel facade. If you are in Symfony, it installs as a normal bundle:
composer require 21torr/rad
Register it in config/bundles.php (the bundle ships a RadBundle class), then confirm the runtime constraints before you plan around it:
Where to look first:
21torr-docs.pages.dev/docs/php/symfony/rad — the README is a pointer, not a manual.src/ tree by feature area: controllers (BaseController), API (ApiResponse, ApiResponseNormalizer), entities (EntityModel, field traits), request handling (ArgumentBag, ImportData), security (AbilitiesVoter), and helpers (EnumValue, TranslationHelper, InMemoryCache).First use case: stand up a JSON endpoint. Extend BaseController, pull a typed service via getService(), load an entity through EntityModel, and return an ApiResponse built with ApiResponseNormalizer::createResponse(). That single vertical slice touches the bundle's most-used pieces and tells you quickly whether its opinions fit your app.
Thin controllers with typed service access. BaseController::getService() exists so PhpStorm and PHPStan know the concrete service type instead of the container's object. Use it rather than reaching into $this->container directly.
final class OrderController extends BaseController
{
public function show(int $id): ApiResponse
{
$model = $this->getService(OrderModel::class);
return $this->getService(ApiResponseNormalizer::class)
->createResponse($model->get($id));
}
}
Entity models instead of repository sprawl. EntityModel wraps Doctrine persistence behind persist() (a unified add/edit), update(), and refresh(). The pattern is: one model per aggregate, controllers call the model, the model owns EntityManager calls. update() has an option to skip marking the entity as modified — useful when you're hydrating/refreshing and don't want a flush to fire.
Field traits for boilerplate columns. EntityFieldsTrait (id + created timestamp) and ModifiableEntityFieldsTrait (adds modified timestamp) replace the old IdTrait/TimestampsTrait split from v2. IDs are unsigned integers in the database, and timestamps go through the clock rather than new \DateTime() — so tests that freeze time behave.
API responses as a builder, with explicit status codes. ApiResponse is a builder type; the constructor now requires an explicit status code (passing a bool was deprecated in 3.1.0). Use the user-readable error message added in 3.3.0 for client-facing failures, and EnumValue to serialize backed enums without ad-hoc ->value noise in every resource.
return ApiResponse::create(Status::NotFound)
->withErrorMessage('Order not found');
Request parsing with ArgumentBag and ImportData. ArgumentBag is an array-like argument holder (with ArrayAccess since 3.2.2) for consolidating controller/command inputs. ImportData is the helper for imported payloads: it normalizes empty strings to null and detects rewritable paths more robustly as of 3.4.6. For JSON bodies, malformed input is logged with the raw JSON — check your logs rather than guessing at decode failures.
Abilities with CAN_ attributes. AbilitiesVoter votes on attributes prefixed CAN_, so authorization reads naturally in controllers and templates: $this->isGranted('CAN_EDIT_ORDER', $order). Register your ability→callback mapping as the extension point; don't hand-roll a second voter.
Cross-cutting helpers. TranslationHelper (3.5.0) plus TranslatableInterface support means helpers accept any translatable, not just TranslatableMessage. InMemoryCache is a per-request memoization cache — good for repeated lookups inside one request, wrong for anything that must survive the request.
Links on entities. Implement LinkableInterface (the v2 Routable successor) so entities can generate their own URLs instead of controllers/templates hardcoding route names.
artisan. Doctrine EntityManager, Symfony voters, and Symfony's container are assumed. Porting patterns across frameworks means rewriting, not importing.Model, and Routable; it also renamed the field traits and moved EntityInterface. Minor bumps still change signatures (e.g. ApiResponse constructor). With ~0 dependents, you're on your own for upgrade guides.DoctrineChangeChecker is deprecated (3.4.5) and its internals changed repeatedly around UnitOfWork handling. Don't build new audit logging on it — assume it disappears in the next major.composer why-not before evaluating features.null normalization in ImportData will surprise you if downstream code distinguishes "" from null (unique constraints, NOT NULL columns with defaults). Decide explicitly whether that's what you want.INT columns, and legacy migrations need review before adopting the traits.BaseController public methods are effectively @protected. Custom public methods were flagged as a BC-break deprecation in 2.4.1. Don't call them from outside the controller, and don't add your own public helpers — use services from getService().CAN_ prefix is load-bearing. AbilitiesVoter only votes on attributes with that prefix; a typo in the attribute string silently falls through to a deny (or to another voter's abstain). Verify with a functional test, not by eyeballing.InMemoryCache doesn't cache across requests. If you're chasing performance, don't mistake it for a PSR-6 pool.ApiResponse/EntityModel usage in your own base classes so you can swap it out if maintenance stalls.trigger_deprecation and PHPDoc @deprecated together. Running the test suite with deprecations escalated to errors is the cheapest early warning you'll get.How can I help you explore Laravel packages today?