Active Record

O Active Record fornece uma interface orientada a objetos para acessar e manipular dados armazenados em bancos de dados. Uma classe Active Record está associado a uma tabela da base de dados, uma instância do Active Record corresponde a uma linha desta tabela, e um atributo desta instância representa o valor de uma coluna desta linha. Em vez de escrever instruções SQL a mão, você pode acessar os atributos do Active Record e chamar os métodos do Active Record para acessar e manipular os dados armazenados nas tabelas do banco de dados.

Por exemplo, assumindo que Customer é uma classe Active Record que está associada com a tabela customer e name é uma coluna desta tabela. Você pode escrever o seguinte código para inserir uma nova linha na tabela customer:

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

O código acima é equivalente a seguinte instrução SQL escrita à mão para MySQL, que é menos intuitiva, mais propenso a erros, e pode até ter problemas de compatibilidade se você estiver usando um tipo diferente de banco de dados:

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
   ':name' => 'Qiang',
])->execute();

O Yii fornece suporte Active Record para os seguintes bancos de dados relacionais:

Neste tutorial, vamos principalmente descrever o uso do Active Record para banco de dados relacionais. Todavia, maior parte do conteúdo descrito aqui também são aplicáveis a Active Record para bancos de dados NoSQL.

Declarando Classes Active Record

Para começar, declare uma classe Active Record estendendo yii\db\ActiveRecord. Porque cada classe Active Record é associada a uma tabela do banco de dados, nesta classe você deve sobrescrever o método tableName() para especificar a tabela que a classe está associada.

No exemplo abaixo, declaramos uma classe Active Record chamada Customer para a tabela do banco de dados customer.

namespace app\models;

use yii\db\ActiveRecord;

class Customer extends ActiveRecord
{
   const STATUS_INACTIVE = 0;
   const STATUS_ACTIVE = 1;
   
   /**
    * @return string the name of the table associated with this ActiveRecord class.
    */
   public static function tableName()
   {
       return 'customer';
   }
}

Instâncias de Active Record são consideradas como models (modelos). Por esta razão, geralmente colocamos as classes Active Record debaixo do namespace app\models (ou outros namespaces destinados a classes model).

Porque yii\db\ActiveRecord estende a partir de yii\base\Model, ele herda todas as características de model, tal como atributos, regras de validação, serialização de dados, etc.

Conectando ao Banco de Dados

Por padrão, o Active Record usa o componente de aplicação db com a DB connection para acessar e manipular os dados da base de dados. Como explicado em Database Access Objects, você pode configurar o componente db na configuração da aplicação como mostrado abaixo,

return [
   'components' => [
       'db' => [
           'class' => 'yii\db\Connection',
           'dsn' => 'mysql:host=localhost;dbname=testdb',
           'username' => 'demo',
           'password' => 'demo',
       ],
   ],
];

Se você quiser usar uma conexão de banco de dados diferente do que o componente db, você deve sobrescrever o método getDb():

class Customer extends ActiveRecord
{
   // ...

   public static function getDb()
   {
       // use the "db2" application component
       return \Yii::$app->db2;  
   }
}

Consultando Dados

Depois de declarar uma classe Active Record, você pode usá-lo para consultar dados da tabela de banco de dados correspondente. Este processo geralmente leva os seguintes três passos:

  1. Crie um novo objeto query chamando o método yii\db\ActiveRecord::find();
  2. Construa o objeto query chamando os métodos de query building;
  3. Chame um método de query para recuperar dados em uma instância do Active Record.

Como você pode ver, isso é muito semelhante ao procedimento com query builder. A única diferença é que em vez de usar o operador new para criar um objeto query, você chama yii\db\ActiveRecord::find() para retornar um novo objeto query que é da classe yii\db\ActiveQuery.

A seguir, estão alguns exemplos que mostram como usar Active Query para pesquisar dados:

// retorna um único customer cujo ID é 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
   ->where(['id' => 123])
   ->one();

// retorna todos customers ativos e os ordena por seus IDs
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
   ->where(['status' => Customer::STATUS_ACTIVE])
   ->orderBy('id')
   ->all();

// retorna a quantidade de customers ativos
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
   ->where(['status' => Customer::STATUS_ACTIVE])
   ->count();

// retorna todos customers em um array indexado pelos seus IDs
// SELECT * FROM `customer`
$customers = Customer::find()
   ->indexBy('id')
   ->all();

No exemplo acima, $customer é um objeto Customer enquanto $customers é um array de objetos Customer. Todos são preenchidos com os dados recuperados da tabela customer.

Observação: Uma vez que o yii\db\ActiveQuery estende de yii\db\Query, você pode usar todos os métodos do query building e da query tal como descrito na seção Query Builder.

Já que consultar por valores de chave primária ou um conjunto de valores de coluna é uma tarefa comum, o Yii fornece dois métodos de atalho para este propósito:

Ambos os métodos pode ter um dos seguintes formatos de parâmetro:

  • Um valor escalar: o valor é tratado como uma chave primária que se deseja procurar. O Yii irá determinar automaticamente que coluna é a chave primária lendo o schema da base de dados.
  • Um array de valores escalar: o array como uma chaves primárias que se deseja procurar.
  • Um array associativo: as chaves são nomes de colunas e os valores são os valores correspondentes as colunas que se deseja procurar. Por favor, consulte o Hash Format para mais detalhes.

O código a seguir mostra como estes métodos podem ser usados:

// retorna um único customer cujo ID é 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// retorna customers cujo ID é 100, 101, 123 or 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// retorna um customer ativo cujo ID é 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
   'id' => 123,
   'status' => Customer::STATUS_ACTIVE,
]);

// retorna todos os customers inativos
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
   'status' => Customer::STATUS_INACTIVE,
]);

Observação: Nem o yii\db\ActiveRecord::findOne() ou o yii\db\ActiveQuery::one() irão adicionar LIMIT 1 para a instrução SQL gerada. Se a sua query retornar muitas linhas de dados, você deve chamar limit(1) explicitamente para melhorar o desempenho, ex., Customer::find()->limit(1)->one().

Além de usar os métodos do query building, você também pode escrever SQLs puros para pesquisar dados e preencher os objetos Active Record com o resultado. Você pode fazer isso chamando o método yii\db\ActiveRecord::findBySql():

// retorna todos os customers inatrivos
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

Não chame outros métodos do query building depois de chamar findBySql() pois eles serão ignorados.

Acessando Dados

Como já mencionado, os dados retornados da base de dados são populados em uma instância do Active Record, e cada linha do resultado da query corresponde a uma única instância do Active Record. Você pode acessar os valores das colunas acessando os atributos da instância do Active Record, Por exemplo,

// "id" and "email" são os nomes das colunas na tabela "customer"
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

Observação: Os atributos Active Record são nomeados após a associação com as colunas da tabela de uma forma case-sensitive. O Yii automaticamente define um atributo no Active Record para todas as colunas da tabela associada. Você NÃO deve declarar novamente qualquer um dos atributos.

Uma vez que os atributos do Active Record são nomeados de acordo com as colunas das tabelas, você pode achar que está escrevendo código PHP como $customer->first_name, que usa sublinhados para separar palavras em nomes de atributos se as colunas da tabela forem nomeadas desta maneira. Se você está preocupado com um estilo de código consistente, você deve renomear suas colunas da tabela em conformidade (usar camelCase, por exemplo).

Transformação de Dados (Data Transformation)

Acontece frequentemente que os dados que estão sendo inseridos e/ou exibidos estão em um formato que é diferente do utilizado no momento da gravação na base de dados. Por exemplo, em uma base de dados você está gravando a data de aniversário do customer como UNIX timestamps (que não é muito amigável), embora, na maioria das vezes você gostaria de manipular aniversários como strings no formato de 'YYYY/MM/DD'. Para atingir este objetivo, você pode definir métodos de transformação de dados na classe Active Record Customer como a seguir:

class Customer extends ActiveRecord
{
   // ...

   public function getBirthdayText()
   {
       return date('Y/m/d', $this->birthday);
   }
   
   public function setBirthdayText($value)
   {
       $this->birthday = strtotime($value);
   }
}

Agora no seu código PHP, em vez de acessar $customer->birthday, você acessaria $customer->birthdayText, que lhe permitirá inserir e exibir data de aniversário dos customers no formato 'YYYY/MM/DD'.

Dica: O exemplo acima mostra uma forma genérica de transformação de dados em diferentes formatos. Se você estiver trabalhando com valores de data, você pode usar o DateValidator e o DatePicker, que é mais fácil e mais poderoso.

Recuperando Dados em Arrays

Embora a recuperação de dados através de objetos Active Record seja conveniente e flexível, pode não ser a melhor opção caso você tenha que retornar uma grande quantidade de dados devido ao grande consumo de memória. Neste caso, você pode recuperar usando arrays do PHP chamando asArray() antes de executar um método query:

// retorna todos os `customers`
// cada `customer` retornado é associado a um array
$customers = Customer::find()
   ->asArray()
   ->all();

Observação: Enquanto este método economiza memória e melhora o desempenho, ele é muito próximo a camada de abstração do DB e você vai perder a maioria dos recursos do Active Record. Uma distinção muito importante reside no tipo dos valores de coluna de dados. Quando você retorna dados em uma instância de Active Record, valores de colunas serão automaticamente convertidos de acordo com os tipos de coluna reais; de outra forma quando você retorna dados em arrays, valores de colunas serão strings (uma vez que são o resultado do PDO sem nenhum processamento), independentemente seus tipos de coluna reais.

Recuperando Dados em Lote

No Query Builder, explicamos que você pode usar batch query para minimizar o uso de memória quando pesquisar uma grande quantidade de dados do banco de dados. Você pode utilizar a mesma técnica no Active Record. Por exemplo,

// descarrega 10 `customers` a cada vez
foreach (Customer::find()->batch(10) as $customers) {
   // $customers é um array de 10 ou memos objetos Customer
}

// descarrega 10 `customers` por vez e faz a iteração deles um por um
foreach (Customer::find()->each(10) as $customer) {
   // $customer é um objeto Customer
}

// batch query com carga antecipada
foreach (Customer::find()->with('orders')->each() as $customer) {
   // $customer é um objeto Customer
}

Salvando Dados

Usando Active Record, você pode facilmente salvar dados em um banco de dados realizando as seguintes etapas:

  1. Preparar uma instância de Active Record
  2. Atribuir novos valores aos atributos do Active Record
  3. Chamar o método yii\db\ActiveRecord::save() para salvar os informações no banco de dados.

Por exemplo,

// insere uma nova linha de dados
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// atualiza uma linha de dados existente
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

O método save() pode tanto inserir ou atualizar dados, dependendo do estado da instância do Active Record. Se a instância tiver sido recém criada através do operador new, ao chamar save() será realizado a inserção de uma nova linha; se a instância for o resultado de um método da query, ao chamar save() será realizado a atualização dos dados associados a instância.

Você pode diferenciar os dois estados de uma instância de Active Record verificando o valor da sua propriedade yii\db\ActiveRecord::isNewRecord. Esta propriedade também é usada internamente pelo save() como mostrado abaixo:

public function save($runValidation = true, $attributeNames = null)
{
   if ($this->getIsNewRecord()) {
       return $this->insert($runValidation, $attributeNames);
   } else {
       return $this->update($runValidation, $attributeNames) !== false;
   }
}

Dica: Você pode chamar insert() ou update() diretamente para inserir ou atualizar dados.

Validação de Dados

