Os métodos mágicos no PHP são métodos especiais que começam com dois underscores (__) e são automaticamente invocados em determinadas situações. Eles permitem que você defina comportamentos personalizados para operações como acesso a propriedades inacessíveis, chamada de métodos inexistentes, conversão para string e uso de objetos como funções. Esses recursos são poderosos, mas exigem cuidado para não comprometer a clareza e o desempenho do código.

Nesta aula, vamos nos aprofundar nos métodos mágicos mais comuns: __get, __set, __call, __toString e __invoke. Cada um será explicado com exemplos práticos, mostrando como e quando utilizá-los. Também discutiremos boas práticas e armadilhas a evitar.

__get, __set, __call

Os métodos __get e __set são invocados quando você tenta ler ou escrever em uma propriedade inacessível (protegida, privada ou inexistente) de um objeto. Eles permitem implementar acesso dinâmico a dados, como em objetos que encapsulam arrays ou usam lazy loading. Já o método __call é chamado quando um método inacessível é invocado, possibilitando a criação de métodos dinâmicos ou a delegação de chamadas.

Exemplo prático: uma classe que armazena dados em um array interno e permite acesso via propriedades dinâmicas.

class DynamicProperties {
    private $data = [];

    public function __get($name) {
        if (array_key_exists($name, $this->data)) {
            return $this->data[$name];
        }
        return null;
    }

    public function __set($name, $value) {
        $this->data[$name] = $value;
    }

    public function __call($name, $arguments) {
        if (method_exists($this, $name)) {
            return call_user_func_array([$this, $name], $arguments);
        }
        // Exemplo: se o método começar com 'get', retorna propriedade
        if (strpos($name, 'get') === 0) {
            $prop = lcfirst(substr($name, 3));
            return $this->__get($prop);
        }
        throw new \BadMethodCallException("Method $name not found");
    }
}

$obj = new DynamicProperties();
$obj->name = 'João';
echo $obj->name; // João
$obj->setName('Maria'); // Chamada dinâmica? Na verdade, __call não é invocado se o método existe.
// Para testar __call, chame um método inexistente:
echo $obj->getName(); // 'Maria' (usando __call)

Note que __call só é acionado para métodos que não existem ou não são acessíveis. No exemplo, getName não existe, então __call o trata como um getter dinâmico.

__toString

O método __toString permite que um objeto seja tratado como string, sendo invocado automaticamente em contextos como echo, print ou concatenação. Ele deve sempre retornar uma string. É útil para fornecer uma representação legível do objeto, como em logs ou depuração.

Exemplo:

class User {
    public $name;
    public $email;

    public function __construct($name, $email) {
        $this->name = $name;
        $this->email = $email;
    }

    public function __toString() {
        return "User: {$this->name} ({$this->email})";
    }
}

$user = new User('Ana', 'ana@example.com');
echo $user; // User: Ana (ana@example.com)

É importante lembrar que a partir do PHP 7.4, você pode declarar o tipo de retorno como string para garantir que o método sempre retorne uma string.

__invoke

O método __invoke permite que um objeto seja chamado como uma função. Quando você coloca parênteses após o objeto, o PHP executa este método. Isso é útil para criar objetos que se comportam como callables, como closures ou funções de callback.

Exemplo:

class Adder {
    private $amount;

    public function __construct($amount) {
        $this->amount = $amount;
    }

    public function __invoke($value) {
        return $value + $this->amount;
    }
}

$addFive = new Adder(5);
echo $addFive(10); // 15

// Pode ser usado como callback:
$numbers = [1, 2, 3];
$result = array_map($addFive, $numbers);
print_r($result); // [6, 7, 8]

Isso é especialmente útil em padrões como Strategy ou quando você precisa passar comportamento como argumento.

Cuidados

Embora os métodos mágicos sejam flexíveis, seu uso excessivo pode tornar o código difícil de entender e depurar. Eles podem esconder erros (como propriedades ou métodos que não existem) e impactar o desempenho, pois são mais lentos que acessos diretos. Além disso, podem quebrar a introspecção de IDEs e ferramentas de análise estática.

Recomendações:

  • Use métodos mágicos apenas quando necessário, como em frameworks ou bibliotecas que precisam de flexibilidade.
  • Documente bem o comportamento dinâmico.
  • Considere alternativas como ArrayAccess para acesso a propriedades dinâmicas.
  • Evite __call para implementar proxies ou delegates; prefira técnicas mais explícitas.
  • Não abuse de __get e __set para criar propriedades públicas dinâmicas; isso pode prejudicar a manutenção.

Lembre-se: poder não significa que você deve usar. Sempre avalie se a simplicidade e clareza não são mais importantes.

Boas Práticas

Ao implementar métodos mágicos, siga estas boas práticas:

  • Declare os métodos com visibilidade pública, pois eles são chamados externamente.
  • Respeite as assinaturas: __get(string $name): mixed, __set(string $name, mixed $value): void, __call(string $name, array $arguments): mixed, __toString(): string, __invoke(mixed ...$args): mixed.
  • Para __toString, sempre retorne uma string; caso contrário, o PHP emitirá um erro fatal.
  • Use type hints sempre que possível para melhorar a previsibilidade.
  • Evite efeitos colaterais em __toString, pois pode ser chamado em contextos inesperados (como depuração).

Referências

Exercícios

  1. Crie uma classe Config que armazena configurações em um array privado e usa __get e __set para acessar e modificar propriedades dinamicamente. Implemente também __toString para retornar uma string JSON das configurações.

    ✓ Resposta:
    class Config {
        private $settings = [];
    
        public function __get($name) {
            return $this->settings[$name] ?? null;
        }
    
        public function __set($name, $value) {
            $this->settings[$name] = $value;
        }
    
        public function __toString() {
            return json_encode($this->settings);
        }
    }
    
    $config = new Config();
    $config->host = 'localhost';
    $config->port = 3306;
    echo $config; // {"host":"localhost","port":3306}
    
  2. Implemente uma classe Calculator que usa __call para tratar métodos como add, subtract, etc. Cada método deve receber dois números e retornar o resultado. Se o método não for reconhecido, lance uma exceção.

    ✓ Resposta:
    class Calculator {
        public function __call($name, $arguments) {
            if (count($arguments) != 2) {
                throw new \InvalidArgumentException('Two arguments required');
            }
            switch ($name) {
                case 'add':
                    return $arguments[0] + $arguments[1];
                case 'subtract':
                    return $arguments[0] - $arguments[1];
                case 'multiply':
                    return $arguments[0] * $arguments[1];
                case 'divide':
                    return $arguments[0] / $arguments[1];
                default:
                    throw new \BadMethodCallException("Method $name not supported");
            }
        }
    }
    
    $calc = new Calculator();
    echo $calc->add(5, 3); // 8
    echo $calc->divide(10, 2); // 5
    
  3. Crie uma classe Multiplier que recebe um fator no construtor e usa __invoke para multiplicar o argumento por esse fator. Use-a com array_map para dobrar uma lista de números.

    ✓ Resposta:
    class Multiplier {
        private $factor;
    
        public function __construct($factor) {
            $this->factor = $factor;
        }
    
        public function __invoke($value) {
            return $value * $this->factor;
        }
    }
    
    $double = new Multiplier(2);
    $numbers = [1, 2, 3, 4];
    $result = array_map($double, $numbers);
    print_r($result); // [2, 4, 6, 8]
    
  4. Explique por que o uso excessivo de métodos mágicos pode ser prejudicial. Dê pelo menos três razões.

    ✓ Resposta:
    1. Dificulta a depuração: erros podem ser mascarados, pois acessos a propriedades ou métodos inexistentes não geram erros imediatos.
    2. Impacto no desempenho: os métodos mágicos são mais lentos que chamadas diretas, pois exigem resolução dinâmica.
    3. Quebra de ferramentas: IDEs e analisadores estáticos perdem a capacidade de autocompletar e verificar tipos, reduzindo a produtividade.
  5. Implemente uma classe User com propriedades privadas name e email, e um método __toString que retorna o nome e email formatados. Crie um objeto e exiba-o com echo.

    ✓ Resposta:
    class User {
        private $name;
        private $email;
    
        public function __construct($name, $email) {
            $this->name = $name;
            $this->email = $email;
        }
    
        public function __toString() {
            return "{$this->name} <{$this->email}>";
        }
    }
    
    $user = new User('Carlos', 'carlos@example.com');
    echo $user; // Carlos