Pular para o conteúdo principal

Coming from Laravel, Symfony or Drupal

Gluo is a REST API starter, not a full framework. Most of what you know transfers — PSR-4, Composer, PHPUnit, controllers, repositories, services, a DI container. This page maps the vocabulary and calls out the handful of places where Gluo genuinely works differently, so you do not lose an afternoon to a surprise.

If you only read one section, read The router is generated below. It is the single most common source of "why does my endpoint 404".

The four real differences​

1. The router is generated — attributes are the source, JSON is the truth​

Routes are not read from your PHP classes at runtime. public/app.php hands OpenApiRouteList the file public/docs/openapi.json, and that file is the routing table. It is produced from the #[OA\...] attributes on your controllers.

#[OA\Get(path: "/project/{id}", tags: ["Project"])]
#[RequireAuthenticated]
public function getProject(HttpResponse $response, HttpRequest $request): void

So adding or changing an endpoint is a two-step operation:

# 1. edit the controller attributes
# 2. rebuild the routing table
composer openapi

Skip step 2 and the endpoint returns 404 with no error — the route simply does not exist yet. To catch this, composer test runs a freshness check first, and you can run it any time:

composer openapi:check

It warns (never blocks) when the spec is older than anything in src/Controller/ or src/Model/.

Laravel: there is no routes/api.php. Symfony: it resembles #[Route], except the "cache rebuild" is explicit and you own it. Drupal: it plays the role of mymodule.routing.yml, except you never edit it by hand.

2. Every binding is explicit — there is no autowire-by-convention​

Constructor injection works the way you expect. Controllers declare what they need and the container provides it:

class ProjectController
{
public function __construct(protected ProjectService $projectService)
{
}

public function getProject(HttpResponse $response, HttpRequest $request): void
{
$response->write($this->projectService->getOrFail($request->attribute('id')));
}
}

Controllers need no registration — one pattern rule covers the namespace:

// config/dev/07-controllers.php
'App\Controller\*' => Autowire::rule()
->withInjectedConstructor() // resolve constructor args from their type hints
->toInstance(), // per-request, not shared

Services and repositories are declared one by one, because those bindings carry decisions a pattern cannot make — which implementation, singleton or not, scalar constructor arguments:

// config/dev/05-services.php
ProjectService::class => DI::bind(ProjectService::class)
->withInjectedConstructor()
->toSingleton(),

So there is no services.yaml-style autowiring for the layers where a choice exists, and no per-class boilerplate for the layer where none does. composer codegen writes the service and repository entries for what it generates.

config/test/ inherits config/dev/, so you register once for both.

Symfony: the controller rule is your App\Controller\: resource: line; the difference is that services get no equivalent — declare each one. Laravel: think explicit $this->app->bind() for services, with controllers resolved for you. Drupal: like mymodule.services.yml, except constructor arguments resolve from type hints and controllers need no entry.

3. Controllers write to a response, they do not return one​

The signature is fixed and the return type is void:

public function listProject(HttpResponse $response, HttpRequest $request): void
{
$response->write($result); // arrays and objects are serialized for you
}

No return response()->json(...), no return new JsonResponse(...), no render arrays.

4. The database comes first, and migrations are plain SQL​

The code generator reads your existing table and writes the PHP:

composer codegen -- --env=dev --table=product all --save

That emits the model, repository, service, controller, functional test and DI bindings. Migrations are hand-written numbered SQL pairs — you write both directions:

db/migrations/up/00002-create-products.sql
db/migrations/down/00001-rollback-products.sql

Laravel: the direction is reversed. You are used to model → migration → table; here it is table → codegen → model. There is no schema builder. Symfony: closer to doctrine:mapping:import than to make:entity. Drupal: replaces hook_schema() and hook_update_N().

Vocabulary​

LaravelSymfonyDrupalGluo
routes/api.php#[Route]*.routing.yml#[OA\Get] + composer openapi
app/Modelssrc/EntityEntity APIsrc/Model + #[TableAttribute]
app/Http/Controllerssrc/ControllerController pluginsrc/Controller
EloquentDoctrine ORMEntity/Field APIbyjg/micro-orm
— (Eloquent is the repo)src/RepositoryEntityStoragesrc/Repository
database/migrationsmigrations/hook_update_Ndb/migrations/{up,down}
config/*.phpconfig/services.yaml*.services.ymlconfig/{env}/*.php
.env.envsettings.phpconfig/{env}/credentials.env
app()->make()autowiring\Drupal::service()constructor injection (bindings declared explicitly)
Gate / Policy#[IsGranted]_permission route key#[RequireRole]
FormRequestValidator + DTOForm API#[ValidateRequest] + OpenAPI schema
php artisan tinker—drush phpcomposer terminal

Commands​

LaravelSymfonyGluo
php artisan servesymfony servedocker compose up -d
php artisan migratedoctrine:migrations:migratecomposer migrate -- --env=dev update
php artisan migrate:freshdoctrine:schema:drop --forcecomposer migrate -- --env=dev reset
php artisan make:model -mcrmake:entity / make:crudcomposer codegen -- --table=x all --save
php artisan testbin/phpunitcomposer test
—cache:clearcomposer openapi
Pint / PHPStanPHPStancomposer psalm

Where you are already at home​

  • Layout. The repository root is the PHP application root — one composer.json, one vendor/, with src/, config/, db/, public/, tests/ beside it. See Repository layout.
  • Layering. Controller → Service → Repository → Model is the Symfony playbook. Simple CRUD can skip the service via the ActiveRecord pattern — see Architecture Decisions.
  • Authorisation attributes. #[RequireAuthenticated] and #[RequireRole('admin')] behave like Symfony's #[IsGranted].
  • Testing. Functional tests run against the OpenAPI contract, so a response that drifts from the documented schema fails the test. See Testing.

What is not included​

Gluo is deliberately small. There is no queue/job system, no event dispatcher, no scheduler, and no template layer for HTML (email templates use jinja-php). If your application needs those, you bring the library.

Next​

  1. Installation — create the project
  2. Your first table — migration then codegen
  3. Your first endpoint — attributes, and the regeneration step