Já que o yii\db\ActiveRecord estende de yii\base\Model, ele compartilha os mesmos recursos de validação de dados. Você pode declarar regras de validação sobrescrevendo o método rules() e realizar a validação de dados chamando o método validate().

Quando você chama save(), por padrão chamará validate() automaticamente. somente quando a validação passa, os dados são de fato salvos; do contrário, simplesmente retorna falso, e você pode verificar a propriedade yii\db\ActiveRecord::errors para recuperar a mensagem de erro de validação.

Dica: Se você tiver certeza que os seus dados não precisam de validação (ex., os dados tem uma origem confiável), você pode chamar save(false) para pular a validação.

Atribuição Maciça

Como um models normal, instância de Active Record também oferece o recurso de atribuição maciça. Usando este recurso, você pode atribuir valores para vários atributos de uma instância de Active Record em uma única declaração PHP, como mostrado a seguir. Lembre-se que somente atributos de segurança pode ser massivamente atribuídos.

$values = [
   'name' => 'James',
   'email' => 'james@example.com',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

Atualizando Contadores

Isto é uma tarefa comum para incrementar ou decrementar uma coluna em uma tabela do banco de dados. Chamamos essas colunas como colunas de contador. Você pode usar updateCounters() para atualizar uma ou mais colunas de contadores. Por exemplo,

$post = Post::findOne(100);

// UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
$post->updateCounters(['view_count' => 1]);

Observação: Se você usar yii\db\ActiveRecord::save() para atualizar uma coluna de contador, você pode acabar com um resultado impreciso, porque é provável que o mesmo contador esteja sendo salvo por várias solicitações que lêem e escrevem o mesmo valor do contador.

Atributos Sujos

Quando você chama save() para salvar uma instância de Active Record, somente atributos sujos serão salvos. Um atributo é considerado sujo se o seu valor foi modificado desde que foi carregado a partir de DB ou salvos em DB, mais recentemente. Note que a validação de dados será realizada, independentemente se a instância do Active Record tiver ou não atributos sujos.

O Active Record mantém automaticamente a lista de atributos sujas. Isto é feito mantendo uma versão antiga dos valores de atributos e comparando-as com as últimas informações. Você pode chamar yii\db\ActiveRecord::getDirtyAttributes() para pegar os atributos sujos correntes. Você também pode chamar yii\db\ActiveRecord::markAttributeDirty() para marcar explicitamente um atributo como sujo.

Se você estiver interessado nos valores de atributos antes da sua modificação mais recente, você pode chamar getOldAttributes() ou getOldAttribute().

Observação: A comparação dos valores antigos e novos será feito usando o operador === portanto, um valor será considerado sujo mesmo se ele tiver o mesmo valor, mas um tipo diferente. Isto é comum quando o modelo recebe a entrada de dados do usuário a partir de um formulário HTML onde todos os valores são representados como string. Para garantir o tipo correto, por exemplo, valores inteiros você pode aplicar um filtro de validação: ['attributeName', 'filter', 'filter' => 'intval'].

Valores Padrões de Atributos

Algumas de suas colunas de tabelas podem ter valores padrões definidos em um banco de dados. Algumas vezes, você pode querer preencher previamente o formulário Web para uma instância de Active Record com os seus valores padrões. Para evitar escrever os mesmos valores padrão novamente, você pode chamar loadDefaultValues() para popular os valores padrões definidos pelo DB para os atributos correspondentes do Active Record:

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz será atribuído o valor padrão declarado na definição da coluna "xyz"

Atualizando Múltiplas Linhas

Os métodos descritos acima fazem todo o trabalho em uma instância individual de Active Record, causando inserção ou atualização de linhas de tabela individuais. Para atualizar múltiplas linhas individualmente, você deve chamar o método estático updateAll().

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

Da mesma forma você pode chamar updateAllCounters() para atualizar colunas de contador de várias linhas ao mesmo tempo.

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

Deletando Dados

Para deletar uma única linha de dados, primeiro recupere a instância de Active Record correspondente a linha e depois chame o método yii\db\ActiveRecord::delete().

$customer = Customer::findOne(123);
$customer->delete();

Você pode chamar yii\db\ActiveRecord::deleteAll() para deletar múltiplas ou todas a linhas de dados. Por exemplo,

Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);

Observação: Tenha muito cuidado quando chamar deleteAll() porque pode apagar todos os dados de sua tabela se você cometer um erro na especificação da condição.

Ciclo de Vida de um Active Record

É importante entender o ciclo de vida de um Active Record quando ele é usado para diferentes propósitos. Durante cada ciclo de vida, uma determinada sequência de métodos será invocada, e você pode substituir esses métodos para ter a chance de personalizar o ciclo de vida. Você também pode responder certos eventos disparados pelo Active Record durante o ciclo de vida para injetar o seu código personalizado. Estes eventos são especialmente úteis quando você está desenvolvendo behaviorsque precisam personalizar o ciclo de vida do Active Record.

A seguir, vamos resumir diversos ciclos de vida de um Active Record e os métodos/eventos que fazem parte do ciclo de vida.

Ciclo de Vida de uma Nova Instância

Quando se cria uma nova instância de Active Record através do operador new, acontece o seguinte ciclo de vida:

  1. Construção da classe;
  2. init(): dispara um evento EVENT_INIT.

Ciclo de Vida de uma Pesquisa de Dados

Quando se pesquisa dados através de um dos métodos de consulta, cada novo Active Record populado sofrerá o seguinte ciclo de vida:

  1. Contrução da classe.
  2. init(): dispara um evento EVENT_INIT.
  3. afterFind(): dispara um evento EVENT_AFTER_FIND.

Ciclo de Vida da Persistência de Dados

