Skip to content

Support PHP 8 Attributes for Model Transformer Declaration #1824

@f-liva

Description

@f-liva

Description

Proposal to add support for declaring transformers via PHP 8 attributes on Eloquent models (or controllers/resources) instead of relying on explicit transformer registration. This will improve readability, reduce boilerplate, and make it easier to maintain mappings between models and their transformers in large APIs.

Motivation

Currently transformers are often registered or resolved through separate configuration files or in controllers/resources using explicit calls. Using PHP 8 attributes to annotate models (or controller actions) with their transformer class makes the relationship explicit, colocated, and easier to discover. This small syntactic improvement increases developer experience and reduces the surface for mistakes when working with many models and transformers.

Benefits

  • Better readability: the transformer is immediately visible where the model is declared.
  • Less boilerplate: no need for central registration or repeated mapping logic.
  • Easier refactoring: changing the transformer is a single-line edit on the model.
  • Backwards-compatible: attribute lookup can be optional and fallback to current registration.

Proposed behaviour

  1. Allow an attribute (e.g. TransformedBy) to be placed on a Model/Controller class:
    • The attribute should accept a transformer class name.
  2. When serializing or responding with a model (e.g., in controllers, resources, or a custom response layer), the framework should resolve the transformer:
    • First try to find an explicit transformer registered by current mechanism (if any).
    • Otherwise, inspect the controller class for the TransformedBy attribute and use that transformer.
    • Otherwise, inspect the model class for the TransformedBy attribute and use that transformer.
  3. Keep the existing behaviour unchanged unless an attribute is present or a configuration flag is enabled.

Example attribute definition:

<?php
namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final class TransformedBy
{
    public function __construct(public string $transformerClass) {}
}

Model example:

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Dingo\Api\Transformer\TransformedBy;
use App\Transformers\UserTransformer;

#[TransformedBy(UserTransformer::class)]
class User extends Model
{
    // model content...
}

Controller example:

<?php
namespace App\Http\Controllers\Api;

use App\Models\User;
use Dingo\Api\Transformer\TransformedBy;
use App\Transformers\UserTransformer;

#[TransformedBy(UserTransformer::class)]
class UserController extends Controller
{
    public function show(User $id)
    {
        // response layer discovers transformer via attribute if not explicitly passed
        return $this->response->item($user);
    }
}

Implementation notes

  • Provide a small utility (e.g., TransformerResolver) that wraps the logic:
    • check explicit registrations (existing)
    • check attributes on model class
    • optionally check for controller-level
    • cache resolved transformer per class for performance
  • Make this optional via config (e.g., api.transformersAttributesLookup = true) so teams can opt-in.
  • Ensure attribute lookup supports namespaced classes and inheritance: if a child model doesn't have the attribute, look up parent classes.
  • Add tests: unit tests for resolver, and a feature test demonstrating controller response uses attribute-declared transformer.

Backward compatibility and migration

  • Default behaviour remains the same unless attribute lookup is enabled or attribute is present.
  • Existing projects continue to function.
  • Provide documentation + a codemod recipe for teams that want to migrate to attributes (nice to have?).

Security and performance considerations

  • Cache attribute resolution per class to avoid repetitive reflection overhead.
  • Keep the resolver simple and deterministic.
  • No change in data transformation semantics beyond which transformer class is selected.

Suggested API / Code locations

  • New attribute class: Dingo\Api\Transformer\TransformedBy
  • Transformer resolution service: Dingo\Api\Transformer\TransformerResolver (or similar) bound in service container
  • Integration points:
    • Response factory / Fractal manager or Dingo transformer resolver extension

Minimal example of resolver (concept):

<?php
namespace Dingo\Api\Transformer;

use ReflectionClass;

class TransformerResolver
{
    protected array $cache = [];

    public function resolveFor(object|string $model): ?string
    {
        $class = is_object($model) ? get_class($model) : $model;

        if (isset($this->cache[$class])) {
            return $this->cache[$class];
        }

        $reflection = new ReflectionClass($class);
        $attributes = $reflection->getAttributes(\App\Attributes\TransformedBy::class);

        if (!empty($attributes)) {
            $instance = $attributes[0]->newInstance();
            return $this->cache[$class] = $instance->transformerClass;
        }

        return $this->cache[$class] = null;
    }
}

Request

Please consider adding support for PHP 8 attributes to declare transformers and expose a small resolver integration (configurable) so that response serialization can leverage the annotated transformer when available.

If agreed, I can prepare a small PR with:

  • the attribute class
  • the resolver service
  • a provider binding + optional config flag
  • unit tests and a simple integration test
  • documentation example showing model/controller usage

Additional questions

Do you prefer the attribute to be placed on Models only, or also allow Controllers to be annotated?
Any preferred attribute namespace or naming convention? Current suggestion: Dingo\Api\Transformer\TransformedBy

Call to action

If you like this idea or want to help, please react/comment below, share suggestions, or volunteer to review/implement the PR — contributions are more than welcome! 🙌✨

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions