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 tinkerdrush 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