Quando se chama save() para inserir ou atualizar uma instância de Active Record, acontece o seguinte ciclo de vida:

  1. beforeValidate(): dispara um evento EVENT_BEFORE_VALIDATE. se o método retornar falso ou yii\base\ModelEvent::$isValid for falso, o restante das etapas serão ignoradas.
  2. Executa a validação de dados. Se a validação de dados falhar, Os passos após o passo 3 serão ignorados.
  3. afterValidate(): dispara um evento EVENT_AFTER_VALIDATE.
  4. beforeSave(): dispara um evento EVENT_BEFORE_INSERT ou evento EVENT_BEFORE_UPDATE. Se o método retornar falso ou yii\base\ModelEvent::$isValid for falso, o restante dos passos serão ignorados.
  5. Realiza a atual inserção ou atualização de dados;
  6. afterSave(): dispara um evento EVENT_AFTER_INSERT ou evento EVENT_AFTER_UPDATE.

Ciclo de Vida da Deleção de Dados

Quando se chama delete() para deletar uma instância de Active Record, acontece o seguinte ciclo de vida:

  1. beforeDelete(): dispara um evento EVENT_BEFORE_DELETE. Se o método retornar falso ou yii\base\ModelEvent::$isValid for falso, o restante dos passos serão ignorados.
  2. Executa a atual deleção de dados.
  3. afterDelete(): dispara um evento EVENT_AFTER_DELETE.

Observação: Chamar qualquer um dos seguintes métodos não iniciará qualquer um dos ciclos de vida listados acima:

Trabalhando com Transações

Existem duas formas de usar transações quando se trabalha com Active Record. A primeira maneira é anexar explicitamente chamadas de método Active Record em um bloco transacional, como mostrado abaixo,

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
   $customer->id = 200;
   $customer->save();
   // ...outras operações DB...
});

// ou como alternativa

$transaction = Customer::getDb()->beginTransaction();
try {
   $customer->id = 200;
   $customer->save();
   // ...outras operações DB...
   $transaction->commit();
} catch(\Exception $e) {
   $transaction->rollBack();
   throw $e;
}

A segunda maneira é listar as operações de banco de dados que exigem suporte transacional no método yii\db\ActiveRecord::transactions(). Por exemplo,

class Customer extends ActiveRecord
{
   public function transactions()
   {
       return [
           'admin' => self::OP_INSERT,
           'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
           // o código acima é equivalente ao código abaixo:
           // 'api' => self::OP_ALL,
       ];
   }
}

O método yii\db\ActiveRecord::transactions() deve retornar um array cujas chaves são cenário nomes e valores das operações correspondentes que devem ser embutidas dentro de transações. Você deve usar as seguintes constantes para fazer referência a diferentes operações de banco de dados:

Utilize o operador | concatenar as constantes acima para indicar várias operações. Você também pode usar a constante de atalho OP_ALL para referenciar todas as três constantes acima.

Bloqueios Otimistas

O bloqueio otimista é uma forma de evitar conflitos que podem ocorrer quando uma única linha de dados está sendo atualizado por vários usuários. Por exemplo, se ambos os usuário A e B estiverem editando o mesmo artigo de Wiki ao mesmo tempo. Após o usuário A salvar suas alterações, o usuário B pressiona o botão "Save" em uma tentativa de salvar suas edições também. Uma vez que o usuário B está trabalhando em uma versão desatualizada do artigo, seria desejável ter uma maneira de impedi-lo de salvar o artigo e mostrar-lhe alguma mensagem.

Bloqueio otimista resolve o problema acima usando uma coluna para gravar o número de versão de cada registro. Quando um registro está sendo salvo com um número de versão desatualizado, uma exceção yii\db\StaleObjectException será lançada, que impede que o registro seja salvo. Bloqueio otimista só é suportado quando você atualiza ou exclui um registro de dados existente usando yii\db\ActiveRecord::update() ou yii\db\ActiveRecord::delete() respectivamente.

Para usar bloqueio otimista:

  1. Crie uma coluna na tabela do banco de dados associada com a classe Active Record para armazenar o número da versão de cada registro. A coluna deve ser do tipo biginteger (no MySQL ela deve ser BIGINT DEFAULT 0).
  2. Sobrescreva o método yii\db\ActiveRecord::optimisticLock() para retornar o nome desta coluna.
  3. No formulário Web que recebe as entradas dos usuários, adicione um campo hidden para armazenar o número da versão corrente do registro que está sendo atualizado. Certifique-se que seu atributo versão possui regras de validação de entrada validadas com êxito.
  4. Na ação do controller que atualiza o registro usando Active Record, mostre uma estrutura try catch da yii\db\StaleObjectException exceção. Implementar lógica de negócios necessária (ex. mesclar as mudanças, mostrar dados obsoletos) para resolver o conflito.

Por exemplo, digamos que a coluna de versão se chama version. Você pode implementar bloqueio otimista conforme o código abaixo.

// ------ view code -------

use yii\helpers\Html;

// ...Outros campos de entrada de dados
echo Html::activeHiddenInput($model, 'version');


// ------ controller code -------

use yii\db\StaleObjectException;

public function actionUpdate($id)
{
   $model = $this->findModel($id);

   try {
       if ($model->load(Yii::$app->request->post()) && $model->save()) {
           return $this->redirect(['view', 'id' => $model->id]);
       } else {
           return $this->render('update', [
               'model' => $model,
           ]);
       }
   } catch (StaleObjectException $e) {
       // lógica para resolver o conflito
   }
}

Trabalhando com Dados Relacionais

Além de trabalhar com tabelas individuais, o Active Record também é capaz de trazer dados de várias tabelas relacionadas, tornando-os prontamente acessíveis através dos dados primários. Por exemplo, a tabela de clientes está relacionada a tabela de pedidos porque um cliente pode ter múltiplos pedidos. Com uma declaração adequada desta relação, você pode ser capaz de acessar informações do pedido de um cliente utilizando a expressão $customer->orders que devolve informações sobre o pedido do cliente em um array de instâncias de Active Record de Pedidos.

Declarando Relações

Para trabalhar com dados relacionais usando Active Record, você primeiro precisa declarar as relações nas classes Active Record. A tarefa é tão simples como declarar um método de relação para cada relação desejada. Segue exemplos:

class Customer extends ActiveRecord
{
   public function getOrders()
   {
       return $this->hasMany(Order::className(), ['customer_id' => 'id']);
   }
}

class Order extends ActiveRecord
{
   public function getCustomer()
   {
       return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
   }
}

No código acima, nós declaramos uma relação orders para a classe Customer, e uma relação customer para a classe Order.

Cada método de relação deve ser nomeado como getXyz. Nós chamamos de xyz (a primeira letra é em letras minúsculas) o nome da relação. Note que os nomes de relações são case sensitive.

Ao declarar uma relação, você deve especificar as seguintes informações:

  • A multiplicidade da relação: especificada chamando tanto o método hasMany() quanto o método hasOne(). No exemplo acima você pode facilmente ler nas declarações de relação que um customer tem vários orders enquanto uma order só tem um customer.
  • O nome da classe Active Record relacionada: especificada no primeiro parâmetro dos métodos hasMany() e hasOne(). Uma prática recomendada é chamar Xyz::className() para obter o nome da classe para que você possa receber suporte do preenchimento automático de IDEs bem como detecção de erros.
  • A ligação entre os dois tipos de dados: especifica a(s) coluna(s) por meio do qual os dois tipos de dados se relacionam. Os valores do array são as colunas da tabela primária (representada pela classe Active Record que você declarou as relações), enquanto as chaves do array são as colunas da tabela relacionada.

Uma regra fácil de lembrar é, como você pode ver no exemplo acima, você escreve a coluna que pertence ao Active Record relacionado diretamente ao lado dele. Você pode ver que customer_id é uma propriedade de Order e id é uma propriedade de Customer.

Acessando Dados Relacionais

Após declarar as relações, você pode acessar os dados relacionados através dos nomes das relações. Isto é como acessar uma propriedade de um objeto definida por um método de relação. Por esta razão, nós podemos chamá-la de propriedade de relação. Por exemplo,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders is an array of Order objects
$orders = $customer->orders;

Observação: quando você declara uma relação chamada xyz através de um método getter getXyz(), você terá acesso a xyz como uma propriedade de objeto. Note que o nome é case-sensitive.

Se a relação for declarada com hasMany(), acessar esta propriedade irá retornar um array de instâncias de Active Record relacionais; Se a relação for declarada com hasOne(), acessar esta propriedade irá retornar a instância de Active Record relacional ou null se não encontrar dados relacionais.

Quando você acessa uma propriedade de relação pela primeira vez, uma instrução SQL será executada, como mostrado no exemplo acima. Se a mesma propriedade for acessada novamente, o resultado anterior será devolvido sem executar novamente a instrução SQL. Para forçar a execução da instrução SQL, você deve primeiramente remover a configuração da propriedade de relação: unset($customer->orders).

Observação: Embora este conceito é semelhante ao recurso de propriedade de objeto, existe uma diferença importante. Em uma propriedade de objeto normal o valor da propriedade é do mesmo tipo que o método getter definindo. Já o método de relação retorna uma instância de yii\db\ActiveQuery, embora o acesso a uma propriedade de relação retorne uma instância de yii\db\ActiveRecord ou um array deste tipo.

$customer->orders; // é um array de objetos `Order`
$customer->getOrders(); // returna uma instância de ActiveQuery

Isso é útil para a criação de consultas personalizadas, que está descrito na próxima seção.

Consulta Relacional Dinâmica

Uma vez que um método de relação retorna uma instância de yii\db\ActiveQuery, você pode construir uma query usando os métodos de query building antes de executá-la. Por exemplo,

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
   ->where(['>', 'subtotal', 200])
   ->orderBy('id')
   ->all();

Diferente de acessar uma propriedade de relação, cada vez que você executar uma consulta relacional dinâmica através de um método de relação, uma instrução SQL será executada, mesmo que a mesma consulta relacional dinâmica tenha sido executada anteriormente.

Algumas vezes você pode querer parametrizar uma relação para que possa executar mais facilmente uma consulta relacional dinâmica. Por exemplo, você pode declarar uma relação bigOrders da seguinte forma,

class Customer extends ActiveRecord
{
   public function getBigOrders($threshold = 100)
   {
       return $this->hasMany(Order::className(), ['customer_id' => 'id'])
           ->where('subtotal > :threshold', [':threshold' => $threshold])
           ->orderBy('id');
   }
}

Em seguida, você será capaz de executar as seguintes consultas relacionais:

// SELECT * FROM `order` WHERE `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

Relações Através de Tabela de Junção

Em uma modelagem de banco de dados, quando a multiplicidade entre duas tabelas relacionadas é many-to-many, geralmente é criada uma tabela de junção. Por exemplo, a tabela order e a tabela item podem se relacionar através da tabela de junção chamada order_item. Um order, então, corresponderá a múltiplos order items, enquanto um product item também corresponderá a múltiplos order items.

Ao declarar tais relações, você chamaria via() ou viaTable() para especificar a tabela de junção. A diferença entre via() e viaTable() é que o primeiro especifica a tabela de junção em função a uma relação existente enquanto o último faz referência diretamente a tabela de junção. Por exemplo,

class Order extends ActiveRecord
{
   public function getItems()
   {
       return $this->hasMany(Item::className(), ['id' => 'item_id'])
           ->viaTable('order_item', ['order_id' => 'id']);
   }
}

ou alternativamente,

class Order extends ActiveRecord
{
   public function getOrderItems()
   {
       return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
   }

   public function getItems()
   {
       return $this->hasMany(Item::className(), ['id' => 'item_id'])
           ->via('orderItems');
   }
}

A utilização das relações declaradas com uma tabela de junção é a mesma que a das relações normais. Por exemplo,

// SELECT * FROM `order` WHERE `id` = 100
$order = Order::findOne(100);

// SELECT * FROM `order_item` WHERE `order_id` = 100
// SELECT * FROM `item` WHERE `item_id` IN (...)
// returns an array of Item objects
$items = $order->items;

Lazy e Eager Loading

Na seção Acessando Dados Relacionais, nós explicamos que você pode acessar uma propriedade de relação de uma instância de Active Record como se acessa uma propriedade de objeto normal. Uma instrução SQL será executada somente quando você acessar a propriedade de relação pela primeira vez. Chamamos esta forma de acessar estes dados de lazy loading. Por exemplo,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// nenhum SQL é executado
$orders2 = $customer->orders;

O uso de lazy loading é muito conveniente. Entretanto, pode haver um problema de desempenho caso você precise acessar a mesma propriedade de relação a partir de múltiplas instâncias de Active Record. Considere o exemplo a seguir. Quantas instruções SQL serão executadas?

// SELECT * FROM `customer` LIMIT 100
$customers = Customer::find()->limit(100)->all();

foreach ($customers as $customer) {
   // SELECT * FROM `order` WHERE `customer_id` = ...
   $orders = $customer->orders;
}

Como você pode ver no comentário do código acima, há 101 instruções SQL sendo executadas! Isto porque cada vez que você acessar a propriedade de relação orders com um objeto Customer diferente no bloco foreach, uma instrução SQL será executada.

Para resolver este problema de performance, você pode usar o eager loading, como mostrado abaixo,

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
   ->with('orders')
   ->limit(100)
   ->all();

foreach ($customers as $customer) {
   // nenhum SQL é executado
   $orders = $customer->orders;
}

Ao chamar yii\db\ActiveQuery::with(), você instrui ao Active Record trazer os orders dos primeiros 100 customers em uma única instrução SQL. Como resultado, você reduz o número de instruções SQL executadas de 101 para 2.

Você pode utilizar esta abordagem com uma ou várias relações. Você pode até mesmo utilizar eager loading com relações aninhadas. Um relação aninhada é uma relação que é declarada dentro de uma classe Active Record de relação. Por exemplo, Customer se relaciona com Order através da relação orders, e Order está relacionado com Item através da relação items. Quando pesquisar por Customer, você pode fazer o eager loading de items usando a notação de relação aninhada orders.items.

O código a seguir mostra diferentes formas de utilizar with(). Assumimos que a classe Customer tem duas relações orders e country, enquanto a classe Order tem uma relação items.

// carga antecipada de "orders" e "country"
$customers = Customer::find()->with('orders', 'country')->all();
// equivalente à sintaxe de array abaixo
$customers = Customer::find()->with(['orders', 'country'])->all();
// nenhum SQL executado
$orders= $customers[0]->orders;
// nenhum SQL executado
$country = $customers[0]->country;

// carga antecipada de "orders" e a relação aninhada "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// acessa os "items" do primeiro "order" e do primeiro "customer"
// nenhum SQL executado
$items = $customers[0]->orders[0]->items;

Você pode carregar antecipadamente relações profundamente aninhadas, tais como a.b.c.d. Todas as relações serão carregadas antecipadamente. Isto é, quando você chamar with() usando a.b.c.d, você fará uma carga antecipada de a, a.b, a.b.c e a.b.c.d.

Observação: Em geral, ao fazer uma carga antecipada de N relações em que M relações são definidas com uma tabela de junção, um total de instruções SQL N+M+1 serão executadas. Note que uma relação aninhada a.b.c.d conta como 4 relações.

Ao carregar antecipadamente uma relação, você pode personalizar a consulta relacional correspondente utilizando uma função anônima. Por exemplo,

// procura “customers” trazendo junto “country” e “orders” ativas
// SELECT * FROM `customer`
// SELECT * FROM `country` WHERE `id` IN (...)
// SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
$customers = Customer::find()->with([
   'country',
   'orders' => function ($query) {
       $query->andWhere(['status' => Order::STATUS_ACTIVE]);
   },
])->all();

Ao personalizar a consulta relacional para uma relação, você deve especificar o nome da relação como uma chave de array e usar uma função anônima como o valor de array correspondente. A função anônima receberá um parâmetro $query que representa o objeto yii\db\ActiveQuery utilizado para realizar a consulta relacional para a relação. No exemplo acima, modificamos a consulta relacional, acrescentando uma condição adicional sobre o status de 'orders'.

Observação: Se você chamar select() ao carregar antecipadamente as relações, você tem que certificar-se de que as colunas referenciadas na relação estão sendo selecionadas. Caso contrário, os modelos relacionados podem não ser carregados corretamente. Por exemplo,

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer é sempre nulo. Para corrigir o problema, você deve fazer o seguinte:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

Relações utilizando JOIN

Observação: O conteúdo descrito nesta subsecção é aplicável apenas aos bancos de dados relacionais, tais como MySQL, PostgreSQL, etc.

As consultas relacionais que temos descrito até agora só fizeram referência as chave primária ao consultar os dados. Na realidade, muitas vezes precisamos referenciar colunas nas tabelas relacionadas. Por exemplo, podemos querer trazer os 'customers' que tenham no mínimo um 'order' ativo. Para solucionar este problema, podemos fazer uma query com join como a seguir:

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id`
// WHERE `order`.`status` = 1
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()
   ->select('customer.*')
   ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`')
   ->where(['order.status' => Order::STATUS_ACTIVE])
   ->with('orders')
   ->all();

Observação: É importante evitar ambiguidade de nomes de colunas ao criar queries com JOIN. Uma prática comum é prefixar os nomes das colunas com os nomes de tabela correspondente. Entretanto, uma melhor abordagem é a de explorar as declarações de relação existente chamando yii\db\ActiveQuery::joinWith():

$customers = Customer::find()
   ->joinWith('orders')
   ->where(['order.status' => Order::STATUS_ACTIVE])
   ->all();

Ambas as formas executam o mesmo conjunto de instruções SQL. Embora a última abordagem seja mais elegante.

Por padrão , joinWith() usará LEFT JOIN para juntar a tabela primária com a tabela relacionada. Você pode especificar um tipo de join diferente (ex. RIGHT JOIN) através do seu terceiro parâmetro $joinType. Se o tipo de join que você quer for INNER JOIN, você pode apenas chamar innerJoinWith().

Chamando joinWith() os dados relacionais serão carregados antecipadamente por padrão. Se você não quiser trazer o dados relacionados, você pode especificar o segundo parâmetro $eagerLoading como falso.

Assim como with(), você pode juntar uma ou várias relações; você pode personalizar as queries relacionais dinamicamente; você pode utilizar join com relações aninhadas; e pode misturar o uso de with() e joinWith(). Por exemplo,

$customers = Customer::find()->joinWith([
   'orders' => function ($query) {
       $query->andWhere(['>', 'subtotal', 100]);
   },
])->with('country')
   ->all();

Algumas vezes ao juntar duas tabelas, você pode precisar especificar alguma condição extra na estrutura SQL ON do JOIN. Isto pode ser feito chamando o método yii\db\ActiveQuery::onCondition() como a seguir:

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()->joinWith([
   'orders' => function ($query) {
       $query->onCondition(['order.status' => Order::STATUS_ACTIVE]);
   },
])->all();

A query acima retorna todos customers, e para cada customer retorna todos orders ativos. Note que isto difere do nosso primeiro exemplo onde retornávamos apenas customers que tinham no mínimo um order ativo.

Observação: Quando yii\db\ActiveQuery é especificada com uma condição usando onCondition(), a condição será colocada no ON se a query tiver um JOIN. Se a query não utilizar JOIN, a condição será colocada no WHERE da query.

Relações Inversas

Declarações de relação são geralmente recíprocas entre duas classes de Active Record. Por exemplo, Customer se relaciona com Order através da relação orders, e Order se relaciona com Customer através da relação customer.

class Customer extends ActiveRecord
{
   public function getOrders()
   {
       return $this->hasMany(Order::className(), ['customer_id' => 'id']);
   }
}

class Order extends ActiveRecord
{
   public function getCustomer()
   {
       return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
   }
}

Agora considere a seguinte parte do código:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SELECT * FROM `customer` WHERE `id` = 123
$customer2 = $order->customer;

// displays "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

Podemos pensar que $customer e $customer2 são a mesma coisa, mas não são! Na verdade eles contêm a mesma informação de customer, mas são objetos diferentes. Ao acessar $order->customer, uma instrução SQL extra é executada para popular um novo objeto $customer2.

Para evitar esta redundância de execução de SQL no exemplo acima, devemos dizer para o Yii que customer é uma relação inversa de orders chamando o método inverseOf() como mostrado abaixo:

class Customer extends ActiveRecord
{
   public function getOrders()
   {
       return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
   }
}

Com esta modificação na declaração de relação, podemos ter:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// Nenhum SQL será executado
$customer2 = $order->customer;

// exibe "same"
echo $customer2 === $customer ? 'same' : 'not the same';

Observação: relações inversas não podem ser definidas por relações que envolvam uma tabela de junção. Isto é, se uma relação for definida com via() ou viaTable(), você não deve chamar inverseOf().

Salvando Relações

Ao trabalhar com dados relacionais, muitas vezes você precisa estabelecer relações entre dados diferentes ou destruir relações existentes. Isso requer a definição de valores apropriados para as colunas que definem as relações. Usando Active Record, você pode acabar escrevendo o seguinte código:

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

// definindo o atributo que define a relação "customer" em "Order"
$order->customer_id = $customer->id;
$order->save();

Active Record fornece o método link() que lhe permite realizar essa tarefa de uma forma mais elegante:

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

$order->link('customer', $customer);

O método link() requer que você especifique o nome da relação e a instância de Active Record alvo cuja relação deve ser estabelecida. O método irá modificar os valores dos atributos que apontam para duas instâncias de Active Record e salvá-los no banco de dados. No exemplo acima, o atributo customer_id da instância Order será definido para ser o valor do atributo id da instância Customer e depois salvá-lo no banco de dados.

Observação: Você não pode conectar duas instâncias de Active Record recém-criadas.

Os benefícios de usar link() é ainda mais óbvio quando uma relação é definida por meio de uma tabela de junção. Por exemplo, você pode usar o código a seguir para ligar uma instância de Order com uma instância de Item:

$order->link('items', $item);

O código acima irá inserir automaticamente uma linha na tabela de junção order_item para relacionar a order com o item.

Observação: O método link() não realizará nenhuma validação de dados ao salvar a instância de Active Record afetada. É de sua responsabilidade validar todos os dados de entrada antes de chamar esse método.

A operação inversa para link() é unlink() que quebra uma relação existente entre duas instâncias de Active Record. Por exemplo,

$customer = Customer::find()->with('orders')->where(['id' => 123])->one();
$customer->unlink('orders', $customer->orders[0]);

Por padrão, o método unlink() irá definir o(s) valor(es) da(s) chave(s) estrangeira(s) que especificam a relação existente para null. Você pode, entretanto, optar por excluir a linha da tabela que contém a chave estrangeira passando o parâmetro $delete como true para o método.

Quando uma tabela de junção está envolvida numa relação, chamar o método unlink() fará com que as chaves estrangeiras na tabela de junção sejam apagadas, ou a deleção da linha correspondente na tabela de junção se $delete for true.

Relações Entre Banco de Dados Diferentes

O Active Record lhe permite declarar relações entre classes Active Record que são alimentados por diferentes bancos de dados. As bases de dados podem ser de diferentes tipos (ex. MySQL e PostgreSQL, ou MS SQL e MongoDB) e podem rodar em diferentes servidores. Você pode usar a mesma sintaxe para executar consultas relacionais. Por exemplo,

// Customer está associado a tabela "customer" em um banco de dados relacional (ex. MySQL)
class Customer extends \yii\db\ActiveRecord
{
   public static function tableName()
   {
       return 'customer';
   }

   public function getComments()
   {
       // a customer tem muitos comments
       return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
   }
}

// Comment está associado com a coleção "comment" em um banco de dados MongoDB
class Comment extends \yii\mongodb\ActiveRecord
{
   public static function collectionName()
   {
       return 'comment';
   }

   public function getCustomer()
   {
       // um comment tem um customer
       return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
   }
}

$customers = Customer::find()->with('comments')->all();

Você pode usar a maioria dos recursos de consulta relacional que foram descritos nesta seção.

Observação: O uso de joinWith() está limitado às bases de dados que permitem JOIN entre diferentes bancos de dados (cross-database). Por esta razão, você não pode usar este método no exemplo acima porque MongoDB não suporta JOIN.

Personalizando Classes de Consulta

Por padrão, todas as consultas do Active Record são suportadas por yii\db\ActiveQuery. Para usar uma classe de consulta customizada em uma classe Active Record, você deve sobrescrever o método yii\db\ActiveRecord::find() e retornar uma instância da sua classe de consulta customizada. Por exemplo,

namespace app\models;

use yii\db\ActiveRecord;
use yii\db\ActiveQuery;

class Comment extends ActiveRecord
{
   public static function find()
   {
       return new CommentQuery(get_called_class());
   }
}

class CommentQuery extends ActiveQuery
{
   // ...
}

Agora sempre que você realizar uma consulta (ex. find(), findOne()) ou definir uma relação (ex. hasOne()) com Comment, você estará trabalhando com a instância de CommentQuery em vez de ActiveQuery.

Dica: Em grandes projetos, é recomendável que você use classes de consulta personalizadas para manter a maioria dos códigos de consultas relacionadas de modo que a classe Active Record possa se manter limpa.

Você pode personalizar uma classe de consulta de várias formas criativas afim de melhorar a sua experiência na construção de consultas. Por exemplo, você pode definir um novo método query building em uma classe de consulta customizada:

class CommentQuery extends ActiveQuery
{
   public function active($state = true)
   {
       return $this->andWhere(['active' => $state]);
   }
}

Observação: Em vez de chamar where(), você geralmente deve chamar andWhere() ou orWhere() para anexar condições adicionais ao definir um novo método de query building de modo que as condições existentes não serão sobrescritas.

Isto permite que você escreva códigos de query building conforme o exemplo abaixo:

$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();

Você também pode usar um novo método de query building ao definir relações sobre Comment ou executar uma consulta relacional:

class Customer extends \yii\db\ActiveRecord
{
   public function getActiveComments()
   {
       return $this->hasMany(Comment::className(), ['customer_id' => 'id'])->active();
   }
}

$customers = Customer::find()->with('activeComments')->all();

// ou alternativamente

$customers = Customer::find()->with([
   'comments' => function($q) {
       $q->active();
   }
])->all();

Observação: No Yii 1.1, existe um conceito chamado scope. Scope já não é suportado no Yii 2.0, e você deve usar classes de consulta personalizadas e métodos de consulta para atingir o mesmo objetivo.

Selecionando Campos Extras

Quando uma instância de Active Record é populada através do resultado de uma consulta, seus atributos são preenchidos pelos valores correspondentes das colunas do conjunto de dados recebidos.

Você é capaz de buscar colunas ou valores adicionais da consulta e armazená-lo dentro do Active Record. Por exemplo, suponha que tenhamos uma tabela chamada 'room', que contém informações sobre quartos disponíveis em um hotel. Cada 'room' grava informações sobre seu tamanho geométrico usando os campos 'length', 'width', 'height'. Imagine que precisemos de uma lista de todos os quartos disponíveis com o seu volume em ordem decrescente. Então você não pode calcular o volume usando PHP, porque precisamos ordenar os registros pelo seu valor, mas você também quer que o 'volume' seja mostrado na lista. Para alcançar este objetivo, você precisa declarar um campo extra na sua classe Active Record 'Room', que vai armazenar o valor do campo 'volume':

class Room extends \yii\db\ActiveRecord
{
   public $volume;

   // ...
}

Então você precisa criar uma consulta, que calcule o volume da sala e realize a ordenação:

$rooms = Room::find()
   ->select([
       '{{room}}.*', // select all columns
       '([[length]] * [[width]].* [[height]]) AS volume', // calculate a volume
   ])
   ->orderBy('volume DESC') // apply sort
   ->all();

foreach ($rooms as $room) {
   echo $room->volume; // contains value calculated by SQL
}

Capacidade de selecionar campos extras pode ser extremamente útil para consultas de agregação. Suponha que você precise exibir uma lista de clientes com a contagem de seus pedidos. Em primeiro lugar, você precisa declarar uma classe Customer com uma relação com 'orders' e um campo extra para armazenamento da contagem:

class Customer extends \yii\db\ActiveRecord
{
   public $ordersCount;

   // ...

   public function getOrders()
   {
       return $this->hasMany(Order::className(), ['customer_id' => 'id']);
   }
}

Então você pode criar uma consulta que faça um JOIN com 'orders' e calcule a quantidade:

$customers = Customer::find()
   ->select([
       '{{customer}}.*', // select all customer fields
       'COUNT({{order}}.id) AS ordersCount' // calculate orders count
   ])
   ->joinWith('orders') // ensure table junction
   ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works
   ->all();